From 8a42a1bc6ca0e054425dcdb640eb422432e14ca7 Mon Sep 17 00:00:00 2001 From: jeffvli Date: Tue, 27 Dec 2022 13:52:50 -0800 Subject: [PATCH] Add song list functionality --- .../components/jellyfin-song-filters.tsx | 133 ++++ .../components/navidrome-song-filters.tsx | 68 ++ .../songs/components/song-list-content.tsx | 207 ++++++ .../songs/components/song-list-header.tsx | 608 ++++++++++++------ .../features/songs/routes/song-list-route.tsx | 109 +--- src/renderer/store/auth.store.ts | 10 +- src/renderer/store/song.store.ts | 38 +- 7 files changed, 851 insertions(+), 322 deletions(-) create mode 100644 src/renderer/features/songs/components/jellyfin-song-filters.tsx create mode 100644 src/renderer/features/songs/components/navidrome-song-filters.tsx create mode 100644 src/renderer/features/songs/components/song-list-content.tsx diff --git a/src/renderer/features/songs/components/jellyfin-song-filters.tsx b/src/renderer/features/songs/components/jellyfin-song-filters.tsx new file mode 100644 index 00000000..aa0e96e6 --- /dev/null +++ b/src/renderer/features/songs/components/jellyfin-song-filters.tsx @@ -0,0 +1,133 @@ +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 debounce from 'lodash/debounce'; +import { useGenreList } from '/@/renderer/features/genres'; + +interface JellyfinSongFiltersProps { + handleFilterChange: (filters: SongListFilter) => void; +} + +export const JellyfinSongFilters = ({ handleFilterChange }: JellyfinSongFiltersProps) => { + const { filter } = useSongListStore(); + const setFilters = useSetSongFilters(); + + // TODO - eventually replace with /items/filters endpoint to fetch genres and tags specific to the selected library + const genreListQuery = useGenreList(null); + + const genreList = useMemo(() => { + if (!genreListQuery?.data) return []; + return genreListQuery.data.map((genre) => ({ + label: genre.name, + value: genre.id, + })); + }, [genreListQuery.data]); + + const selectedGenres = useMemo(() => { + return filter.jfParams?.genreIds?.split(','); + }, [filter.jfParams?.genreIds]); + + const toggleFilters = [ + { + label: 'Is favorited', + onChange: (e: ChangeEvent) => { + const updatedFilters = setFilters({ + jfParams: { + ...filter.jfParams, + includeItemTypes: 'Audio', + isFavorite: e.currentTarget.checked ? true : undefined, + }, + }); + handleFilterChange(updatedFilters); + }, + value: filter.jfParams?.isFavorite, + }, + ]; + + const handleMinYearFilter = debounce((e: number | undefined) => { + if (e && (e < 1700 || e > 2300)) return; + const updatedFilters = setFilters({ + jfParams: { + ...filter.jfParams, + includeItemTypes: 'Audio', + minYear: e, + }, + }); + handleFilterChange(updatedFilters); + }, 500); + + const handleMaxYearFilter = debounce((e: number | undefined) => { + if (e && (e < 1700 || e > 2300)) return; + const updatedFilters = setFilters({ + jfParams: { + ...filter.jfParams, + includeItemTypes: 'Audio', + maxYear: e, + }, + }); + 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', + }, + }); + handleFilterChange(updatedFilters); + }, 250); + + return ( + + {toggleFilters.map((filter) => ( + + {filter.label} + + + ))} + + + Year range + + + + + + + + Genres + + + + ); +}; diff --git a/src/renderer/features/songs/components/navidrome-song-filters.tsx b/src/renderer/features/songs/components/navidrome-song-filters.tsx new file mode 100644 index 00000000..0fc67095 --- /dev/null +++ b/src/renderer/features/songs/components/navidrome-song-filters.tsx @@ -0,0 +1,68 @@ +import { ChangeEvent } from 'react'; +import { Divider, Group, Stack } from '@mantine/core'; +import { NumberInput, Switch, Text } from '/@/renderer/components'; +import { SongListFilter, useSetSongFilters, useSongListStore } from '/@/renderer/store'; +import debounce from 'lodash/debounce'; + +interface NavidromeSongFiltersProps { + handleFilterChange: (filters: SongListFilter) => void; +} + +export const NavidromeSongFilters = ({ handleFilterChange }: NavidromeSongFiltersProps) => { + const { filter } = useSongListStore(); + const setFilters = useSetSongFilters(); + + const toggleFilters = [ + { + label: 'Is favorited', + onChange: (e: ChangeEvent) => { + const updatedFilters = setFilters({ + ndParams: { ...filter.ndParams, starred: e.currentTarget.checked ? true : undefined }, + }); + handleFilterChange(updatedFilters); + }, + value: filter.ndParams?.starred, + }, + ]; + + const handleYearFilter = debounce((e: number | undefined) => { + const updatedFilters = setFilters({ + ndParams: { + ...filter.ndParams, + year: e, + }, + }); + + handleFilterChange(updatedFilters); + }, 500); + + return ( + + {toggleFilters.map((filter) => ( + + {filter.label} + + + ))} + + + Year + + + + ); +}; diff --git a/src/renderer/features/songs/components/song-list-content.tsx b/src/renderer/features/songs/components/song-list-content.tsx new file mode 100644 index 00000000..371b41a7 --- /dev/null +++ b/src/renderer/features/songs/components/song-list-content.tsx @@ -0,0 +1,207 @@ +import { MutableRefObject, useCallback, useMemo } from 'react'; +import type { + BodyScrollEvent, + ColDef, + GridReadyEvent, + IDatasource, + PaginationChangedEvent, +} 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 { + getColumnDefs, + TablePagination, + VirtualGridAutoSizerContainer, + VirtualTable, +} from '/@/renderer/components'; +import { useSongList } from '/@/renderer/features/songs/queries/song-list-query'; +import { + useCurrentServer, + useSetSongTable, + useSetSongTablePagination, + useSongListStore, + useSongTablePagination, +} from '/@/renderer/store'; +import { ListDisplayType } from '/@/renderer/types'; +import { AnimatePresence } from 'framer-motion'; +import debounce from 'lodash/debounce'; + +interface SongListContentProps { + tableRef: MutableRefObject; +} + +export const SongListContent = ({ tableRef }: SongListContentProps) => { + const queryClient = useQueryClient(); + const server = useCurrentServer(); + const page = useSongListStore(); + + const pagination = useSongTablePagination(); + const setPagination = useSetSongTablePagination(); + const setTable = useSetSongTable(); + + const isPaginationEnabled = page.display === ListDisplayType.TABLE_PAGINATED; + + const checkSongList = useSongList({ + limit: 1, + startIndex: 0, + ...page.filter, + }); + + const columnDefs = 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.songs.list(server?.id || '', { + limit, + startIndex, + ...page.filter, + }); + + const songsRes = await queryClient.fetchQuery(queryKey, async ({ signal }) => + api.controller.getSongList({ + query: { + limit, + startIndex, + ...page.filter, + }, + server, + signal, + }), + ); + + const songs = api.normalize.songList(songsRes, server); + params.successCallback(songs?.items || [], songsRes?.totalRecordCount); + }, + rowCount: undefined, + }; + params.api.setDatasource(dataSource); + params.api.ensureIndexVisible(page.table.scrollOffset || 0, 'top'); + }, + [page.filter, page.table.scrollOffset, queryClient, server], + ); + + const onPaginationChanged = useCallback( + (event: PaginationChangedEvent) => { + if (!isPaginationEnabled || !event.api) return; + + // Scroll to top of page on pagination change + const currentPageStartIndex = pagination.currentPage * pagination.itemsPerPage; + event.api?.ensureIndexVisible(currentPageStartIndex, 'top'); + + setPagination({ + 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 handleColumnMove = 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.getColDef().width, + }), + }); + } + } + + setTable({ columns: updatedColumns }); + }, [page.table.autoFit, page.table.columns, setTable, tableRef]); + + const handleScroll = debounce((e: BodyScrollEvent) => { + const scrollOffset = Number((e.top / page.table.rowHeight).toFixed(0)); + setTable({ scrollOffset }); + }, 200); + + return ( + + + data.data.uniqueId} + infiniteInitialRowCount={checkSongList.data?.totalRecordCount || 10} + pagination={isPaginationEnabled} + paginationAutoPageSize={isPaginationEnabled} + rowBuffer={20} + rowHeight={page.table.rowHeight || 40} + rowModelType="infinite" + rowSelection="multiple" + // onBodyScroll={handleScroll} + onBodyScrollEnd={handleScroll} + onCellContextMenu={(e) => console.log('context', e)} + onColumnMoved={handleColumnMove} + onGridReady={onGridReady} + onGridSizeChanged={handleGridSizeChange} + onPaginationChanged={onPaginationChanged} + /> + + + {page.display === ListDisplayType.TABLE_PAGINATED && ( + + )} + + + ); +}; diff --git a/src/renderer/features/songs/components/song-list-header.tsx b/src/renderer/features/songs/components/song-list-header.tsx index e9996738..c7c9e0f1 100644 --- a/src/renderer/features/songs/components/song-list-header.tsx +++ b/src/renderer/features/songs/components/song-list-header.tsx @@ -1,40 +1,76 @@ -import type { MouseEvent } from 'react'; -import { useCallback } from 'react'; -import { Group } from '@mantine/core'; -import { Button, Slider, PageHeader, DropdownMenu } from '/@/renderer/components'; -import throttle from 'lodash/throttle'; -import { RiArrowDownSLine } from 'react-icons/ri'; -import { SongListSort, SortOrder } from '/@/renderer/api/types'; -import { useCurrentServer, useAppStoreActions, useSongRouteStore } from '/@/renderer/store'; -import { CardDisplayType } from '/@/renderer/types'; +import type { IDatasource } from '@ag-grid-community/core'; +import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; +import { Flex, Group } from '@mantine/core'; +import debounce from 'lodash/debounce'; +import { ChangeEvent, MouseEvent, MutableRefObject, useCallback } from 'react'; +import { + RiArrowDownSLine, + RiFilter3Line, + RiFolder2Line, + RiMoreFill, + RiSortAsc, + RiSortDesc, +} from 'react-icons/ri'; +import { api } from '/@/renderer/api'; +import { queryKeys } from '/@/renderer/api/query-keys'; +import { ServerType, SongListSort, SortOrder } from '/@/renderer/api/types'; +import { + Button, + DropdownMenu, + PageHeader, + SearchInput, + Slider, + TextTitle, + Switch, + MultiSelect, + Text, + SONG_TABLE_COLUMNS, +} from '/@/renderer/components'; +import { 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'; +import { queryClient } from '/@/renderer/lib/react-query'; +import { + SongListFilter, + useCurrentServer, + useSetSongFilters, + useSetSongStore, + useSetSongTable, + useSetSongTablePagination, + useSongListStore, +} from '/@/renderer/store'; +import { ListDisplayType, TableColumn } from '/@/renderer/types'; const FILTERS = { jellyfin: [ - { name: 'Album Artist', value: SongListSort.ALBUM_ARTIST }, - { name: 'Artist', value: SongListSort.ARTIST }, - { name: 'Duration', value: SongListSort.DURATION }, - { name: 'Name', value: SongListSort.NAME }, - { name: 'Name', value: SongListSort.PLAY_COUNT }, - { name: 'Random', value: SongListSort.RANDOM }, - { name: 'Recently Added', value: SongListSort.RECENTLY_ADDED }, - { name: 'Recently Played', value: SongListSort.RECENTLY_PLAYED }, - { name: 'Release Date', value: SongListSort.RELEASE_DATE }, + { defaultOrder: SortOrder.ASC, name: 'Album', value: SongListSort.ALBUM }, + { defaultOrder: SortOrder.ASC, name: 'Album Artist', value: SongListSort.ALBUM_ARTIST }, + { defaultOrder: SortOrder.ASC, name: 'Artist', value: SongListSort.ARTIST }, + { defaultOrder: SortOrder.ASC, name: 'Duration', value: SongListSort.DURATION }, + { defaultOrder: SortOrder.ASC, name: 'Most Played', value: SongListSort.PLAY_COUNT }, + { defaultOrder: SortOrder.ASC, name: 'Name', value: SongListSort.NAME }, + { defaultOrder: SortOrder.ASC, name: 'Random', value: SongListSort.RANDOM }, + { defaultOrder: SortOrder.ASC, name: 'Recently Added', value: SongListSort.RECENTLY_ADDED }, + { defaultOrder: SortOrder.ASC, name: 'Recently Played', value: SongListSort.RECENTLY_PLAYED }, + { defaultOrder: SortOrder.ASC, name: 'Release Date', value: SongListSort.RELEASE_DATE }, ], navidrome: [ - { name: 'Album Artist', value: SongListSort.ALBUM_ARTIST }, - { name: 'Artist', value: SongListSort.ARTIST }, - { name: 'BPM', value: SongListSort.BPM }, - { name: 'Channels', value: SongListSort.CHANNELS }, - { name: 'Comment', value: SongListSort.COMMENT }, - { name: 'Duration', value: SongListSort.DURATION }, - { name: 'Favorited', value: SongListSort.FAVORITED }, - { name: 'Genre', value: SongListSort.GENRE }, - { name: 'Name', value: SongListSort.NAME }, - { name: 'Play Count', value: SongListSort.PLAY_COUNT }, - { name: 'Rating', value: SongListSort.RATING }, - { name: 'Recently Added', value: SongListSort.RECENTLY_ADDED }, - { name: 'Recently Played', value: SongListSort.RECENTLY_PLAYED }, - { name: 'Year', value: SongListSort.YEAR }, + { defaultOrder: SortOrder.ASC, name: 'Album', value: SongListSort.ALBUM }, + { defaultOrder: SortOrder.ASC, name: 'Album Artist', value: SongListSort.ALBUM_ARTIST }, + { defaultOrder: SortOrder.ASC, name: 'Artist', value: SongListSort.ARTIST }, + { defaultOrder: SortOrder.DESC, name: 'BPM', value: SongListSort.BPM }, + { defaultOrder: SortOrder.ASC, name: 'Channels', value: SongListSort.CHANNELS }, + { defaultOrder: SortOrder.ASC, name: 'Comment', value: SongListSort.COMMENT }, + { defaultOrder: SortOrder.DESC, name: 'Duration', value: SongListSort.DURATION }, + { defaultOrder: SortOrder.DESC, name: 'Favorited', value: SongListSort.FAVORITED }, + { defaultOrder: SortOrder.ASC, name: 'Genre', value: SongListSort.GENRE }, + { defaultOrder: SortOrder.ASC, name: 'Name', value: SongListSort.NAME }, + { defaultOrder: SortOrder.DESC, name: 'Play Count', value: SongListSort.PLAY_COUNT }, + { defaultOrder: SortOrder.DESC, name: 'Rating', value: SongListSort.RATING }, + { defaultOrder: SortOrder.DESC, name: 'Recently Added', value: SongListSort.RECENTLY_ADDED }, + { defaultOrder: SortOrder.DESC, name: 'Recently Played', value: SongListSort.RECENTLY_PLAYED }, + { defaultOrder: SortOrder.DESC, name: 'Year', value: SongListSort.YEAR }, ], }; @@ -43,219 +79,363 @@ const ORDER = [ { name: 'Descending', value: SortOrder.DESC }, ]; -export const SongListHeader = () => { +interface SongListHeaderProps { + tableRef: MutableRefObject; +} + +export const SongListHeader = ({ tableRef }: SongListHeaderProps) => { const server = useCurrentServer(); - const { setPage } = useAppStoreActions(); - const page = useSongRouteStore(); - const filters = page.list.filter; + const page = useSongListStore(); + const setPage = useSetSongStore(); + const setFilter = useSetSongFilters(); + const setTable = useSetSongTable(); + const cq = useContainerQuery(); + + const musicFoldersQuery = useMusicFolders(); const sortByLabel = (server?.type && (FILTERS[server.type as keyof typeof FILTERS] as { name: string; value: string }[]).find( - (f) => f.value === filters.sortBy, + (f) => f.value === page.filter.sortBy, )?.name) || 'Unknown'; - const sortOrderLabel = ORDER.find((s) => s.value === filters.sortOrder)?.name; + const sortOrderLabel = ORDER.find((s) => s.value === page.filter.sortOrder)?.name; - const setSize = throttle( - (e: number) => - setPage('songs', { - ...page, - list: { ...page.list, size: e }, - }), - 200, + const handleFilterChange = useCallback( + async (filters?: SongListFilter) => { + const dataSource: IDatasource = { + getRows: async (params) => { + const limit = params.endRow - params.startRow; + const startIndex = params.startRow; + + const pageFilters = filters || page.filter; + + const queryKey = queryKeys.songs.list(server?.id || '', { + limit, + startIndex, + ...pageFilters, + }); + + const songsRes = await queryClient.fetchQuery(queryKey, async ({ signal }) => + api.controller.getSongList({ + query: { + limit, + startIndex, + ...pageFilters, + }, + server, + signal, + }), + ); + + const songs = api.normalize.songList(songsRes, server); + params.successCallback(songs?.items || [], songsRes?.totalRecordCount); + }, + rowCount: undefined, + }; + tableRef.current?.api.setDatasource(dataSource); + tableRef.current?.api.purgeInfiniteCache(); + tableRef.current?.api.ensureIndexVisible(0, 'top'); + }, + [page.filter, server, tableRef], ); - const handleSetFilter = useCallback( + const handleSetSortBy = useCallback( + (e: MouseEvent) => { + if (!e.currentTarget?.value || !server?.type) return; + + const sortOrder = FILTERS[server.type as keyof typeof FILTERS].find( + (f) => f.value === e.currentTarget.value, + )?.defaultOrder; + + const updatedFilters = setFilter({ + sortBy: e.currentTarget.value as SongListSort, + sortOrder: sortOrder || SortOrder.ASC, + }); + + handleFilterChange(updatedFilters); + }, + [handleFilterChange, server?.type, setFilter], + ); + + const handleSetMusicFolder = useCallback( (e: MouseEvent) => { if (!e.currentTarget?.value) return; - setPage('songs', { - list: { - ...page.list, - filter: { - ...page.list.filter, - sortBy: e.currentTarget.value as SongListSort, - }, - }, - }); + + let updatedFilters = null; + if (e.currentTarget.value === String(page.filter.musicFolderId)) { + updatedFilters = setFilter({ musicFolderId: undefined }); + } else { + updatedFilters = setFilter({ musicFolderId: e.currentTarget.value }); + } + + handleFilterChange(updatedFilters); }, - [page.list, setPage], + [handleFilterChange, page.filter.musicFolderId, setFilter], ); - const handleSetOrder = useCallback( - (e: MouseEvent) => { - if (!e.currentTarget?.value) return; - setPage('songs', { - list: { - ...page.list, - filter: { - ...page.list.filter, - sortOrder: e.currentTarget.value as SortOrder, - }, - }, - }); - }, - [page.list, setPage], - ); + const handleToggleSortOrder = useCallback(() => { + const newSortOrder = page.filter.sortOrder === SortOrder.ASC ? SortOrder.DESC : SortOrder.ASC; + const updatedFilters = setFilter({ sortOrder: newSortOrder }); + handleFilterChange(updatedFilters); + }, [page.filter.sortOrder, handleFilterChange, setFilter]); + + const setPagination = useSetSongTablePagination(); const handleSetViewType = useCallback( (e: MouseEvent) => { if (!e.currentTarget?.value) return; - const type = e.currentTarget.value; - if (type === CardDisplayType.CARD) { - setPage('songs', { - ...page, - list: { - ...page.list, - display: CardDisplayType.CARD, - type: 'grid', - }, - }); - } else if (type === CardDisplayType.POSTER) { - setPage('songs', { - ...page, - list: { - ...page.list, - display: CardDisplayType.POSTER, - type: 'grid', - }, - }); - } else { - setPage('songs', { - ...page, - list: { - ...page.list, - type: 'list', - }, - }); + const display = e.currentTarget.value as ListDisplayType; + setPage({ list: { ...page, display: e.currentTarget.value as ListDisplayType } }); + + if (display === ListDisplayType.TABLE) { + tableRef.current?.api.paginationSetPageSize(tableRef.current.props.infiniteInitialRowCount); + setPagination({ currentPage: 0 }); + } else if (display === ListDisplayType.TABLE_PAGINATED) { + setPagination({ currentPage: 0 }); } }, - [page, setPage], + [page, setPage, setPagination, tableRef], ); + const handleSearch = debounce((e: ChangeEvent) => { + const updatedFilters = setFilter({ + searchTerm: e.target.value === '' ? undefined : e.target.value, + }); + handleFilterChange(updatedFilters); + }, 500); + + const handleTableColumns = (values: TableColumn[]) => { + const existingColumns = page.table.columns; + + if (values.length === 0) { + return setTable({ + columns: [], + }); + } + + // If adding a column + if (values.length > existingColumns.length) { + const newColumn = { column: values[values.length - 1], width: 100 }; + + return setTable({ columns: [...existingColumns, newColumn] }); + } + + // 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 }); + }; + + const handleAutoFitColumns = (e: ChangeEvent) => { + setTable({ autoFit: e.currentTarget.checked }); + + if (e.currentTarget.checked) { + tableRef.current?.api.sizeColumnsToFit(); + } + }; + + const handleRowHeight = (e: number) => { + setTable({ rowHeight: e }); + }; + return ( - - + - - - - - - - - - - Card - - - Poster - - - List - - - - - - - - - {FILTERS[server?.type as keyof typeof FILTERS].map((filter) => ( - + + - - - {ORDER.map((sort) => ( + + Tracks + + + + + Display type - {sort.name} + Table - ))} - - - - - - - {/* - {serverFolders?.map((folder) => ( - - {folder.name} - - ))} - */} - - + + Table (paginated) + + + Item Size + + + + Table Columns + + column.column)} + width={300} + onChange={handleTableColumns} + /> + + + + Auto Fit Columns + + + + + + + + + + + {FILTERS[server?.type as keyof typeof FILTERS].map((filter) => ( + + {filter.name} + + ))} + + + + {server?.type === ServerType.JELLYFIN && ( + + + + + + {musicFoldersQuery.data?.map((folder) => ( + + {folder.name} + + ))} + + + )} + + + + + + {server?.type === ServerType.NAVIDROME ? ( + + ) : ( + + )} + + + + + + + + Play + Play last + Play next + Add to playlist + + + + + + + ); }; diff --git a/src/renderer/features/songs/routes/song-list-route.tsx b/src/renderer/features/songs/routes/song-list-route.tsx index 75f18124..777ef0ba 100644 --- a/src/renderer/features/songs/routes/song-list-route.tsx +++ b/src/renderer/features/songs/routes/song-list-route.tsx @@ -1,113 +1,18 @@ -import { useCallback, useMemo } from 'react'; -import { - VirtualGridContainer, - VirtualGridAutoSizerContainer, - VirtualTable, - getColumnDefs, -} from '/@/renderer/components'; -import type { ColDef, GridReadyEvent, IDatasource } from '@ag-grid-community/core'; +import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; +import { useRef } from 'react'; +import { VirtualGridContainer } from '/@/renderer/components'; import { AnimatedPage } from '/@/renderer/features/shared'; -import { useTableSettings } from '/@/renderer/store/settings.store'; +import { SongListContent } from '/@/renderer/features/songs/components/song-list-content'; import { SongListHeader } from '/@/renderer/features/songs/components/song-list-header'; -import { useSongList } from '/@/renderer/features/songs/queries/song-list-query'; -import { SongListSort, SortOrder } from '/@/renderer/api/types'; -import { queryKeys } from '/@/renderer/api/query-keys'; -import { useCurrentServer, useSongRouteStore } from '/@/renderer/store'; -import { controller } from '/@/renderer/api/controller'; -import { api } from '/@/renderer/api'; -import { useQueryClient } from '@tanstack/react-query'; const TrackListRoute = () => { - const queryClient = useQueryClient(); - const server = useCurrentServer(); - const page = useSongRouteStore(); - const filters = page.list.filter; - const tableConfig = useTableSettings('songs'); - - const checkSongList = useSongList({ - limit: 1, - sortBy: SongListSort.NAME, - sortOrder: SortOrder.ASC, - startIndex: 0, - }); - - const columnDefs = useMemo(() => getColumnDefs(tableConfig.columns), [tableConfig.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.songs.list(server?.id || '', { - limit, - startIndex, - ...filters, - }); - - const songsRes = await queryClient.fetchQuery(queryKey, async ({ signal }) => - controller.getSongList({ - query: { - limit, - sortBy: filters.sortBy, - sortOrder: filters.sortOrder, - startIndex, - }, - server, - signal, - }), - ); - - const songs = api.normalize.songList(songsRes, server); - - params.successCallback(songs?.items || [], -1); - }, - rowCount: undefined, - }; - params.api.setDatasource(dataSource); - }, - [filters, queryClient, server], - ); + const tableRef = useRef(null); return ( - - - {!checkSongList.isLoading && ( - data.data.uniqueId} - infiniteInitialRowCount={checkSongList.data?.totalRecordCount} - rowBuffer={20} - rowHeight={tableConfig.rowHeight || 40} - rowModelType="infinite" - rowSelection="multiple" - onCellContextMenu={(e) => console.log('context', e)} - onGridReady={onGridReady} - /> - )} - + + ); diff --git a/src/renderer/store/auth.store.ts b/src/renderer/store/auth.store.ts index 3968479b..eb1f8ccd 100644 --- a/src/renderer/store/auth.store.ts +++ b/src/renderer/store/auth.store.ts @@ -3,8 +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, SortOrder } from '/@/renderer/api/types'; +import { AlbumListSort, SongListSort, SortOrder } from '/@/renderer/api/types'; import { useAlbumStore } from '/@/renderer/store/album.store'; +import { useSongStore } from '/@/renderer/store/song.store'; import { ServerListItem } from '/@/renderer/types'; export interface AuthState { @@ -48,7 +49,12 @@ export const useAuthStore = create()( useAlbumStore.getState().actions.setFilters({ musicFolderId: undefined, sortBy: AlbumListSort.RECENTLY_ADDED, - sortOrder: SortOrder.ASC, + sortOrder: SortOrder.DESC, + }); + useSongStore.getState().actions.setFilters({ + musicFolderId: undefined, + sortBy: SongListSort.RECENTLY_ADDED, + sortOrder: SortOrder.DESC, }); } }); diff --git a/src/renderer/store/song.store.ts b/src/renderer/store/song.store.ts index 2bebe391..eb1d8abb 100644 --- a/src/renderer/store/song.store.ts +++ b/src/renderer/store/song.store.ts @@ -4,9 +4,10 @@ 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 } from '/@/renderer/types'; +import { ListDisplayType, TableColumn, TablePagination } from '/@/renderer/types'; type TableProps = { + pagination: TablePagination; scrollOffset: number; } & DataTableProps; @@ -16,16 +17,18 @@ type ListProps = { table: TableProps; }; -type AlbumListFilter = Omit; +export type SongListFilter = Omit; interface SongState { - list: ListProps; + list: ListProps; } export interface SongSlice extends SongState { actions: { - setFilters: (data: Partial) => void; + setFilters: (data: Partial) => SongListFilter; setStore: (data: Partial) => void; + setTable: (data: Partial) => void; + setTablePagination: (data: Partial) => void; }; } @@ -38,10 +41,22 @@ export const useSongStore = create()( 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, @@ -78,6 +93,12 @@ export const useSongStore = create()( width: 100, }, ], + pagination: { + currentPage: 1, + itemsPerPage: 100, + totalItems: 1, + totalPages: 1, + }, rowHeight: 60, scrollOffset: 0, }, @@ -99,8 +120,17 @@ 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 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);