import { Flex, Slider } from '@mantine/core'; import { useQueryClient } from '@tanstack/react-query'; import debounce from 'lodash/debounce'; import throttle from 'lodash/throttle'; import type { ChangeEvent, MouseEvent, MutableRefObject } from 'react'; import { useCallback } from 'react'; 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 { AlbumListSort, ServerType, SortOrder } from '/@/renderer/api/types'; import { Button, DropdownMenu, PageHeader, Popover, SearchInput, 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 { useContainerQuery } from '/@/renderer/hooks'; import { AlbumListFilter, useAlbumListStore, useCurrentServer, useSetAlbumFilters, useSetAlbumStore, } from '/@/renderer/store'; import { CardDisplayType } from '/@/renderer/types'; 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; `; interface AlbumListHeaderProps { gridRef: MutableRefObject; } export const AlbumListHeader = ({ gridRef }: 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 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 setSize = throttle( (e: number) => setPage({ list: { ...page, grid: { ...page.grid, size: e } }, }), 200, ); const fetch = useCallback( async (skip: number, take: number, filters: AlbumListFilter) => { const queryKey = queryKeys.albums.list(server?.id || '', { limit: take, startIndex: skip, ...filters, }); const albums = await queryClient.fetchQuery(queryKey, async ({ signal }) => controller.getAlbumList({ query: { limit: take, startIndex: skip, ...filters, }, server, signal, }), ); return api.normalize.albumList(albums, server); }, [queryClient, server], ); const handleFilterChange = useCallback( async (filters: any) => { 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); }, [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 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 handleSetOrder = useCallback( (e: MouseEvent) => { if (!e.currentTarget?.value) return; const updatedFilters = setFilter({ sortOrder: e.currentTarget.value as SortOrder }); handleFilterChange(updatedFilters); }, [handleFilterChange, setFilter], ); const handleSetViewType = useCallback( (e: MouseEvent) => { if (!e.currentTarget?.value) return; const type = e.currentTarget.value; if (type === CardDisplayType.CARD) { setPage({ list: { ...page, display: CardDisplayType.CARD } }); } else if (type === CardDisplayType.POSTER) { setPage({ list: { ...page, display: CardDisplayType.POSTER } }); } else { setPage({ list: { ...page, display: CardDisplayType.TABLE } }); } }, [page, setPage], ); const handleSearch = debounce((e: ChangeEvent) => { const updatedFilters = setFilter({ searchTerm: e.target.value }); handleFilterChange(updatedFilters); }, 500); return ( Card Poster List {FILTERS[server?.type as keyof typeof FILTERS].map((filter) => ( {filter.name} ))} {ORDER.map((sort) => ( {sort.name} ))} {server?.type === ServerType.JELLYFIN && ( {musicFoldersQuery.data?.map((folder) => ( {folder.name} ))} )} {server?.type === ServerType.NAVIDROME ? ( ) : ( )} Play Play last Play next Add to playlist ); };