From 38118e74aeb756798a22a2fe45f3275e4f82fe54 Mon Sep 17 00:00:00 2001 From: jeffvli Date: Sun, 5 Feb 2023 22:41:47 -0800 Subject: [PATCH] Update to new list header style --- .../components/album-list-header-filters.tsx | 587 ++++++++++++++++++ .../albums/components/album-list-header.tsx | 468 ++------------ .../album-artist-list-header-filters.tsx | 470 ++++++++++++++ .../components/album-artist-list-header.tsx | 405 ++---------- .../components/playlist-list-content.tsx | 12 +- .../playlist-list-header-filters.tsx | 325 ++++++++++ .../components/playlist-list-header.tsx | 359 ++--------- .../playlists/routes/playlist-list-route.tsx | 25 +- .../components/song-list-header-filters.tsx | 452 ++++++++++++++ .../songs/components/song-list-header.tsx | 443 ++----------- 10 files changed, 2051 insertions(+), 1495 deletions(-) create mode 100644 src/renderer/features/albums/components/album-list-header-filters.tsx create mode 100644 src/renderer/features/artists/components/album-artist-list-header-filters.tsx create mode 100644 src/renderer/features/playlists/components/playlist-list-header-filters.tsx create mode 100644 src/renderer/features/songs/components/song-list-header-filters.tsx diff --git a/src/renderer/features/albums/components/album-list-header-filters.tsx b/src/renderer/features/albums/components/album-list-header-filters.tsx new file mode 100644 index 00000000..8bd85e67 --- /dev/null +++ b/src/renderer/features/albums/components/album-list-header-filters.tsx @@ -0,0 +1,587 @@ +import { MutableRefObject, useCallback, MouseEvent, ChangeEvent } 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 { openModal } from '@mantine/modals'; +import { useQueryClient } from '@tanstack/react-query'; +import { + RiSortAsc, + RiSortDesc, + RiFolder2Line, + RiFilter3Line, + RiMoreFill, + RiAddBoxFill, + RiPlayFill, + RiAddCircleFill, + RiRefreshLine, + RiSettings3Fill, +} from 'react-icons/ri'; +import { api } from '/@/renderer/api'; +import { queryKeys } from '/@/renderer/api/query-keys'; +import { AlbumListQuery, AlbumListSort, LibraryItem, SortOrder } from '/@/renderer/api/types'; +import { + ALBUM_TABLE_COLUMNS, + Button, + DropdownMenu, + MultiSelect, + Slider, + Switch, + 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, +} from '/@/renderer/store'; +import { ServerType, Play, ListDisplayType, TableColumn } from '/@/renderer/types'; +import { useMusicFolders } from '/@/renderer/features/shared'; +import { usePlayQueueAdd } from '/@/renderer/features/player'; + +const FILTERS = { + jellyfin: [ + { defaultOrder: SortOrder.ASC, name: 'Album Artist', value: AlbumListSort.ALBUM_ARTIST }, + { + defaultOrder: SortOrder.DESC, + name: 'Community Rating', + value: AlbumListSort.COMMUNITY_RATING, + }, + { defaultOrder: SortOrder.DESC, name: 'Critic Rating', value: AlbumListSort.CRITIC_RATING }, + { defaultOrder: SortOrder.ASC, name: 'Name', value: AlbumListSort.NAME }, + { defaultOrder: SortOrder.ASC, name: 'Random', value: AlbumListSort.RANDOM }, + { defaultOrder: SortOrder.DESC, name: 'Recently Added', value: AlbumListSort.RECENTLY_ADDED }, + { defaultOrder: SortOrder.DESC, name: 'Release Date', value: AlbumListSort.RELEASE_DATE }, + ], + navidrome: [ + { defaultOrder: SortOrder.ASC, name: 'Album Artist', value: AlbumListSort.ALBUM_ARTIST }, + { defaultOrder: SortOrder.ASC, name: 'Artist', value: AlbumListSort.ARTIST }, + { defaultOrder: SortOrder.DESC, name: 'Duration', value: AlbumListSort.DURATION }, + { defaultOrder: SortOrder.DESC, name: 'Most Played', value: AlbumListSort.PLAY_COUNT }, + { defaultOrder: SortOrder.ASC, name: 'Name', value: AlbumListSort.NAME }, + { defaultOrder: SortOrder.ASC, name: 'Random', value: AlbumListSort.RANDOM }, + { defaultOrder: SortOrder.DESC, name: 'Rating', value: AlbumListSort.RATING }, + { defaultOrder: SortOrder.DESC, name: 'Recently Added', value: AlbumListSort.RECENTLY_ADDED }, + { defaultOrder: SortOrder.DESC, name: 'Recently Played', value: AlbumListSort.RECENTLY_PLAYED }, + { defaultOrder: SortOrder.DESC, name: 'Song Count', value: AlbumListSort.SONG_COUNT }, + { defaultOrder: SortOrder.DESC, name: 'Favorited', value: AlbumListSort.FAVORITED }, + { defaultOrder: SortOrder.DESC, name: 'Year', value: AlbumListSort.YEAR }, + ], +}; + +const ORDER = [ + { name: 'Ascending', value: SortOrder.ASC }, + { name: 'Descending', value: SortOrder.DESC }, +]; + +interface AlbumListHeaderFiltersProps { + customFilters?: Partial; + gridRef: MutableRefObject; + itemCount?: number; + tableRef: MutableRefObject; +} + +export const AlbumListHeaderFilters = ({ + customFilters, + gridRef, + tableRef, + itemCount, +}: AlbumListHeaderFiltersProps) => { + const queryClient = useQueryClient(); + const server = useCurrentServer(); + + const setPage = useSetAlbumStore(); + const setFilter = useSetAlbumFilters(); + const page = useAlbumListStore(); + const filters = page.filter; + 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) || + 'Unknown'; + + const sortOrderLabel = ORDER.find((o) => o.value === filters.sortOrder)?.name || 'Unknown'; + + const fetch = useCallback( + async (skip: number, take: number, filters: AlbumListFilter) => { + const query: AlbumListQuery = { + limit: take, + startIndex: skip, + ...filters, + jfParams: { + ...filters.jfParams, + ...customFilters?.jfParams, + }, + ndParams: { + ...filters.ndParams, + ...customFilters?.ndParams, + }, + ...customFilters, + }; + + const queryKey = queryKeys.albums.list(server?.id || '', query); + + const albums = await queryClient.fetchQuery( + queryKey, + async ({ signal }) => + api.controller.getAlbumList({ + query, + server, + signal, + }), + { cacheTime: 1000 * 60 * 1 }, + ); + + return api.normalize.albumList(albums, server); + }, + [customFilters, queryClient, server], + ); + + const handleFilterChange = useCallback( + async (filters: AlbumListFilter) => { + if ( + page.display === ListDisplayType.TABLE || + page.display === ListDisplayType.TABLE_PAGINATED + ) { + const dataSource: IDatasource = { + getRows: async (params) => { + const limit = params.endRow - params.startRow; + const startIndex = params.startRow; + + const query: AlbumListQuery = { + limit, + startIndex, + ...filters, + ...customFilters, + jfParams: { + ...filters.jfParams, + ...customFilters?.jfParams, + }, + ndParams: { + ...filters.ndParams, + ...customFilters?.ndParams, + }, + }; + + const queryKey = queryKeys.albums.list(server?.id || '', query); + + const albumsRes = await queryClient.fetchQuery( + queryKey, + async ({ signal }) => + api.controller.getAlbumList({ + query, + server, + signal, + }), + { cacheTime: 1000 * 60 * 1 }, + ); + + const albums = api.normalize.albumList(albumsRes, server); + params.successCallback(albums?.items || [], albumsRes?.totalRecordCount || undefined); + }, + rowCount: undefined, + }; + tableRef.current?.api.setDatasource(dataSource); + tableRef.current?.api.purgeInfiniteCache(); + tableRef.current?.api.ensureIndexVisible(0, 'top'); + + if (page.display === ListDisplayType.TABLE_PAGINATED) { + setPagination({ currentPage: 0 }); + } + } else { + gridRef.current?.scrollTo(0); + gridRef.current?.resetLoadMoreItemsCache(); + + // Refetching within the virtualized grid may be inconsistent due to it refetching + // using an outdated set of filters. To avoid this, we fetch using the updated filters + // and then set the grid's data here. + const data = await fetch(0, 200, filters); + + if (!data?.items) return; + gridRef.current?.setItemData(data.items); + } + }, + [page.display, tableRef, customFilters, server, queryClient, setPagination, gridRef, fetch], + ); + + const handleOpenFiltersModal = () => { + openModal({ + children: ( + <> + {server?.type === ServerType.NAVIDROME ? ( + + ) : ( + + )} + + ), + title: 'Album Filters', + }); + }; + + const handleRefresh = useCallback(() => { + queryClient.invalidateQueries(queryKeys.albums.list(server?.id || '')); + handleFilterChange(filters); + }, [filters, handleFilterChange, queryClient, server?.id]); + + 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 AlbumListSort, + sortOrder: sortOrder || SortOrder.ASC, + }); + + handleFilterChange(updatedFilters); + }, + [handleFilterChange, server?.type, setFilter], + ); + + const handleSetMusicFolder = useCallback( + (e: MouseEvent) => { + if (!e.currentTarget?.value) return; + + let updatedFilters = null; + if (e.currentTarget.value === String(page.filter.musicFolderId)) { + updatedFilters = setFilter({ musicFolderId: undefined }); + } else { + updatedFilters = setFilter({ musicFolderId: e.currentTarget.value }); + } + + handleFilterChange(updatedFilters); + }, + [handleFilterChange, page.filter.musicFolderId, setFilter], + ); + + const handleToggleSortOrder = useCallback(() => { + const newSortOrder = filters.sortOrder === SortOrder.ASC ? SortOrder.DESC : SortOrder.ASC; + const updatedFilters = setFilter({ sortOrder: newSortOrder }); + handleFilterChange(updatedFilters); + }, [filters.sortOrder, handleFilterChange, setFilter]); + + const handlePlayQueueAdd = usePlayQueueAdd(); + + const handlePlay = async (play: Play) => { + if (!itemCount || itemCount === 0) return; + + const query = { + startIndex: 0, + ...filters, + ...customFilters, + jfParams: { + ...filters.jfParams, + ...customFilters?.jfParams, + }, + ndParams: { + ...filters.ndParams, + ...customFilters?.ndParams, + }, + }; + const queryKey = queryKeys.albums.list(server?.id || '', query); + + const albumListRes = await queryClient.fetchQuery({ + queryFn: ({ signal }) => api.controller.getAlbumList({ query, server, signal }), + queryKey, + }); + + const albumIds = + api.normalize.albumList(albumListRes, server).items?.map((item) => item.id) || []; + + handlePlayQueueAdd?.({ + byItemType: { + id: albumIds, + type: LibraryItem.ALBUM, + }, + play, + }); + }; + + const handleItemSize = (e: number) => { + if ( + page.display === ListDisplayType.TABLE || + page.display === ListDisplayType.TABLE_PAGINATED + ) { + setTable({ rowHeight: e }); + } else { + setPage({ list: { ...page, grid: { ...page.grid, size: e } } }); + } + }; + + const handleSetViewType = useCallback( + (e: MouseEvent) => { + if (!e.currentTarget?.value) return; + setPage({ list: { ...page, display: e.currentTarget.value as ListDisplayType } }); + }, + [page, setPage], + ); + + 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 }; + + setTable({ columns: [...existingColumns, newColumn] }); + } 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 }); + } + + return tableRef.current?.api.sizeColumnsToFit(); + }; + + const handleAutoFitColumns = (e: ChangeEvent) => { + setTable({ autoFit: e.currentTarget.checked }); + + if (e.currentTarget.checked) { + tableRef.current?.api.sizeColumnsToFit(); + } + }; + + return ( + + + + + + + + {FILTERS[server?.type as keyof typeof FILTERS].map((filter) => ( + + {filter.name} + + ))} + + + + {server?.type === ServerType.JELLYFIN && ( + + + + + + {musicFoldersQuery.data?.map((folder) => ( + + {folder.name} + + ))} + + + )} + + + + + + + } + onClick={() => handlePlay(Play.NOW)} + > + Play + + } + onClick={() => handlePlay(Play.LAST)} + > + Add to queue + + } + onClick={() => handlePlay(Play.NEXT)} + > + Add to queue next + + + } + onClick={handleRefresh} + > + Refresh + + + + + + + + + + + Display type + + Card + + + Poster + + + Table + + + Table (paginated) + + + Item size + + + + {(page.display === ListDisplayType.TABLE || + page.display === ListDisplayType.TABLE_PAGINATED) && ( + <> + Table Columns + + + 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 48a09ba4..61269b47 100644 --- a/src/renderer/features/albums/components/album-list-header.tsx +++ b/src/renderer/features/albums/components/album-list-header.tsx @@ -1,101 +1,34 @@ -import type { ChangeEvent, MouseEvent, MutableRefObject } from 'react'; +import type { ChangeEvent, MutableRefObject } from 'react'; import { useCallback } 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 { openModal } from '@mantine/modals'; import { useQueryClient } from '@tanstack/react-query'; import debounce from 'lodash/debounce'; -import { - RiArrowDownSLine, - RiFilter3Line, - RiFolder2Line, - RiMoreFill, - RiSortAsc, - RiSortDesc, -} from 'react-icons/ri'; -import styled from 'styled-components'; import { api } from '/@/renderer/api'; import { controller } from '/@/renderer/api/controller'; import { queryKeys } from '/@/renderer/api/query-keys'; +import { AlbumListQuery, LibraryItem } from '/@/renderer/api/types'; import { - AlbumListQuery, - AlbumListSort, - LibraryItem, - ServerType, - SortOrder, -} from '/@/renderer/api/types'; -import { - ALBUM_TABLE_COLUMNS, - Badge, - Button, - DropdownMenu, - MultiSelect, PageHeader, + Paper, SearchInput, - Slider, SpinnerIcon, - Switch, - Text, - TextTitle, 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 { useMusicFolders } from '/@/renderer/features/shared'; +import { LibraryHeaderBar } from '/@/renderer/features/shared'; import { useContainerQuery } from '/@/renderer/hooks'; import { AlbumListFilter, useAlbumListStore, useCurrentServer, useSetAlbumFilters, - useSetAlbumStore, - useSetAlbumTable, useSetAlbumTablePagination, } from '/@/renderer/store'; -import { ListDisplayType, Play, TableColumn } from '/@/renderer/types'; +import { ListDisplayType, Play } from '/@/renderer/types'; +import { AlbumListHeaderFilters } from '/@/renderer/features/albums/components/album-list-header-filters'; import { usePlayQueueAdd } from '/@/renderer/features/player'; - -const FILTERS = { - jellyfin: [ - { defaultOrder: SortOrder.ASC, name: 'Album Artist', value: AlbumListSort.ALBUM_ARTIST }, - { - defaultOrder: SortOrder.DESC, - name: 'Community Rating', - value: AlbumListSort.COMMUNITY_RATING, - }, - { defaultOrder: SortOrder.DESC, name: 'Critic Rating', value: AlbumListSort.CRITIC_RATING }, - { defaultOrder: SortOrder.ASC, name: 'Name', value: AlbumListSort.NAME }, - { defaultOrder: SortOrder.ASC, name: 'Random', value: AlbumListSort.RANDOM }, - { defaultOrder: SortOrder.DESC, name: 'Recently Added', value: AlbumListSort.RECENTLY_ADDED }, - { defaultOrder: SortOrder.DESC, name: 'Release Date', value: AlbumListSort.RELEASE_DATE }, - ], - navidrome: [ - { defaultOrder: SortOrder.ASC, name: 'Album Artist', value: AlbumListSort.ALBUM_ARTIST }, - { defaultOrder: SortOrder.ASC, name: 'Artist', value: AlbumListSort.ARTIST }, - { defaultOrder: SortOrder.DESC, name: 'Duration', value: AlbumListSort.DURATION }, - { defaultOrder: SortOrder.DESC, name: 'Most Played', value: AlbumListSort.PLAY_COUNT }, - { defaultOrder: SortOrder.ASC, name: 'Name', value: AlbumListSort.NAME }, - { defaultOrder: SortOrder.ASC, name: 'Random', value: AlbumListSort.RANDOM }, - { defaultOrder: SortOrder.DESC, name: 'Rating', value: AlbumListSort.RATING }, - { defaultOrder: SortOrder.DESC, name: 'Recently Added', value: AlbumListSort.RECENTLY_ADDED }, - { defaultOrder: SortOrder.DESC, name: 'Recently Played', value: AlbumListSort.RECENTLY_PLAYED }, - { defaultOrder: SortOrder.DESC, name: 'Song Count', value: AlbumListSort.SONG_COUNT }, - { defaultOrder: SortOrder.DESC, name: 'Favorited', value: AlbumListSort.FAVORITED }, - { defaultOrder: SortOrder.DESC, name: 'Year', value: AlbumListSort.YEAR }, - ], -}; - -const ORDER = [ - { name: 'Ascending', value: SortOrder.ASC }, - { name: 'Descending', value: SortOrder.DESC }, -]; - -const HeaderItems = styled.div` - display: flex; - flex-direction: row; - justify-content: space-between; -`; +import { usePlayButtonBehavior } from '/@/renderer/store/settings.store'; interface AlbumListHeaderProps { customFilters?: Partial; @@ -114,34 +47,12 @@ export const AlbumListHeader = ({ }: AlbumListHeaderProps) => { const queryClient = useQueryClient(); const server = useCurrentServer(); - const setPage = useSetAlbumStore(); const setFilter = useSetAlbumFilters(); const page = useAlbumListStore(); const filters = page.filter; 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) || - 'Unknown'; - - const sortOrderLabel = ORDER.find((o) => o.value === filters.sortOrder)?.name || 'Unknown'; - - const handleItemSize = (e: number) => { - if ( - page.display === ListDisplayType.TABLE || - page.display === ListDisplayType.TABLE_PAGINATED - ) { - setTable({ rowHeight: e }); - } else { - setPage({ list: { ...page, grid: { ...page.grid, size: e } } }); - } - }; const fetch = useCallback( async (skip: number, take: number, filters: AlbumListFilter) => { @@ -245,80 +156,6 @@ export const AlbumListHeader = ({ [page.display, tableRef, customFilters, server, queryClient, setPagination, gridRef, fetch], ); - const handleOpenFiltersModal = () => { - openModal({ - children: ( - <> - {server?.type === ServerType.NAVIDROME ? ( - - ) : ( - - )} - - ), - title: 'Album Filters', - }); - }; - - const handleRefresh = useCallback(() => { - queryClient.invalidateQueries(queryKeys.albums.list(server?.id || '')); - handleFilterChange(filters); - }, [filters, handleFilterChange, queryClient, server?.id]); - - 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 AlbumListSort, - sortOrder: sortOrder || SortOrder.ASC, - }); - - handleFilterChange(updatedFilters); - }, - [handleFilterChange, server?.type, setFilter], - ); - - const handleSetMusicFolder = useCallback( - (e: MouseEvent) => { - if (!e.currentTarget?.value) return; - - let updatedFilters = null; - if (e.currentTarget.value === String(page.filter.musicFolderId)) { - updatedFilters = setFilter({ musicFolderId: undefined }); - } else { - updatedFilters = setFilter({ musicFolderId: e.currentTarget.value }); - } - - handleFilterChange(updatedFilters); - }, - [handleFilterChange, page.filter.musicFolderId, setFilter], - ); - - const handleToggleSortOrder = useCallback(() => { - const newSortOrder = filters.sortOrder === SortOrder.ASC ? SortOrder.DESC : SortOrder.ASC; - const updatedFilters = setFilter({ sortOrder: newSortOrder }); - handleFilterChange(updatedFilters); - }, [filters.sortOrder, handleFilterChange, setFilter]); - - const handleSetViewType = useCallback( - (e: MouseEvent) => { - if (!e.currentTarget?.value) return; - setPage({ list: { ...page, display: e.currentTarget.value as ListDisplayType } }); - }, - [page, setPage], - ); - const handleSearch = debounce((e: ChangeEvent) => { const previousSearchTerm = page.filter.searchTerm; const searchTerm = e.target.value === '' ? undefined : e.target.value; @@ -326,40 +163,8 @@ export const AlbumListHeader = ({ if (previousSearchTerm !== searchTerm) 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 }; - - setTable({ columns: [...existingColumns, newColumn] }); - } 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 }); - } - - return tableRef.current?.api.sizeColumnsToFit(); - }; - - const handleAutoFitColumns = (e: ChangeEvent) => { - setTable({ autoFit: e.currentTarget.checked }); - - if (e.currentTarget.checked) { - tableRef.current?.api.sizeColumnsToFit(); - } - }; - const handlePlayQueueAdd = usePlayQueueAdd(); + const playButtonBehavior = usePlayButtonBehavior(); const handlePlay = async (play: Play) => { if (!itemCount || itemCount === 0) return; @@ -397,220 +202,53 @@ export const AlbumListHeader = ({ }; return ( - - + + - - - - - - Display type - - Card - - - Poster - - - Table - - - Table (paginated) - - - Item size - - - - {(page.display === ListDisplayType.TABLE || - page.display === ListDisplayType.TABLE_PAGINATED) && ( - <> - 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} - - ))} - - - )} - - - - - - - handlePlay(Play.NOW)}>Play - handlePlay(Play.LAST)}> - Add to queue - - handlePlay(Play.NEXT)}> - Add to queue next - - - Refresh - - + + + handlePlay(playButtonBehavior)} /> + {title || 'Albums'} + + + {itemCount === null || itemCount === undefined ? : itemCount} + + + + + - - - - - + + + + + ); }; diff --git a/src/renderer/features/artists/components/album-artist-list-header-filters.tsx b/src/renderer/features/artists/components/album-artist-list-header-filters.tsx new file mode 100644 index 00000000..684d0777 --- /dev/null +++ b/src/renderer/features/artists/components/album-artist-list-header-filters.tsx @@ -0,0 +1,470 @@ +import { useCallback, 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 { Group, Stack, Flex } from '@mantine/core'; +import { useQueryClient } from '@tanstack/react-query'; +import { + RiSortAsc, + RiSortDesc, + RiFolder2Line, + RiMoreFill, + RiSettings2Fill, + RiRefreshLine, +} from 'react-icons/ri'; +import { api } from '/@/renderer/api'; +import { queryKeys } from '/@/renderer/api/query-keys'; +import { AlbumArtistListSort, SortOrder } from '/@/renderer/api/types'; +import { + DropdownMenu, + ALBUMARTIST_TABLE_COLUMNS, + VirtualInfiniteGridRef, + Text, + Button, + Slider, + MultiSelect, + Switch, +} from '/@/renderer/components'; +import { useMusicFolders } from '/@/renderer/features/shared'; +import { useContainerQuery } from '/@/renderer/hooks'; +import { + useCurrentServer, + useSetAlbumArtistStore, + useSetAlbumArtistFilters, + useAlbumArtistListStore, + useSetAlbumArtistTablePagination, + useSetAlbumArtistTable, + AlbumArtistListFilter, +} from '/@/renderer/store'; +import { ListDisplayType, TableColumn, ServerType } from '/@/renderer/types'; + +const FILTERS = { + jellyfin: [ + { defaultOrder: SortOrder.ASC, name: 'Album', value: AlbumArtistListSort.ALBUM }, + { defaultOrder: SortOrder.DESC, name: 'Duration', value: AlbumArtistListSort.DURATION }, + { defaultOrder: SortOrder.ASC, name: 'Name', value: AlbumArtistListSort.NAME }, + { defaultOrder: SortOrder.ASC, name: 'Random', value: AlbumArtistListSort.RANDOM }, + { + defaultOrder: SortOrder.DESC, + name: 'Recently Added', + value: AlbumArtistListSort.RECENTLY_ADDED, + }, + // { defaultOrder: SortOrder.DESC, name: 'Release Date', value: AlbumArtistListSort.RELEASE_DATE }, + ], + navidrome: [ + { defaultOrder: SortOrder.DESC, name: 'Album Count', value: AlbumArtistListSort.ALBUM_COUNT }, + { defaultOrder: SortOrder.DESC, name: 'Favorited', value: AlbumArtistListSort.FAVORITED }, + { defaultOrder: SortOrder.DESC, name: 'Most Played', value: AlbumArtistListSort.PLAY_COUNT }, + { defaultOrder: SortOrder.ASC, name: 'Name', value: AlbumArtistListSort.NAME }, + { defaultOrder: SortOrder.DESC, name: 'Rating', value: AlbumArtistListSort.RATING }, + { defaultOrder: SortOrder.DESC, name: 'Song Count', value: AlbumArtistListSort.SONG_COUNT }, + ], +}; + +const ORDER = [ + { name: 'Ascending', value: SortOrder.ASC }, + { name: 'Descending', value: SortOrder.DESC }, +]; + +interface AlbumArtistListHeaderFiltersProps { + gridRef: MutableRefObject; + tableRef: MutableRefObject; +} + +export const AlbumArtistListHeaderFilters = ({ + gridRef, + tableRef, +}: AlbumArtistListHeaderFiltersProps) => { + const queryClient = useQueryClient(); + const server = useCurrentServer(); + const setPage = useSetAlbumArtistStore(); + const setFilter = useSetAlbumArtistFilters(); + const page = useAlbumArtistListStore(); + const filters = page.filter; + const cq = useContainerQuery(); + + const musicFoldersQuery = useMusicFolders(); + + const setPagination = useSetAlbumArtistTablePagination(); + const setTable = useSetAlbumArtistTable(); + + const sortByLabel = + (server?.type && + FILTERS[server.type as keyof typeof FILTERS].find((f) => f.value === filters.sortBy)?.name) || + 'Unknown'; + + const sortOrderLabel = ORDER.find((o) => o.value === filters.sortOrder)?.name || 'Unknown'; + + const handleItemSize = (e: number) => { + if ( + page.display === ListDisplayType.TABLE || + page.display === ListDisplayType.TABLE_PAGINATED + ) { + setTable({ rowHeight: e }); + } else { + setPage({ list: { ...page, grid: { ...page.grid, size: e } } }); + } + }; + + const fetch = useCallback( + async (startIndex: number, limit: number, filters: AlbumArtistListFilter) => { + const queryKey = queryKeys.albumArtists.list(server?.id || '', { + limit, + startIndex, + ...filters, + }); + + const albums = await queryClient.fetchQuery( + queryKey, + async ({ signal }) => + api.controller.getAlbumArtistList({ + query: { + limit, + startIndex, + ...filters, + }, + server, + signal, + }), + { cacheTime: 1000 * 60 * 1 }, + ); + + return api.normalize.albumArtistList(albums, server); + }, + [queryClient, server], + ); + + const handleFilterChange = useCallback( + async (filters: AlbumArtistListFilter) => { + if ( + page.display === ListDisplayType.TABLE || + page.display === ListDisplayType.TABLE_PAGINATED + ) { + const dataSource: IDatasource = { + getRows: async (params) => { + const limit = params.endRow - params.startRow; + const startIndex = params.startRow; + + const queryKey = queryKeys.albumArtists.list(server?.id || '', { + limit, + startIndex, + ...filters, + }); + + const albumArtistsRes = await queryClient.fetchQuery( + queryKey, + async ({ signal }) => + api.controller.getAlbumArtistList({ + query: { + limit, + startIndex, + ...filters, + }, + server, + signal, + }), + { cacheTime: 1000 * 60 * 1 }, + ); + + const albumArtists = api.normalize.albumArtistList(albumArtistsRes, server); + params.successCallback( + albumArtists?.items || [], + albumArtistsRes?.totalRecordCount || undefined, + ); + }, + rowCount: undefined, + }; + tableRef.current?.api.setDatasource(dataSource); + tableRef.current?.api.purgeInfiniteCache(); + tableRef.current?.api.ensureIndexVisible(0, 'top'); + + if (page.display === ListDisplayType.TABLE_PAGINATED) { + setPagination({ currentPage: 0 }); + } + } else { + gridRef.current?.scrollTo(0); + gridRef.current?.resetLoadMoreItemsCache(); + + // Refetching within the virtualized grid may be inconsistent due to it refetching + // using an outdated set of filters. To avoid this, we fetch using the updated filters + // and then set the grid's data here. + const data = await fetch(0, 200, filters); + + if (!data?.items) return; + gridRef.current?.setItemData(data.items); + } + }, + [page.display, tableRef, setPagination, server, queryClient, gridRef, fetch], + ); + + 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 AlbumArtistListSort, + sortOrder: sortOrder || SortOrder.ASC, + }); + + handleFilterChange(updatedFilters); + }, + [handleFilterChange, server?.type, setFilter], + ); + + const handleSetMusicFolder = useCallback( + (e: MouseEvent) => { + if (!e.currentTarget?.value) return; + + let updatedFilters = null; + if (e.currentTarget.value === String(page.filter.musicFolderId)) { + updatedFilters = setFilter({ musicFolderId: undefined }); + } else { + updatedFilters = setFilter({ musicFolderId: e.currentTarget.value }); + } + + handleFilterChange(updatedFilters); + }, + [handleFilterChange, page.filter.musicFolderId, setFilter], + ); + + const handleToggleSortOrder = useCallback(() => { + const newSortOrder = filters.sortOrder === SortOrder.ASC ? SortOrder.DESC : SortOrder.ASC; + const updatedFilters = setFilter({ sortOrder: newSortOrder }); + handleFilterChange(updatedFilters); + }, [filters.sortOrder, handleFilterChange, setFilter]); + + const handleSetViewType = useCallback( + (e: MouseEvent) => { + if (!e.currentTarget?.value) return; + setPage({ list: { ...page, display: e.currentTarget.value as ListDisplayType } }); + }, + [page, setPage], + ); + + 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 }; + + setTable({ columns: [...existingColumns, newColumn] }); + } 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 }); + } + + return tableRef.current?.api.sizeColumnsToFit(); + }; + + const handleAutoFitColumns = (e: ChangeEvent) => { + setTable({ autoFit: e.currentTarget.checked }); + + if (e.currentTarget.checked) { + tableRef.current?.api.sizeColumnsToFit(); + } + }; + + const handleRefresh = useCallback(() => { + queryClient.invalidateQueries(queryKeys.albumArtists.list(server?.id || '')); + handleFilterChange(filters); + }, [filters, handleFilterChange, queryClient, server?.id]); + + return ( + + + + + + + + {FILTERS[server?.type as keyof typeof FILTERS].map((filter) => ( + + {filter.name} + + ))} + + + + {server?.type === ServerType.JELLYFIN && ( + + + + + + {musicFoldersQuery.data?.map((folder) => ( + + {folder.name} + + ))} + + + )} + + + + + + } + onClick={handleRefresh} + > + Refresh + + + + + + + + + + + Display type + + Card + + + Poster + + + Table + + + Table (paginated) + + + Item size + + + + {(page.display === ListDisplayType.TABLE || + page.display === ListDisplayType.TABLE_PAGINATED) && ( + <> + Table Columns + + + column.column)} + width={300} + onChange={handleTableColumns} + /> + + Auto Fit Columns + + + + + + )} + + + + + ); +}; diff --git a/src/renderer/features/artists/components/album-artist-list-header.tsx b/src/renderer/features/artists/components/album-artist-list-header.tsx index 4aa32b11..c370aa40 100644 --- a/src/renderer/features/artists/components/album-artist-list-header.tsx +++ b/src/renderer/features/artists/components/album-artist-list-header.tsx @@ -1,76 +1,30 @@ -import type { ChangeEvent, MouseEvent, MutableRefObject } from 'react'; +import type { ChangeEvent, MutableRefObject } from 'react'; import { useCallback } 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 { useQueryClient } from '@tanstack/react-query'; import debounce from 'lodash/debounce'; -import { RiArrowDownSLine, RiFolder2Line, RiMoreFill, RiSortAsc, RiSortDesc } from 'react-icons/ri'; -import styled from 'styled-components'; import { api } from '/@/renderer/api'; import { queryKeys } from '/@/renderer/api/query-keys'; -import { AlbumArtistListSort, ServerType, SortOrder } from '/@/renderer/api/types'; import { - ALBUMARTIST_TABLE_COLUMNS, - Badge, - Button, - DropdownMenu, - MultiSelect, PageHeader, + Paper, SearchInput, - Slider, SpinnerIcon, - Switch, - Text, - TextTitle, VirtualInfiniteGridRef, } from '/@/renderer/components'; -import { useMusicFolders } from '/@/renderer/features/shared'; +import { LibraryHeaderBar } from '/@/renderer/features/shared'; import { useContainerQuery } from '/@/renderer/hooks'; import { AlbumArtistListFilter, useAlbumArtistListStore, useCurrentServer, useSetAlbumArtistFilters, - useSetAlbumArtistStore, - useSetAlbumArtistTable, useSetAlbumArtistTablePagination, } from '/@/renderer/store'; -import { ListDisplayType, TableColumn } from '/@/renderer/types'; - -const FILTERS = { - jellyfin: [ - { defaultOrder: SortOrder.ASC, name: 'Album', value: AlbumArtistListSort.ALBUM }, - { defaultOrder: SortOrder.DESC, name: 'Duration', value: AlbumArtistListSort.DURATION }, - { defaultOrder: SortOrder.ASC, name: 'Name', value: AlbumArtistListSort.NAME }, - { defaultOrder: SortOrder.ASC, name: 'Random', value: AlbumArtistListSort.RANDOM }, - { - defaultOrder: SortOrder.DESC, - name: 'Recently Added', - value: AlbumArtistListSort.RECENTLY_ADDED, - }, - // { defaultOrder: SortOrder.DESC, name: 'Release Date', value: AlbumArtistListSort.RELEASE_DATE }, - ], - navidrome: [ - { defaultOrder: SortOrder.DESC, name: 'Album Count', value: AlbumArtistListSort.ALBUM_COUNT }, - { defaultOrder: SortOrder.DESC, name: 'Favorited', value: AlbumArtistListSort.FAVORITED }, - { defaultOrder: SortOrder.DESC, name: 'Most Played', value: AlbumArtistListSort.PLAY_COUNT }, - { defaultOrder: SortOrder.ASC, name: 'Name', value: AlbumArtistListSort.NAME }, - { defaultOrder: SortOrder.DESC, name: 'Rating', value: AlbumArtistListSort.RATING }, - { defaultOrder: SortOrder.DESC, name: 'Song Count', value: AlbumArtistListSort.SONG_COUNT }, - ], -}; - -const ORDER = [ - { name: 'Ascending', value: SortOrder.ASC }, - { name: 'Descending', value: SortOrder.DESC }, -]; - -const HeaderItems = styled.div` - display: flex; - flex-direction: row; - justify-content: space-between; -`; +import { ListDisplayType } from '/@/renderer/types'; +import { AlbumArtistListHeaderFilters } from '/@/renderer/features/artists/components/album-artist-list-header-filters'; interface AlbumArtistListHeaderProps { gridRef: MutableRefObject; @@ -85,34 +39,11 @@ export const AlbumArtistListHeader = ({ }: AlbumArtistListHeaderProps) => { const queryClient = useQueryClient(); const server = useCurrentServer(); - const setPage = useSetAlbumArtistStore(); const setFilter = useSetAlbumArtistFilters(); const page = useAlbumArtistListStore(); - const filters = page.filter; const cq = useContainerQuery(); - const musicFoldersQuery = useMusicFolders(); - const setPagination = useSetAlbumArtistTablePagination(); - const setTable = useSetAlbumArtistTable(); - - const sortByLabel = - (server?.type && - FILTERS[server.type as keyof typeof FILTERS].find((f) => f.value === filters.sortBy)?.name) || - 'Unknown'; - - const sortOrderLabel = ORDER.find((o) => o.value === filters.sortOrder)?.name || 'Unknown'; - - const handleItemSize = (e: number) => { - if ( - page.display === ListDisplayType.TABLE || - page.display === ListDisplayType.TABLE_PAGINATED - ) { - setTable({ rowHeight: e }); - } else { - setPage({ list: { ...page, grid: { ...page.grid, size: e } } }); - } - }; const fetch = useCallback( async (startIndex: number, limit: number, filters: AlbumArtistListFilter) => { @@ -205,54 +136,6 @@ export const AlbumArtistListHeader = ({ [page.display, tableRef, setPagination, server, queryClient, gridRef, fetch], ); - 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 AlbumArtistListSort, - sortOrder: sortOrder || SortOrder.ASC, - }); - - handleFilterChange(updatedFilters); - }, - [handleFilterChange, server?.type, setFilter], - ); - - const handleSetMusicFolder = useCallback( - (e: MouseEvent) => { - if (!e.currentTarget?.value) return; - - let updatedFilters = null; - if (e.currentTarget.value === String(page.filter.musicFolderId)) { - updatedFilters = setFilter({ musicFolderId: undefined }); - } else { - updatedFilters = setFilter({ musicFolderId: e.currentTarget.value }); - } - - handleFilterChange(updatedFilters); - }, - [handleFilterChange, page.filter.musicFolderId, setFilter], - ); - - const handleToggleSortOrder = useCallback(() => { - const newSortOrder = filters.sortOrder === SortOrder.ASC ? SortOrder.DESC : SortOrder.ASC; - const updatedFilters = setFilter({ sortOrder: newSortOrder }); - handleFilterChange(updatedFilters); - }, [filters.sortOrder, handleFilterChange, setFilter]); - - const handleSetViewType = useCallback( - (e: MouseEvent) => { - if (!e.currentTarget?.value) return; - setPage({ list: { ...page, display: e.currentTarget.value as ListDisplayType } }); - }, - [page, setPage], - ); - const handleSearch = debounce((e: ChangeEvent) => { const previousSearchTerm = page.filter.searchTerm; const searchTerm = e.target.value === '' ? undefined : e.target.value; @@ -260,245 +143,51 @@ export const AlbumArtistListHeader = ({ if (previousSearchTerm !== searchTerm) 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 }; - - setTable({ columns: [...existingColumns, newColumn] }); - } 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 }); - } - - return tableRef.current?.api.sizeColumnsToFit(); - }; - - const handleAutoFitColumns = (e: ChangeEvent) => { - setTable({ autoFit: e.currentTarget.checked }); - - if (e.currentTarget.checked) { - tableRef.current?.api.sizeColumnsToFit(); - } - }; - - const handleRefresh = useCallback(() => { - queryClient.invalidateQueries(queryKeys.albumArtists.list(server?.id || '')); - handleFilterChange(filters); - }, [filters, handleFilterChange, queryClient, server?.id]); - return ( - - + + - - - - - - Display type - - Card - - - Poster - - - Table - - - Table (paginated) - - - Item size - - - - {(page.display === ListDisplayType.TABLE || - page.display === ListDisplayType.TABLE_PAGINATED) && ( - <> - 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} - - ))} - - - )} - - - - - - Play - Add to queue next - Add to queue - - Refresh - - + + + Album Artists + + + {itemCount === null || itemCount === undefined ? : itemCount} + + + + + - - - - - + + + + + ); }; diff --git a/src/renderer/features/playlists/components/playlist-list-content.tsx b/src/renderer/features/playlists/components/playlist-list-content.tsx index 54ed13e8..2edec947 100644 --- a/src/renderer/features/playlists/components/playlist-list-content.tsx +++ b/src/renderer/features/playlists/components/playlist-list-content.tsx @@ -30,16 +30,16 @@ 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 { usePlaylistList } from '/@/renderer/features/playlists/queries/playlist-list-query'; import { generatePath, useNavigate } from 'react-router'; import { AppRoute } from '/@/renderer/router/routes'; import { LibraryItem } from '/@/renderer/api/types'; interface PlaylistListContentProps { + itemCount?: number; tableRef: MutableRefObject; } -export const PlaylistListContent = ({ tableRef }: PlaylistListContentProps) => { +export const PlaylistListContent = ({ tableRef, itemCount }: PlaylistListContentProps) => { const navigate = useNavigate(); const queryClient = useQueryClient(); const server = useCurrentServer(); @@ -51,12 +51,6 @@ export const PlaylistListContent = ({ tableRef }: PlaylistListContentProps) => { const isPaginationEnabled = page.display === ListDisplayType.TABLE_PAGINATED; - const checkPlaylistList = usePlaylistList({ - limit: 1, - startIndex: 0, - ...page.filter, - }); - const columnDefs: ColDef[] = useMemo( () => getColumnDefs(page.table.columns), [page.table.columns], @@ -194,7 +188,7 @@ export const PlaylistListContent = ({ tableRef }: PlaylistListContentProps) => { defaultColDef={defaultColumnDefs} enableCellChangeFlash={false} getRowId={(data) => data.data.id} - infiniteInitialRowCount={checkPlaylistList.data?.totalRecordCount || 100} + infiniteInitialRowCount={itemCount || 100} pagination={isPaginationEnabled} paginationAutoPageSize={isPaginationEnabled} paginationPageSize={page.table.pagination.itemsPerPage || 100} diff --git a/src/renderer/features/playlists/components/playlist-list-header-filters.tsx b/src/renderer/features/playlists/components/playlist-list-header-filters.tsx new file mode 100644 index 00000000..061c91b8 --- /dev/null +++ b/src/renderer/features/playlists/components/playlist-list-header-filters.tsx @@ -0,0 +1,325 @@ +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 { useQueryClient } from '@tanstack/react-query'; +import { RiSortAsc, RiSortDesc, 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 { + DropdownMenu, + PLAYLIST_TABLE_COLUMNS, + Text, + Button, + Slider, + MultiSelect, + Switch, +} from '/@/renderer/components'; +import { useContainerQuery } from '/@/renderer/hooks'; +import { + PlaylistListFilter, + useCurrentServer, + usePlaylistListStore, + useSetPlaylistFilters, + useSetPlaylistStore, + useSetPlaylistTable, + useSetPlaylistTablePagination, +} from '/@/renderer/store'; +import { ListDisplayType, TableColumn } from '/@/renderer/types'; + +const FILTERS = { + jellyfin: [ + { defaultOrder: SortOrder.DESC, name: 'Duration', value: PlaylistListSort.DURATION }, + { defaultOrder: SortOrder.ASC, name: 'Name', value: PlaylistListSort.NAME }, + { defaultOrder: SortOrder.DESC, name: 'Song Count', value: PlaylistListSort.SONG_COUNT }, + ], + navidrome: [ + { defaultOrder: SortOrder.DESC, name: 'Duration', value: PlaylistListSort.DURATION }, + { defaultOrder: SortOrder.ASC, name: 'Name', value: PlaylistListSort.NAME }, + { defaultOrder: SortOrder.ASC, name: 'Owner', value: PlaylistListSort.OWNER }, + { defaultOrder: SortOrder.DESC, name: 'Public', value: PlaylistListSort.PUBLIC }, + { defaultOrder: SortOrder.DESC, name: 'Song Count', value: PlaylistListSort.SONG_COUNT }, + { defaultOrder: SortOrder.DESC, name: 'Updated At', value: PlaylistListSort.UPDATED_AT }, + ], +}; + +const ORDER = [ + { name: 'Ascending', value: SortOrder.ASC }, + { name: 'Descending', value: SortOrder.DESC }, +]; + +interface PlaylistListHeaderFiltersProps { + tableRef: MutableRefObject; +} + +export const PlaylistListHeaderFilters = ({ tableRef }: PlaylistListHeaderFiltersProps) => { + const queryClient = useQueryClient(); + const server = useCurrentServer(); + const page = usePlaylistListStore(); + const setPage = useSetPlaylistStore(); + const setFilter = useSetPlaylistFilters(); + const setTable = useSetPlaylistTable(); + const setPagination = useSetPlaylistTablePagination(); + const cq = useContainerQuery(); + + const sortByLabel = + (server?.type && + (FILTERS[server.type as keyof typeof FILTERS] as { name: string; value: string }[]).find( + (f) => f.value === page.filter.sortBy, + )?.name) || + 'Unknown'; + + const sortOrderLabel = ORDER.find((s) => s.value === page.filter.sortOrder)?.name; + + const handleFilterChange = useCallback( + async (filters?: PlaylistListFilter) => { + const dataSource: IDatasource = { + getRows: async (params) => { + const limit = params.endRow - params.startRow; + const startIndex = params.startRow; + + const pageFilters = filters || page.filter; + + const queryKey = queryKeys.playlists.list(server?.id || '', { + limit, + startIndex, + ...pageFilters, + }); + + const playlistsRes = await queryClient.fetchQuery( + queryKey, + async ({ signal }) => + api.controller.getPlaylistList({ + query: { + limit, + startIndex, + ...pageFilters, + }, + server, + signal, + }), + { cacheTime: 1000 * 60 * 1 }, + ); + + const playlists = api.normalize.playlistList(playlistsRes, server); + params.successCallback(playlists?.items || [], playlistsRes?.totalRecordCount); + }, + rowCount: undefined, + }; + tableRef.current?.api.setDatasource(dataSource); + tableRef.current?.api.purgeInfiniteCache(); + tableRef.current?.api.ensureIndexVisible(0, 'top'); + setPagination({ currentPage: 0 }); + }, + [page.filter, queryClient, server, setPagination, tableRef], + ); + + 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 PlaylistListSort, + sortOrder: sortOrder || SortOrder.ASC, + }); + + handleFilterChange(updatedFilters); + }, + [handleFilterChange, server?.type, setFilter], + ); + + 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 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 } }); + + 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, setPagination, tableRef], + ); + + 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 ( + + + + + + + + {FILTERS[server?.type as keyof typeof FILTERS].map((filter) => ( + + {filter.name} + + ))} + + + + + + + + + }>Refresh + + + + + + + + + + Display type + + Table + + + Table (paginated) + + + Item Size + + + + Table Columns + + + column.column)} + width={300} + onChange={handleTableColumns} + /> + + Auto Fit Columns + + + + + + + + + ); +}; diff --git a/src/renderer/features/playlists/components/playlist-list-header.tsx b/src/renderer/features/playlists/components/playlist-list-header.tsx index 403224e7..f3e87c1b 100644 --- a/src/renderer/features/playlists/components/playlist-list-header.tsx +++ b/src/renderer/features/playlists/components/playlist-list-header.tsx @@ -1,337 +1,54 @@ -import type { 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 { ChangeEvent, MouseEvent, MutableRefObject, useCallback } from 'react'; -import { RiArrowDownSLine, RiMoreFill, RiSortAsc, RiSortDesc } from 'react-icons/ri'; -import { api } from '/@/renderer/api'; -import { queryKeys } from '/@/renderer/api/query-keys'; -import { PlaylistListSort, SortOrder } from '/@/renderer/api/types'; -import { - Button, - DropdownMenu, - PageHeader, - Slider, - TextTitle, - Switch, - MultiSelect, - Text, - PLAYLIST_TABLE_COLUMNS, -} from '/@/renderer/components'; +import { MutableRefObject } from 'react'; +import { PageHeader, SpinnerIcon, Paper } from '/@/renderer/components'; +import { PlaylistListHeaderFilters } from '/@/renderer/features/playlists/components/playlist-list-header-filters'; +import { LibraryHeaderBar } from '/@/renderer/features/shared'; import { useContainerQuery } from '/@/renderer/hooks'; -import { queryClient } from '/@/renderer/lib/react-query'; -import { - PlaylistListFilter, - useCurrentServer, - usePlaylistListStore, - useSetPlaylistFilters, - useSetPlaylistStore, - useSetPlaylistTable, - useSetPlaylistTablePagination, -} from '/@/renderer/store'; -import { ListDisplayType, TableColumn } from '/@/renderer/types'; - -const FILTERS = { - jellyfin: [ - { defaultOrder: SortOrder.DESC, name: 'Duration', value: PlaylistListSort.DURATION }, - { defaultOrder: SortOrder.ASC, name: 'Name', value: PlaylistListSort.NAME }, - { defaultOrder: SortOrder.DESC, name: 'Song Count', value: PlaylistListSort.SONG_COUNT }, - ], - navidrome: [ - { defaultOrder: SortOrder.DESC, name: 'Duration', value: PlaylistListSort.DURATION }, - { defaultOrder: SortOrder.ASC, name: 'Name', value: PlaylistListSort.NAME }, - { defaultOrder: SortOrder.ASC, name: 'Owner', value: PlaylistListSort.OWNER }, - { defaultOrder: SortOrder.DESC, name: 'Public', value: PlaylistListSort.PUBLIC }, - { defaultOrder: SortOrder.DESC, name: 'Song Count', value: PlaylistListSort.SONG_COUNT }, - { defaultOrder: SortOrder.DESC, name: 'Updated At', value: PlaylistListSort.UPDATED_AT }, - ], -}; - -const ORDER = [ - { name: 'Ascending', value: SortOrder.ASC }, - { name: 'Descending', value: SortOrder.DESC }, -]; interface PlaylistListHeaderProps { + itemCount?: number; tableRef: MutableRefObject; } -export const PlaylistListHeader = ({ tableRef }: PlaylistListHeaderProps) => { - const server = useCurrentServer(); - const page = usePlaylistListStore(); - const setPage = useSetPlaylistStore(); - const setFilter = useSetPlaylistFilters(); - const setTable = useSetPlaylistTable(); - const setPagination = useSetPlaylistTablePagination(); +export const PlaylistListHeader = ({ itemCount, tableRef }: PlaylistListHeaderProps) => { const cq = useContainerQuery(); - const sortByLabel = - (server?.type && - (FILTERS[server.type as keyof typeof FILTERS] as { name: string; value: string }[]).find( - (f) => f.value === page.filter.sortBy, - )?.name) || - 'Unknown'; - - const sortOrderLabel = ORDER.find((s) => s.value === page.filter.sortOrder)?.name; - - const handleFilterChange = useCallback( - async (filters?: PlaylistListFilter) => { - const dataSource: IDatasource = { - getRows: async (params) => { - const limit = params.endRow - params.startRow; - const startIndex = params.startRow; - - const pageFilters = filters || page.filter; - - const queryKey = queryKeys.playlists.list(server?.id || '', { - limit, - startIndex, - ...pageFilters, - }); - - const playlistsRes = await queryClient.fetchQuery( - queryKey, - async ({ signal }) => - api.controller.getPlaylistList({ - query: { - limit, - startIndex, - ...pageFilters, - }, - server, - signal, - }), - { cacheTime: 1000 * 60 * 1 }, - ); - - const playlists = api.normalize.playlistList(playlistsRes, server); - params.successCallback(playlists?.items || [], playlistsRes?.totalRecordCount); - }, - rowCount: undefined, - }; - tableRef.current?.api.setDatasource(dataSource); - tableRef.current?.api.purgeInfiniteCache(); - tableRef.current?.api.ensureIndexVisible(0, 'top'); - setPagination({ currentPage: 0 }); - }, - [page.filter, server, setPagination, tableRef], - ); - - 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 PlaylistListSort, - sortOrder: sortOrder || SortOrder.ASC, - }); - - handleFilterChange(updatedFilters); - }, - [handleFilterChange, server?.type, setFilter], - ); - - 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 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 } }); - - 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, setPagination, tableRef], - ); - - 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 ( - - + + - - - - - - Display type - - Table - - - 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} - - ))} - - - - - - - - - Play - Add to queue - Add to queue next - Add to playlist - - + + + Playlists + + + {itemCount === null || itemCount === undefined ? : itemCount} + + - - + + + + + ); }; diff --git a/src/renderer/features/playlists/routes/playlist-list-route.tsx b/src/renderer/features/playlists/routes/playlist-list-route.tsx index ed0369fb..280140d6 100644 --- a/src/renderer/features/playlists/routes/playlist-list-route.tsx +++ b/src/renderer/features/playlists/routes/playlist-list-route.tsx @@ -1,15 +1,38 @@ import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; import { useRef } from 'react'; +import { PlaylistListSort, SortOrder } from '/@/renderer/api/types'; 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'; import { AnimatedPage } from '/@/renderer/features/shared'; const PlaylistListRoute = () => { const tableRef = useRef(null); + const itemCountCheck = usePlaylistList( + { + limit: 1, + sortBy: PlaylistListSort.NAME, + sortOrder: SortOrder.ASC, + startIndex: 0, + }, + { + cacheTime: 1000 * 60 * 60 * 2, + staleTime: 1000 * 60 * 60 * 2, + }, + ); + + const itemCount = + itemCountCheck.data?.totalRecordCount === null + ? undefined + : itemCountCheck.data?.totalRecordCount; + return ( - + ); diff --git a/src/renderer/features/songs/components/song-list-header-filters.tsx b/src/renderer/features/songs/components/song-list-header-filters.tsx new file mode 100644 index 00000000..d36d9f0b --- /dev/null +++ b/src/renderer/features/songs/components/song-list-header-filters.tsx @@ -0,0 +1,452 @@ +import { useCallback, ChangeEvent, MutableRefObject, MouseEvent } from 'react'; +import { IDatasource } from '@ag-grid-community/core'; +import { Flex, Group, Stack } from '@mantine/core'; +import { openModal } from '@mantine/modals'; +import { + RiSortAsc, + RiSortDesc, + RiFolder2Line, + RiFilter3Line, + RiMoreFill, + RiSettings3Fill, +} from 'react-icons/ri'; +import { api } from '/@/renderer/api'; +import { queryKeys } from '/@/renderer/api/query-keys'; +import { LibraryItem, SongListQuery, SongListSort, SortOrder } from '/@/renderer/api/types'; +import { + DropdownMenu, + SONG_TABLE_COLUMNS, + Button, + Slider, + MultiSelect, + Switch, + Text, +} from '/@/renderer/components'; +import { usePlayQueueAdd } from '/@/renderer/features/player'; +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 type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; +import { + useCurrentServer, + useSongListStore, + useSetSongStore, + useSetSongFilters, + useSetSongTable, + useSetSongTablePagination, + SongListFilter, +} from '/@/renderer/store'; +import { ListDisplayType, ServerType, Play, TableColumn } from '/@/renderer/types'; + +const FILTERS = { + jellyfin: [ + { 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: [ + { 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 }, + ], +}; + +const ORDER = [ + { name: 'Ascending', value: SortOrder.ASC }, + { name: 'Descending', value: SortOrder.DESC }, +]; + +interface SongListHeaderFiltersProps { + customFilters?: Partial; + itemCount?: number; + tableRef: MutableRefObject; +} + +export const SongListHeaderFilters = ({ + customFilters, + itemCount, + tableRef, +}: SongListHeaderFiltersProps) => { + const server = useCurrentServer(); + const page = useSongListStore(); + const setPage = useSetSongStore(); + const setFilter = useSetSongFilters(); + const setTable = useSetSongTable(); + const setPagination = useSetSongTablePagination(); + const handlePlayQueueAdd = usePlayQueueAdd(); + 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 === page.filter.sortBy, + )?.name) || + 'Unknown'; + + const sortOrderLabel = ORDER.find((s) => s.value === page.filter.sortOrder)?.name; + + 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 query: SongListQuery = { + limit, + startIndex, + ...pageFilters, + ...customFilters, + }; + + const queryKey = queryKeys.songs.list(server?.id || '', query); + + const songsRes = await queryClient.fetchQuery( + queryKey, + async ({ signal }) => + api.controller.getSongList({ + query, + server, + signal, + }), + { cacheTime: 1000 * 60 * 1 }, + ); + + 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'); + setPagination({ currentPage: 0 }); + }, + [customFilters, page.filter, server, setPagination, tableRef], + ); + + 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; + + let updatedFilters = null; + if (e.currentTarget.value === String(page.filter.musicFolderId)) { + updatedFilters = setFilter({ musicFolderId: undefined }); + } else { + updatedFilters = setFilter({ musicFolderId: e.currentTarget.value }); + } + + handleFilterChange(updatedFilters); + }, + [handleFilterChange, page.filter.musicFolderId, setFilter], + ); + + 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 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 } }); + + 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, setPagination, tableRef], + ); + + 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 }); + }; + + const handleRefresh = () => { + queryClient.invalidateQueries(queryKeys.songs.list(server?.id || '')); + handleFilterChange(page.filter); + }; + + const handlePlay = async (play: Play) => { + if (!itemCount || itemCount === 0) return; + const query: SongListQuery = { startIndex: 0, ...page.filter }; + + handlePlayQueueAdd?.({ + byItemType: { + id: query, + type: LibraryItem.SONG, + }, + play, + }); + }; + + const handleOpenFiltersModal = () => { + openModal({ + children: ( + <> + {server?.type === ServerType.NAVIDROME ? ( + + ) : ( + + )} + + ), + title: 'Song Filters', + }); + }; + + return ( + + + + + + + + {FILTERS[server?.type as keyof typeof FILTERS].map((filter) => ( + + {filter.name} + + ))} + + + + {server?.type === ServerType.JELLYFIN && ( + + + + + + {musicFoldersQuery.data?.map((folder) => ( + + {folder.name} + + ))} + + + )} + + + + + + + handlePlay(Play.NOW)}>Play + handlePlay(Play.LAST)}> + Add to queue + + handlePlay(Play.NEXT)}> + Add to queue next + + + Refresh + + + + + + + + + + Display type + + Table + + + Table (paginated) + + + Item Size + + + + Table Columns + + + 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 558f880d..4217e920 100644 --- a/src/renderer/features/songs/components/song-list-header.tsx +++ b/src/renderer/features/songs/components/song-list-header.tsx @@ -1,93 +1,26 @@ import type { 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 { openModal } from '@mantine/modals'; import debounce from 'lodash/debounce'; -import { ChangeEvent, MouseEvent, MutableRefObject, useCallback } from 'react'; -import { - RiArrowDownSLine, - RiFilter3Line, - RiFolder2Line, - RiMoreFill, - RiSortAsc, - RiSortDesc, -} from 'react-icons/ri'; +import { ChangeEvent, MutableRefObject, useCallback } from 'react'; import { api } from '/@/renderer/api'; import { queryKeys } from '/@/renderer/api/query-keys'; -import { - LibraryItem, - ServerType, - SongListQuery, - SongListSort, - SortOrder, -} from '/@/renderer/api/types'; -import { - Button, - DropdownMenu, - PageHeader, - SearchInput, - Slider, - TextTitle, - Switch, - MultiSelect, - Text, - SONG_TABLE_COLUMNS, - Badge, - SpinnerIcon, -} from '/@/renderer/components'; +import { LibraryItem, SongListQuery } from '/@/renderer/api/types'; +import { PageHeader, Paper, SearchInput, SpinnerIcon } from '/@/renderer/components'; import { usePlayQueueAdd } from '/@/renderer/features/player'; -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 { LibraryHeaderBar } from '/@/renderer/features/shared'; +import { SongListHeaderFilters } from '/@/renderer/features/songs/components/song-list-header-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, Play, TableColumn } from '/@/renderer/types'; - -const FILTERS = { - jellyfin: [ - { 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: [ - { 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 }, - ], -}; - -const ORDER = [ - { name: 'Ascending', value: SortOrder.ASC }, - { name: 'Descending', value: SortOrder.DESC }, -]; +import { usePlayButtonBehavior } from '/@/renderer/store/settings.store'; +import { Play } from '/@/renderer/types'; interface SongListHeaderProps { customFilters?: Partial; @@ -104,24 +37,11 @@ export const SongListHeader = ({ }: SongListHeaderProps) => { const server = useCurrentServer(); const page = useSongListStore(); - const setPage = useSetSongStore(); const setFilter = useSetSongFilters(); - const setTable = useSetSongTable(); const setPagination = useSetSongTablePagination(); const handlePlayQueueAdd = usePlayQueueAdd(); 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 === page.filter.sortBy, - )?.name) || - 'Unknown'; - - const sortOrderLabel = ORDER.find((s) => s.value === page.filter.sortOrder)?.name; - const handleFilterChange = useCallback( async (filters?: SongListFilter) => { const dataSource: IDatasource = { @@ -164,62 +84,6 @@ export const SongListHeader = ({ [customFilters, page.filter, server, setPagination, tableRef], ); - 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; - - let updatedFilters = null; - if (e.currentTarget.value === String(page.filter.musicFolderId)) { - updatedFilters = setFilter({ musicFolderId: undefined }); - } else { - updatedFilters = setFilter({ musicFolderId: e.currentTarget.value }); - } - - handleFilterChange(updatedFilters); - }, - [handleFilterChange, page.filter.musicFolderId, setFilter], - ); - - 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 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 } }); - - 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, setPagination, tableRef], - ); - const handleSearch = debounce((e: ChangeEvent) => { const previousSearchTerm = page.filter.searchTerm; const searchTerm = e.target.value === '' ? undefined : e.target.value; @@ -227,45 +91,7 @@ export const SongListHeader = ({ if (previousSearchTerm !== searchTerm) 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 }); - }; - - const handleRefresh = () => { - queryClient.invalidateQueries(queryKeys.songs.list(server?.id || '')); - handleFilterChange(page.filter); - }; + const playButtonBehavior = usePlayButtonBehavior(); const handlePlay = async (play: Play) => { if (!itemCount || itemCount === 0) return; @@ -280,218 +106,53 @@ export const SongListHeader = ({ }); }; - const handleOpenFiltersModal = () => { - openModal({ - children: ( - <> - {server?.type === ServerType.NAVIDROME ? ( - - ) : ( - - )} - - ), - title: 'Song Filters', - }); - }; - return ( - - + + - - - - - - Display type - - Table - - - 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} - - ))} - - - )} - - - - - - - handlePlay(Play.NOW)}>Play - handlePlay(Play.LAST)}> - Add to queue - - handlePlay(Play.NEXT)}> - Add to queue next - - - Refresh - - + + + handlePlay(playButtonBehavior)} /> + {title || 'Tracks'} + + + {itemCount === null || itemCount === undefined ? : itemCount} + + + + + - - - - - + + + + + ); };