From 33735c1314fdc2642ad78ddec90524901857291b Mon Sep 17 00:00:00 2001 From: jeffvli Date: Mon, 13 Oct 2025 20:16:06 -0700 Subject: [PATCH] implement new lists for songs --- .../components/jellyfin-song-filters.tsx | 142 +--- .../components/navidrome-song-filters.tsx | 6 +- .../songs/components/song-list-content.tsx | 140 +++- .../songs/components/song-list-grid-view.tsx | 237 ------- .../components/song-list-header-filters.tsx | 609 +----------------- .../songs/components/song-list-header.tsx | 77 +-- .../components/song-list-infinite-grid.tsx | 67 ++ .../components/song-list-infinite-table.tsx | 77 +++ .../components/song-list-paginated-grid.tsx | 58 ++ .../components/song-list-paginated-table.tsx | 86 +++ .../songs/components/song-list-table-view.tsx | 73 --- ...g-filter.tsx => subsonic-song-filters.tsx} | 66 +- .../songs/hooks/use-song-list-filters.ts | 78 +++ .../features/songs/routes/song-list-route.tsx | 104 +-- 14 files changed, 583 insertions(+), 1237 deletions(-) delete mode 100644 src/renderer/features/songs/components/song-list-grid-view.tsx create mode 100644 src/renderer/features/songs/components/song-list-infinite-grid.tsx create mode 100644 src/renderer/features/songs/components/song-list-infinite-table.tsx create mode 100644 src/renderer/features/songs/components/song-list-paginated-grid.tsx create mode 100644 src/renderer/features/songs/components/song-list-paginated-table.tsx delete mode 100644 src/renderer/features/songs/components/song-list-table-view.tsx rename src/renderer/features/songs/components/{subsonic-song-filter.tsx => subsonic-song-filters.tsx} (50%) create mode 100644 src/renderer/features/songs/hooks/use-song-list-filters.ts diff --git a/src/renderer/features/songs/components/jellyfin-song-filters.tsx b/src/renderer/features/songs/components/jellyfin-song-filters.tsx index 6bf024950..ce35e7b72 100644 --- a/src/renderer/features/songs/components/jellyfin-song-filters.tsx +++ b/src/renderer/features/songs/components/jellyfin-song-filters.tsx @@ -6,31 +6,24 @@ import { useTranslation } from 'react-i18next'; import { MultiSelectWithInvalidData } from '/@/renderer/components/select-with-invalid-data'; import { genresQueries } from '/@/renderer/features/genres/api/genres-api'; import { sharedQueries } from '/@/renderer/features/shared/api/shared-api'; -import { SongListFilter, useListFilterByKey, useListStoreActions } from '/@/renderer/store'; +import { useSongListFilters } from '/@/renderer/features/songs/hooks/use-song-list-filters'; +import { SongListFilter, useCurrentServer } from '/@/renderer/store'; import { Divider } from '/@/shared/components/divider/divider'; import { Group } from '/@/shared/components/group/group'; import { NumberInput } from '/@/shared/components/number-input/number-input'; import { Stack } from '/@/shared/components/stack/stack'; import { Text } from '/@/shared/components/text/text'; import { YesNoSelect } from '/@/shared/components/yes-no-select/yes-no-select'; -import { GenreListSort, LibraryItem, SongListQuery, SortOrder } from '/@/shared/types/domain-types'; +import { GenreListSort, LibraryItem, SortOrder } from '/@/shared/types/domain-types'; interface JellyfinSongFiltersProps { customFilters?: Partial; - onFilterChange: (filters: SongListFilter) => void; - pageKey: string; - serverId: string; } -export const JellyfinSongFilters = ({ - customFilters, - onFilterChange, - pageKey, - serverId, -}: JellyfinSongFiltersProps) => { +export const JellyfinSongFilters = ({ customFilters }: JellyfinSongFiltersProps) => { + const server = useCurrentServer(); const { t } = useTranslation(); - const { setFilter } = useListStoreActions(); - const filter = useListFilterByKey({ key: pageKey }); + const { query, setCustom, setFavorite, setMaxYear, setMinYear } = useSongListFilters(); const isGenrePage = customFilters?.genreIds !== undefined; @@ -39,12 +32,12 @@ export const JellyfinSongFilters = ({ const genreListQuery = useQuery( genresQueries.list({ query: { - musicFolderId: filter?.musicFolderId, + musicFolderId: query.musicFolderId, sortBy: GenreListSort.NAME, sortOrder: SortOrder.ASC, startIndex: 0, }, - serverId, + serverId: server.id, }), ); @@ -59,122 +52,57 @@ export const JellyfinSongFilters = ({ const tagsQuery = useQuery( sharedQueries.tags({ query: { - folder: filter?.musicFolderId, + folder: query.musicFolderId, type: LibraryItem.SONG, }, - serverId, + serverId: server.id, }), ); const selectedGenres = useMemo(() => { - return filter?._custom?.jellyfin?.GenreIds?.split(','); - }, [filter?._custom?.jellyfin?.GenreIds]); + return query._custom?.GenreIds?.split(','); + }, [query._custom?.GenreIds]); const selectedTags = useMemo(() => { - return filter?._custom?.jellyfin?.Tags?.split('|'); - }, [filter?._custom?.jellyfin?.Tags]); + return query._custom?.Tags?.split('|'); + }, [query._custom?.Tags]); const yesNoFilters = [ { label: t('filter.isFavorited', { postProcess: 'sentenceCase' }), - onChange: (favorite?: boolean) => { - const updatedFilters = setFilter({ - customFilters, - data: { - _custom: { - ...filter?._custom, - jellyfin: { - ...filter?._custom?.jellyfin, - IncludeItemTypes: 'Audio', - }, - }, - favorite, - }, - itemType: LibraryItem.SONG, - key: pageKey, - }) as SongListFilter; - onFilterChange(updatedFilters); + onChange: (favorite: boolean | undefined) => { + setFavorite(favorite ?? null); }, - value: filter.favorite, + value: query.favorite, }, ]; const handleMinYearFilter = debounce((e: number | string) => { if (typeof e === 'number' && (e < 1700 || e > 2300)) return; - const updatedFilters = setFilter({ - customFilters, - data: { - _custom: { - ...filter?._custom, - jellyfin: { - ...filter?._custom?.jellyfin, - IncludeItemTypes: 'Audio', - }, - }, - minYear: e === '' ? undefined : (e as number), - }, - itemType: LibraryItem.SONG, - key: pageKey, - }) as SongListFilter; - onFilterChange(updatedFilters); + setMinYear(e === '' ? null : (e as number)); }, 500); const handleMaxYearFilter = debounce((e: number | string) => { if (typeof e === 'number' && (e < 1700 || e > 2300)) return; - const updatedFilters = setFilter({ - customFilters, - data: { - _custom: { - ...filter?._custom, - jellyfin: { - ...filter?._custom?.jellyfin, - IncludeItemTypes: 'Audio', - }, - }, - maxYear: e === '' ? undefined : (e as number), - }, - itemType: LibraryItem.SONG, - key: pageKey, - }) as SongListFilter; - onFilterChange(updatedFilters); + setMaxYear(e === '' ? null : (e as number)); }, 500); const handleGenresFilter = debounce((e: string[] | undefined) => { - const updatedFilters = setFilter({ - customFilters, - data: { - _custom: { - ...filter?._custom, - jellyfin: { - ...filter?._custom?.jellyfin, - IncludeItemTypes: 'Audio', - }, - }, - genreIds: e, - }, - itemType: LibraryItem.SONG, - key: pageKey, - }) as SongListFilter; - onFilterChange(updatedFilters); + setCustom((prev) => ({ + ...prev, + GenreIds: e?.join(',') || undefined, + IncludeItemTypes: 'Audio', + ...prev?.jellyfin, + })); }, 250); const handleTagFilter = debounce((e: string[] | undefined) => { - const updatedFilters = setFilter({ - customFilters, - data: { - _custom: { - ...filter?._custom, - jellyfin: { - ...filter?._custom?.jellyfin, - IncludeItemTypes: 'Audio', - Tags: e?.join('|') || undefined, - }, - }, - }, - itemType: LibraryItem.SONG, - key: pageKey, - }) as SongListFilter; - onFilterChange(updatedFilters); + setCustom((prev) => ({ + ...prev, + IncludeItemTypes: 'Audio', + Tags: e?.join('|') || undefined, + ...prev?.jellyfin, + })); }, 250); return ( @@ -188,20 +116,20 @@ export const JellyfinSongFilters = ({ {!isGenrePage && ( diff --git a/src/renderer/features/songs/components/navidrome-song-filters.tsx b/src/renderer/features/songs/components/navidrome-song-filters.tsx index 4472a9abe..71929dce7 100644 --- a/src/renderer/features/songs/components/navidrome-song-filters.tsx +++ b/src/renderer/features/songs/components/navidrome-song-filters.tsx @@ -74,7 +74,7 @@ export const NavidromeSongFilters = ({ })); }, [genreListQuery.data]); - const hasBrf = hasFeature(server, ServerFeature.BFR); + const hasBFR = hasFeature(server, ServerFeature.BFR); const handleGenresFilter = debounce((e: null | string[]) => { const updatedFilters = setFilter({ @@ -166,7 +166,7 @@ export const NavidromeSongFilters = ({ value={filter._custom?.navidrome?.year} width={50} /> - {!isGenrePage && !hasBrf && ( + {!isGenrePage && !hasBFR && ( )} - {!isGenrePage && hasBrf && ( + {!isGenrePage && hasBFR && ( - import('/@/renderer/features/songs/components/song-list-table-view').then((module) => ({ - default: module.SongListTableView, +const SongListInfiniteGrid = lazy(() => + import('/@/renderer/features/songs/components/song-list-infinite-grid').then((module) => ({ + default: module.SongListInfiniteGrid, + })), +); +const SongListPaginatedGrid = lazy(() => + import('/@/renderer/features/songs/components/song-list-paginated-grid').then((module) => ({ + default: module.SongListPaginatedGrid, + })), +); +const SongListInfiniteTable = lazy(() => + import('/@/renderer/features/songs/components/song-list-infinite-table').then((module) => ({ + default: module.SongListInfiniteTable, + })), +); +const SongListPaginatedTable = lazy(() => + import('/@/renderer/features/songs/components/song-list-paginated-table').then((module) => ({ + default: module.SongListPaginatedTable, })), ); -const SongListGridView = lazy(() => - import('/@/renderer/features/songs/components/song-list-grid-view').then((module) => ({ - default: module.SongListGridView, - })), -); - -interface SongListContentProps { - gridRef: MutableRefObject; - itemCount?: number; - tableRef: MutableRefObject; -} - -export const SongListContent = ({ gridRef, itemCount, tableRef }: SongListContentProps) => { - const { pageKey } = useListContext(); - const { display } = useListStoreByKey({ key: pageKey }); - - const isGrid = display === ListDisplayType.CARD || display === ListDisplayType.GRID; +export const SongListContent = () => { + const { display, grid, itemsPerPage, pagination, table } = useListSettings(ItemListKey.SONG); return ( }> - {isGrid ? ( - - ) : ( - - )} + ); }; + +export const SongListView = ({ + display, + grid, + itemsPerPage, + pagination, + table, +}: ItemListSettings) => { + const server = useCurrentServer(); + + const { query } = useSongListFilters(); + + switch (display) { + case ListDisplayType.GRID: { + switch (pagination) { + case ListPaginationType.INFINITE: + return ( + + ); + case ListPaginationType.PAGINATED: + return ( + + ); + default: + return null; + } + } + case ListDisplayType.TABLE: { + switch (pagination) { + case ListPaginationType.INFINITE: + return ( + + ); + case ListPaginationType.PAGINATED: + return ( + + ); + default: + return null; + } + } + } + + return null; +}; diff --git a/src/renderer/features/songs/components/song-list-grid-view.tsx b/src/renderer/features/songs/components/song-list-grid-view.tsx deleted file mode 100644 index b19637b3d..000000000 --- a/src/renderer/features/songs/components/song-list-grid-view.tsx +++ /dev/null @@ -1,237 +0,0 @@ -import { QueryKey, useQueryClient } from '@tanstack/react-query'; -import { MutableRefObject, useCallback, useEffect, useMemo } from 'react'; -import { useSearchParams } from 'react-router-dom'; -import AutoSizer, { Size } from 'react-virtualized-auto-sizer'; -import { ListOnScrollProps } from 'react-window'; - -import { controller } from '/@/renderer/api/controller'; -import { queryKeys } from '/@/renderer/api/query-keys'; -import { SONG_CARD_ROWS } from '/@/renderer/components/card/card-rows'; -import { VirtualGridAutoSizerContainer } from '/@/renderer/components/virtual-grid/virtual-grid-wrapper'; -import { - VirtualInfiniteGrid, - VirtualInfiniteGridRef, -} from '/@/renderer/components/virtual-grid/virtual-infinite-grid'; -import { useListContext } from '/@/renderer/context/list-context'; -import { usePlayQueueAdd } from '/@/renderer/features/player/hooks/use-playqueue-add'; -import { useHandleFavorite } from '/@/renderer/features/shared/hooks/use-handle-favorite'; -import { AppRoute } from '/@/renderer/router/routes'; -import { useCurrentServer, useListStoreActions, useListStoreByKey } from '/@/renderer/store'; -import { useEventStore } from '/@/renderer/store/event.store'; -import { - LibraryItem, - Song, - SongListQuery, - SongListResponse, - SongListSort, -} from '/@/shared/types/domain-types'; -import { CardRow, ListDisplayType } from '/@/shared/types/types'; -interface SongListGridViewProps { - gridRef: MutableRefObject; - itemCount?: number; -} - -export const SongListGridView = ({ gridRef, itemCount }: SongListGridViewProps) => { - const queryClient = useQueryClient(); - const server = useCurrentServer(); - const handlePlayQueueAdd = usePlayQueueAdd(); - const { customFilters, id, pageKey } = useListContext(); - const { display, filter, grid } = useListStoreByKey({ key: pageKey }); - const { setGrid } = useListStoreActions(); - - const [searchParams, setSearchParams] = useSearchParams(); - const scrollOffset = searchParams.get('scrollOffset'); - const initialScrollOffset = Number(id ? scrollOffset : grid?.scrollOffset) || 0; - - const handleFavorite = useHandleFavorite({ gridRef }); - - useEffect(() => { - const unSub = useEventStore.subscribe((state) => { - const event = state.event; - if (event && event.event === 'favorite') { - const idSet = new Set(state.ids); - const userFavorite = event.favorite; - - gridRef.current?.updateItemData((data) => { - if (idSet.has(data.id)) { - return { - ...data, - userFavorite, - }; - } - return data; - }); - } - }); - - return () => { - unSub(); - }; - }, [gridRef]); - - const cardRows = useMemo(() => { - const rows: CardRow[] = [ - SONG_CARD_ROWS.name, - SONG_CARD_ROWS.album, - SONG_CARD_ROWS.albumArtists, - ]; - - switch (filter.sortBy) { - case SongListSort.ALBUM: - break; - case SongListSort.ARTIST: - break; - case SongListSort.DURATION: - rows.push(SONG_CARD_ROWS.duration); - break; - case SongListSort.EXPLICIT_STATUS: - rows.push(SONG_CARD_ROWS.explicitStatus); - break; - case SongListSort.FAVORITED: - break; - case SongListSort.NAME: - break; - case SongListSort.PLAY_COUNT: - rows.push(SONG_CARD_ROWS.playCount); - break; - case SongListSort.RANDOM: - break; - case SongListSort.RATING: - rows.push(SONG_CARD_ROWS.rating); - break; - case SongListSort.RECENTLY_ADDED: - rows.push(SONG_CARD_ROWS.createdAt); - break; - case SongListSort.RECENTLY_PLAYED: - rows.push(SONG_CARD_ROWS.lastPlayedAt); - break; - case SongListSort.YEAR: - rows.push(SONG_CARD_ROWS.releaseYear); - break; - case SongListSort.RELEASE_DATE: - rows.push(SONG_CARD_ROWS.releaseDate); - } - - return rows; - }, [filter.sortBy]); - - const handleGridScroll = useCallback( - (e: ListOnScrollProps) => { - if (id) { - setSearchParams( - (params) => { - params.set('scrollOffset', String(e.scrollOffset)); - return params; - }, - { replace: true }, - ); - } else { - setGrid({ data: { scrollOffset: e.scrollOffset }, key: pageKey }); - } - }, - [id, pageKey, setGrid, setSearchParams], - ); - - const fetchInitialData = useCallback(() => { - const query: SongListQuery = { - ...filter, - ...customFilters, - }; - - const queryKey = queryKeys.songs.list(server?.id || '', query, id); - - const queriesFromCache: [QueryKey, SongListResponse | undefined][] = - queryClient.getQueriesData({ - exact: false, - fetchStatus: 'idle', - queryKey, - stale: false, - }); - - const itemData: Song[] = []; - - for (const [, data] of queriesFromCache) { - const { items, startIndex } = data || {}; - - if (items && items.length !== 1 && startIndex !== undefined) { - let itemIndex = 0; - for ( - let rowIndex = startIndex; - rowIndex < startIndex + items.length; - rowIndex += 1 - ) { - itemData[rowIndex] = items[itemIndex]; - itemIndex += 1; - } - } - } - - return itemData; - }, [customFilters, filter, id, queryClient, server?.id]); - - const fetch = useCallback( - async ({ skip, take }: { skip: number; take: number }) => { - if (!server) { - return []; - } - - const query: SongListQuery = { - imageSize: 250, - limit: take, - ...filter, - ...customFilters, - startIndex: skip, - }; - - const queryKey = queryKeys.songs.list(server?.id || '', query, id); - - const songs = await queryClient.fetchQuery({ - queryFn: async ({ signal }) => - controller.getSongList({ - apiClientProps: { - serverId: server?.id || '', - signal, - }, - query, - }), - queryKey, - }); - - return songs; - }, - [customFilters, filter, id, queryClient, server], - ); - - return ( - - - {({ height, width }: Size) => ( - - )} - - - ); -}; diff --git a/src/renderer/features/songs/components/song-list-header-filters.tsx b/src/renderer/features/songs/components/song-list-header-filters.tsx index f9c39c81f..4d370719c 100644 --- a/src/renderer/features/songs/components/song-list-header-filters.tsx +++ b/src/renderer/features/songs/components/song-list-header-filters.tsx @@ -1,599 +1,36 @@ -import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; - -import { openModal } from '@mantine/modals'; -import { useQuery } from '@tanstack/react-query'; -import debounce from 'lodash/debounce'; -import { MouseEvent, MutableRefObject, useCallback, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; - -import i18n from '/@/i18n/i18n'; -import { queryKeys } from '/@/renderer/api/query-keys'; -import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid/virtual-infinite-grid'; -import { SONG_TABLE_COLUMNS } from '/@/renderer/components/virtual-table'; -import { useListContext } from '/@/renderer/context/list-context'; -import { sharedQueries } from '/@/renderer/features/shared/api/shared-api'; -import { FilterButton } from '/@/renderer/features/shared/components/filter-button'; -import { FolderButton } from '/@/renderer/features/shared/components/folder-button'; +import { SONG_TABLE_COLUMNS } from '/@/renderer/components/item-list/item-table-list/default-columns'; import { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu'; -import { MoreButton } from '/@/renderer/features/shared/components/more-button'; -import { OrderToggleButton } from '/@/renderer/features/shared/components/order-toggle-button'; -import { RefreshButton } from '/@/renderer/features/shared/components/refresh-button'; -import { JellyfinSongFilters } from '/@/renderer/features/songs/components/jellyfin-song-filters'; -import { NavidromeSongFilters } from '/@/renderer/features/songs/components/navidrome-song-filters'; -import { SubsonicSongFilters } from '/@/renderer/features/songs/components/subsonic-song-filter'; -import { useContainerQuery } from '/@/renderer/hooks'; -import { useListFilterRefresh } from '/@/renderer/hooks/use-list-filter-refresh'; -import { queryClient } from '/@/renderer/lib/react-query'; -import { - PersistedTableColumn, - SongListFilter, - useCurrentServer, - useListStoreActions, -} from '/@/renderer/store'; -import { useListStoreByKey } from '/@/renderer/store/list.store'; -import { Button } from '/@/shared/components/button/button'; +import { ListFilters } from '/@/renderer/features/shared/components/list-filters'; +import { ListMusicFolderDropdown } from '/@/renderer/features/shared/components/list-music-folder-dropdown'; +import { ListRefreshButton } from '/@/renderer/features/shared/components/list-refresh-button'; +import { ListSortByDropdown } from '/@/renderer/features/shared/components/list-sort-by-dropdown'; +import { ListSortOrderToggleButton } from '/@/renderer/features/shared/components/list-sort-order-toggle-button'; import { Divider } from '/@/shared/components/divider/divider'; -import { DropdownMenu } from '/@/shared/components/dropdown-menu/dropdown-menu'; import { Flex } from '/@/shared/components/flex/flex'; import { Group } from '/@/shared/components/group/group'; -import { Icon } from '/@/shared/components/icon/icon'; -import { - LibraryItem, - ServerType, - SongListQuery, - SongListSort, - SortOrder, -} from '/@/shared/types/domain-types'; -import { ListDisplayType, Play } from '/@/shared/types/types'; - -const FILTERS = { - jellyfin: [ - { - defaultOrder: SortOrder.ASC, - name: i18n.t('filter.album', { postProcess: 'titleCase' }), - value: SongListSort.ALBUM, - }, - { - defaultOrder: SortOrder.ASC, - name: i18n.t('filter.albumArtist', { postProcess: 'titleCase' }), - value: SongListSort.ALBUM_ARTIST, - }, - { - defaultOrder: SortOrder.ASC, - name: i18n.t('filter.artist', { postProcess: 'titleCase' }), - value: SongListSort.ARTIST, - }, - { - defaultOrder: SortOrder.ASC, - name: i18n.t('filter.duration', { postProcess: 'titleCase' }), - value: SongListSort.DURATION, - }, - { - defaultOrder: SortOrder.ASC, - name: i18n.t('filter.playCount', { postProcess: 'titleCase' }), - value: SongListSort.PLAY_COUNT, - }, - { - defaultOrder: SortOrder.ASC, - name: i18n.t('filter.name', { postProcess: 'titleCase' }), - value: SongListSort.NAME, - }, - { - defaultOrder: SortOrder.ASC, - name: i18n.t('filter.random', { postProcess: 'titleCase' }), - value: SongListSort.RANDOM, - }, - { - defaultOrder: SortOrder.ASC, - name: i18n.t('filter.recentlyAdded', { postProcess: 'titleCase' }), - value: SongListSort.RECENTLY_ADDED, - }, - { - defaultOrder: SortOrder.ASC, - name: i18n.t('filter.recentlyPlayed', { postProcess: 'titleCase' }), - value: SongListSort.RECENTLY_PLAYED, - }, - { - defaultOrder: SortOrder.ASC, - name: i18n.t('filter.releaseDate', { postProcess: 'titleCase' }), - value: SongListSort.RELEASE_DATE, - }, - ], - navidrome: [ - { - defaultOrder: SortOrder.ASC, - name: i18n.t('filter.album', { postProcess: 'titleCase' }), - value: SongListSort.ALBUM, - }, - { - defaultOrder: SortOrder.ASC, - name: i18n.t('filter.albumArtist', { postProcess: 'titleCase' }), - value: SongListSort.ALBUM_ARTIST, - }, - { - defaultOrder: SortOrder.ASC, - name: i18n.t('filter.artist', { postProcess: 'titleCase' }), - value: SongListSort.ARTIST, - }, - { - defaultOrder: SortOrder.DESC, - name: i18n.t('filter.bpm', { postProcess: 'titleCase' }), - value: SongListSort.BPM, - }, - { - defaultOrder: SortOrder.ASC, - name: i18n.t('common.channel', { count: 2, postProcess: 'titleCase' }), - value: SongListSort.CHANNELS, - }, - { - defaultOrder: SortOrder.ASC, - name: i18n.t('filter.comment', { postProcess: 'titleCase' }), - value: SongListSort.COMMENT, - }, - { - defaultOrder: SortOrder.DESC, - name: i18n.t('filter.duration', { postProcess: 'titleCase' }), - value: SongListSort.DURATION, - }, - { - defaultOrder: SortOrder.DESC, - name: i18n.t('filter.explicitStatus', { postProcess: 'titleCase' }), - value: SongListSort.EXPLICIT_STATUS, - }, - { - defaultOrder: SortOrder.DESC, - name: i18n.t('filter.isFavorited', { postProcess: 'titleCase' }), - value: SongListSort.FAVORITED, - }, - { - defaultOrder: SortOrder.ASC, - name: i18n.t('filter.genre', { postProcess: 'titleCase' }), - value: SongListSort.GENRE, - }, - { - defaultOrder: SortOrder.ASC, - name: i18n.t('filter.name', { postProcess: 'titleCase' }), - value: SongListSort.NAME, - }, - { - defaultOrder: SortOrder.DESC, - name: i18n.t('filter.playCount', { postProcess: 'titleCase' }), - value: SongListSort.PLAY_COUNT, - }, - { - defaultOrder: SortOrder.ASC, - name: i18n.t('filter.random', { postProcess: 'titleCase' }), - value: SongListSort.RANDOM, - }, - { - defaultOrder: SortOrder.DESC, - name: i18n.t('filter.rating', { postProcess: 'titleCase' }), - value: SongListSort.RATING, - }, - { - defaultOrder: SortOrder.DESC, - name: i18n.t('filter.recentlyAdded', { postProcess: 'titleCase' }), - value: SongListSort.RECENTLY_ADDED, - }, - { - defaultOrder: SortOrder.DESC, - name: i18n.t('filter.recentlyPlayed', { postProcess: 'titleCase' }), - value: SongListSort.RECENTLY_PLAYED, - }, - { - defaultOrder: SortOrder.DESC, - name: i18n.t('filter.releaseYear', { postProcess: 'titleCase' }), - value: SongListSort.YEAR, - }, - ], - subsonic: [ - { - defaultOrder: SortOrder.ASC, - name: i18n.t('filter.name', { postProcess: 'titleCase' }), - value: SongListSort.NAME, - }, - ], -}; - -interface SongListHeaderFiltersProps { - gridRef: MutableRefObject; - itemCount?: number; - tableRef: MutableRefObject; -} - -export const SongListHeaderFilters = ({ - gridRef, - itemCount, - tableRef, -}: SongListHeaderFiltersProps) => { - const { t } = useTranslation(); - const server = useCurrentServer(); - const { customFilters, handlePlay, pageKey } = useListContext(); - const { display, filter, grid, table } = useListStoreByKey({ - filter: customFilters, - key: pageKey, - }); - - const { setDisplayType, setFilter, setGrid, setTable, setTablePagination } = - useListStoreActions(); - - const { handleRefreshGrid, handleRefreshTable } = useListFilterRefresh({ - itemCount, - itemType: LibraryItem.SONG, - server, - }); - - const cq = useContainerQuery(); - - const musicFoldersQuery = useQuery( - sharedQueries.musicFolders({ query: null, serverId: server?.id }), - ); - - const sortByLabel = - (server?.type && - ( - FILTERS[server.type as keyof typeof FILTERS] as { name: string; value: string }[] - ).find((f) => f.value === filter.sortBy)?.name) || - 'Unknown'; - - const isGrid = display === ListDisplayType.CARD || display === ListDisplayType.GRID; - - 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({ - customFilters, - data: { - sortBy: e.currentTarget.value as SongListSort, - sortOrder: sortOrder || SortOrder.ASC, - }, - itemType: LibraryItem.SONG, - key: pageKey, - }) as SongListFilter; - - if (isGrid) { - handleRefreshGrid(gridRef, updatedFilters); - } else { - handleRefreshTable(tableRef, updatedFilters); - } - }, - [ - customFilters, - gridRef, - handleRefreshGrid, - handleRefreshTable, - isGrid, - pageKey, - server?.type, - setFilter, - tableRef, - ], - ); - - const handleSetMusicFolder = useCallback( - (e: MouseEvent) => { - if (!e.currentTarget?.value) return; - - let updatedFilters: null | SongListFilter = null; - if (e.currentTarget.value === String(filter.musicFolderId)) { - updatedFilters = setFilter({ - customFilters, - data: { musicFolderId: undefined }, - itemType: LibraryItem.SONG, - key: pageKey, - }) as SongListFilter; - } else { - updatedFilters = setFilter({ - customFilters, - data: { musicFolderId: e.currentTarget.value }, - itemType: LibraryItem.SONG, - key: pageKey, - }) as SongListFilter; - } - - if (isGrid) { - handleRefreshGrid(gridRef, updatedFilters); - } else { - handleRefreshTable(tableRef, updatedFilters); - } - }, - [ - filter.musicFolderId, - isGrid, - setFilter, - customFilters, - pageKey, - handleRefreshGrid, - gridRef, - handleRefreshTable, - tableRef, - ], - ); - - const handleToggleSortOrder = useCallback(() => { - const newSortOrder = filter.sortOrder === SortOrder.ASC ? SortOrder.DESC : SortOrder.ASC; - const updatedFilters = setFilter({ - customFilters, - data: { sortOrder: newSortOrder }, - itemType: LibraryItem.SONG, - key: pageKey, - }) as SongListFilter; - - if (isGrid) { - handleRefreshGrid(gridRef, updatedFilters); - } else { - handleRefreshTable(tableRef, updatedFilters); - } - }, [ - customFilters, - filter.sortOrder, - gridRef, - handleRefreshGrid, - handleRefreshTable, - isGrid, - pageKey, - setFilter, - tableRef, - ]); - - const handleSetViewType = useCallback( - (displayType: ListDisplayType) => { - setDisplayType({ - data: displayType, - key: pageKey, - }); - - if (display === ListDisplayType.TABLE) { - tableRef.current?.api.paginationSetPageSize( - tableRef.current.props.infiniteInitialRowCount, - ); - setTablePagination({ data: { currentPage: 0 }, key: pageKey }); - } else if (display === ListDisplayType.TABLE_PAGINATED) { - setTablePagination({ data: { currentPage: 0 }, key: pageKey }); - } - }, - [display, pageKey, setDisplayType, setTablePagination, tableRef], - ); - - const handleTableColumns = (values: string[]) => { - const existingColumns = table.columns; - - if (values.length === 0) { - return setTable({ - data: { - columns: [], - }, - key: pageKey, - }); - } - - // If adding a column - if (values.length > existingColumns.length) { - const newColumn = { - column: values[values.length - 1], - width: 100, - } as PersistedTableColumn; - - return setTable({ data: { columns: [...existingColumns, newColumn] }, key: pageKey }); - } - - // If removing a column - const removed = existingColumns.filter((column) => !values.includes(column.column)); - const newColumns = existingColumns.filter((column) => !removed.includes(column)); - - return setTable({ data: { columns: newColumns }, key: pageKey }); - }; - - const handleAutoFitColumns = (autoFitColumns: boolean) => { - setTable({ data: { autoFit: autoFitColumns }, key: pageKey }); - - if (autoFitColumns) { - tableRef.current?.api.sizeColumnsToFit(); - } - }; - - const handleItemSize = (e: number) => { - if (isGrid) { - setGrid({ data: { itemSize: e }, key: pageKey }); - } else { - setTable({ data: { rowHeight: e }, key: pageKey }); - } - }; - - const debouncedHandleItemSize = debounce(handleItemSize, 20); - - const handleItemGap = (e: number) => { - setGrid({ data: { itemGap: e }, key: pageKey }); - }; - - const handleRefresh = () => { - queryClient.invalidateQueries({ queryKey: queryKeys.songs.list(server?.id || '') }); - if (isGrid) { - handleRefreshGrid(gridRef, filter); - } else { - handleRefreshTable(tableRef, filter); - } - }; - - const onFilterChange = (filter: SongListFilter) => { - if (isGrid) { - handleRefreshGrid(gridRef, { - ...filter, - }); - } else { - handleRefreshTable(tableRef, { - ...filter, - }); - } - }; - - const handleOpenFiltersModal = () => { - let FilterComponent; - - switch (server?.type) { - case ServerType.JELLYFIN: - FilterComponent = JellyfinSongFilters; - break; - case ServerType.NAVIDROME: - FilterComponent = NavidromeSongFilters; - break; - case ServerType.SUBSONIC: - FilterComponent = SubsonicSongFilters; - break; - } - - if (!FilterComponent) { - return; - } - - openModal({ - children: ( - - ), - title: 'Song Filters', - }); - }; - - const isFilterApplied = useMemo(() => { - const isNavidromeFilterApplied = - server?.type === ServerType.NAVIDROME && - filter._custom?.navidrome && - Object.values(filter?._custom?.navidrome).some((value) => value !== undefined); - - const isJellyfinFilterApplied = - server?.type === ServerType.JELLYFIN && - filter?._custom?.jellyfin && - Object.values(filter?._custom?.jellyfin) - .filter((value) => value !== 'Audio') // Don't account for includeItemTypes: Audio - .some((value) => value !== undefined); - - const isGenericFilterApplied = filter?.favorite !== undefined || filter?.genreIds?.length; - - return isNavidromeFilterApplied || isJellyfinFilterApplied || isGenericFilterApplied; - }, [ - filter._custom?.jellyfin, - filter._custom?.navidrome, - filter?.favorite, - filter?.genreIds?.length, - server?.type, - ]); - - const isFolderFilterApplied = useMemo(() => { - return filter.musicFolderId !== undefined; - }, [filter.musicFolderId]); +import { LibraryItem, SongListSort, SortOrder } from '/@/shared/types/domain-types'; +import { ItemListKey } from '/@/shared/types/types'; +export const SongListHeaderFilters = () => { return ( - - - - - - - {FILTERS[server?.type as keyof typeof FILTERS].map((f) => ( - - {f.name} - - ))} - - + + - {server?.type !== ServerType.SUBSONIC && ( - - )} - {server?.type === ServerType.JELLYFIN && ( - <> - - - - - - {musicFoldersQuery.data?.items.map((folder) => ( - - {folder.name} - - ))} - - - - )} - - - - - - - - } - onClick={() => handlePlay?.({ playType: Play.NOW })} - > - {t('player.play', { postProcess: 'sentenceCase' })} - - } - onClick={() => handlePlay?.({ playType: Play.SHUFFLE })} - > - {t('player.shuffle', { postProcess: 'sentenceCase' })} - - } - onClick={() => handlePlay?.({ playType: Play.LAST })} - > - {t('player.addLast', { postProcess: 'sentenceCase' })} - - } - onClick={() => handlePlay?.({ playType: Play.NEXT })} - > - {t('player.addNext', { postProcess: 'sentenceCase' })} - - - } - onClick={handleRefresh} - > - {t('common.refresh', { postProcess: 'titleCase' })} - - - + + + + - column.column)} - tableColumnsData={SONG_TABLE_COLUMNS} - /> + ); diff --git a/src/renderer/features/songs/components/song-list-header.tsx b/src/renderer/features/songs/components/song-list-header.tsx index 85b4f338f..c51298e3b 100644 --- a/src/renderer/features/songs/components/song-list-header.tsx +++ b/src/renderer/features/songs/components/song-list-header.tsx @@ -1,86 +1,33 @@ -import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; - -import debounce from 'lodash/debounce'; -import { ChangeEvent, MutableRefObject, useEffect, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { PageHeader } from '/@/renderer/components/page-header/page-header'; -import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid/virtual-infinite-grid'; +import { useListContext } from '/@/renderer/context/list-context'; import { FilterBar } from '/@/renderer/features/shared/components/filter-bar'; import { LibraryHeaderBar } from '/@/renderer/features/shared/components/library-header-bar'; -import { SearchInput } from '/@/renderer/features/shared/components/search-input'; +import { ListSearchInput } from '/@/renderer/features/shared/components/list-search-input'; import { SongListHeaderFilters } from '/@/renderer/features/songs/components/song-list-header-filters'; -import { useContainerQuery } from '/@/renderer/hooks'; -import { useDisplayRefresh } from '/@/renderer/hooks/use-display-refresh'; -import { SongListFilter, useCurrentServer } from '/@/renderer/store'; -import { usePlayButtonBehavior } from '/@/renderer/store/settings.store'; import { Flex } from '/@/shared/components/flex/flex'; import { Group } from '/@/shared/components/group/group'; import { Stack } from '/@/shared/components/stack/stack'; -import { LibraryItem, SongListQuery } from '/@/shared/types/domain-types'; interface SongListHeaderProps { genreId?: string; - gridRef: MutableRefObject; - itemCount?: number; - tableRef: MutableRefObject; title?: string; } -export const SongListHeader = ({ - genreId, - gridRef, - itemCount, - tableRef, - title, -}: SongListHeaderProps) => { +export const SongListHeader = ({ title }: SongListHeaderProps) => { const { t } = useTranslation(); - const server = useCurrentServer(); - const cq = useContainerQuery(); - const genreRef = useRef(undefined); - const { customFilters, filter, handlePlay, refresh, search } = useDisplayRefresh( - { - gridRef, - itemCount, - itemType: LibraryItem.SONG, - server, - tableRef, - }, - ); - - const handleSearch = debounce((e: ChangeEvent) => { - const updatedFilters = search(e) as SongListFilter; - - const filterWithCustom = { - ...updatedFilters, - ...customFilters, - }; - - refresh(filterWithCustom); - }, 500); - - useEffect(() => { - if (genreRef.current && genreRef.current !== genreId) { - refresh(customFilters); - } - - genreRef.current = genreId; - }, [customFilters, genreId, refresh, tableRef]); - - const playButtonBehavior = usePlayButtonBehavior(); + const { itemCount } = useListContext(); + const pageTitle = title || t('page.trackList.title', { postProcess: 'titleCase' }); return ( - + - handlePlay?.({ playType: playButtonBehavior })} - /> - - {title || t('page.trackList.title', { postProcess: 'titleCase' })} - + {}} /> + {pageTitle} @@ -88,16 +35,12 @@ export const SongListHeader = ({ - + - + ); diff --git a/src/renderer/features/songs/components/song-list-infinite-grid.tsx b/src/renderer/features/songs/components/song-list-infinite-grid.tsx new file mode 100644 index 000000000..8506760f1 --- /dev/null +++ b/src/renderer/features/songs/components/song-list-infinite-grid.tsx @@ -0,0 +1,67 @@ +import { UseSuspenseQueryOptions } from '@tanstack/react-query'; +import { forwardRef } from 'react'; + +import { api } from '/@/renderer/api'; +import { useItemListInfiniteLoader } from '/@/renderer/components/item-list/helpers/item-list-infinite-loader'; +import { useItemListScrollPersist } from '/@/renderer/components/item-list/helpers/use-item-list-scroll-persist'; +import { ItemGridList } from '/@/renderer/components/item-list/item-grid-list/item-grid-list'; +import { ItemListGridComponentProps } from '/@/renderer/components/item-list/types'; +import { songsQueries } from '/@/renderer/features/songs/api/songs-api'; +import { LibraryItem, SongListQuery, SongListSort, SortOrder } from '/@/shared/types/domain-types'; +import { ItemListKey } from '/@/shared/types/types'; + +interface SongListInfiniteGridProps extends ItemListGridComponentProps {} + +export const SongListInfiniteGrid = forwardRef( + ( + { + gap = 'md', + itemsPerPage = 100, + itemsPerRow, + query = { + sortBy: SongListSort.NAME, + sortOrder: SortOrder.ASC, + }, + saveScrollOffset = true, + serverId, + }, + ref, + ) => { + const listCountQuery = songsQueries.listCount({ + query: { ...query }, + serverId: serverId, + }) as UseSuspenseQueryOptions; + + const listQueryFn = api.controller.getSongList; + + const { data, onRangeChanged } = useItemListInfiniteLoader({ + eventKey: ItemListKey.SONG, + itemsPerPage, + itemType: LibraryItem.SONG, + listCountQuery, + listQueryFn, + query, + serverId, + }); + + const { handleOnScrollEnd, scrollOffset } = useItemListScrollPersist({ + enabled: saveScrollOffset, + }); + + return ( + + ); + }, +); diff --git a/src/renderer/features/songs/components/song-list-infinite-table.tsx b/src/renderer/features/songs/components/song-list-infinite-table.tsx new file mode 100644 index 000000000..d0af63807 --- /dev/null +++ b/src/renderer/features/songs/components/song-list-infinite-table.tsx @@ -0,0 +1,77 @@ +import { UseSuspenseQueryOptions } from '@tanstack/react-query'; +import { forwardRef } from 'react'; + +import { api } from '/@/renderer/api'; +import { useItemListInfiniteLoader } from '/@/renderer/components/item-list/helpers/item-list-infinite-loader'; +import { useItemListScrollPersist } from '/@/renderer/components/item-list/helpers/use-item-list-scroll-persist'; +import { ItemTableList } from '/@/renderer/components/item-list/item-table-list/item-table-list'; +import { ItemTableListColumn } from '/@/renderer/components/item-list/item-table-list/item-table-list-column'; +import { ItemListTableComponentProps } from '/@/renderer/components/item-list/types'; +import { songsQueries } from '/@/renderer/features/songs/api/songs-api'; +import { LibraryItem, SongListQuery, SongListSort, SortOrder } from '/@/shared/types/domain-types'; +import { ItemListKey } from '/@/shared/types/types'; + +interface SongListInfiniteTableProps extends ItemListTableComponentProps {} + +export const SongListInfiniteTable = forwardRef( + ( + { + columns, + enableAlternateRowColors = false, + enableHorizontalBorders = false, + enableRowHoverHighlight = true, + enableVerticalBorders = false, + itemsPerPage = 100, + query = { + sortBy: SongListSort.NAME, + sortOrder: SortOrder.ASC, + }, + saveScrollOffset = true, + serverId, + size = 'default', + }, + ref, + ) => { + const listCountQuery = songsQueries.listCount({ + query: { ...query }, + serverId: serverId, + }) as UseSuspenseQueryOptions; + + const listQueryFn = api.controller.getSongList; + + const { data, onRangeChanged } = useItemListInfiniteLoader({ + eventKey: ItemListKey.SONG, + itemsPerPage, + itemType: LibraryItem.SONG, + listCountQuery, + listQueryFn, + query, + serverId, + }); + + const { handleOnScrollEnd, scrollOffset } = useItemListScrollPersist({ + enabled: saveScrollOffset, + }); + + return ( + + ); + }, +); diff --git a/src/renderer/features/songs/components/song-list-paginated-grid.tsx b/src/renderer/features/songs/components/song-list-paginated-grid.tsx new file mode 100644 index 000000000..3bb31e34b --- /dev/null +++ b/src/renderer/features/songs/components/song-list-paginated-grid.tsx @@ -0,0 +1,58 @@ +import { UseSuspenseQueryOptions } from '@tanstack/react-query'; +import { forwardRef } from 'react'; + +import { api } from '/@/renderer/api'; +import { useItemListPaginatedLoader } from '/@/renderer/components/item-list/helpers/item-list-paginated-loader'; +import { ItemGridList } from '/@/renderer/components/item-list/item-grid-list/item-grid-list'; +import { ItemListWithPagination } from '/@/renderer/components/item-list/item-list-pagination/item-list-pagination'; +import { useItemListPagination } from '/@/renderer/components/item-list/item-list-pagination/use-item-list-pagination'; +import { ItemListGridComponentProps } from '/@/renderer/components/item-list/types'; +import { songsQueries } from '/@/renderer/features/songs/api/songs-api'; +import { LibraryItem, SongListQuery, SongListSort, SortOrder } from '/@/shared/types/domain-types'; + +interface SongListPaginatedGridProps extends ItemListGridComponentProps {} + +export const SongListPaginatedGrid = forwardRef( + ( + { + gap = 'md', + itemsPerPage = 100, + query = { + sortBy: SongListSort.NAME, + sortOrder: SortOrder.ASC, + }, + serverId, + }, + ref, + ) => { + const listCountQuery = songsQueries.listCount({ + query: { ...query }, + serverId: serverId, + }) as UseSuspenseQueryOptions; + + const listQueryFn = api.controller.getSongList; + + const { currentPage, onChange } = useItemListPagination(); + + const { data, pageCount, totalItemCount } = useItemListPaginatedLoader({ + currentPage, + itemsPerPage, + listCountQuery, + listQueryFn, + query, + serverId, + }); + + return ( + + + + ); + }, +); diff --git a/src/renderer/features/songs/components/song-list-paginated-table.tsx b/src/renderer/features/songs/components/song-list-paginated-table.tsx new file mode 100644 index 000000000..ea9784ad3 --- /dev/null +++ b/src/renderer/features/songs/components/song-list-paginated-table.tsx @@ -0,0 +1,86 @@ +import { UseSuspenseQueryOptions } from '@tanstack/react-query'; +import { forwardRef } from 'react'; + +import { api } from '/@/renderer/api'; +import { useItemListPaginatedLoader } from '/@/renderer/components/item-list/helpers/item-list-paginated-loader'; +import { useItemListScrollPersist } from '/@/renderer/components/item-list/helpers/use-item-list-scroll-persist'; +import { ItemListWithPagination } from '/@/renderer/components/item-list/item-list-pagination/item-list-pagination'; +import { useItemListPagination } from '/@/renderer/components/item-list/item-list-pagination/use-item-list-pagination'; +import { ItemTableList } from '/@/renderer/components/item-list/item-table-list/item-table-list'; +import { ItemTableListColumn } from '/@/renderer/components/item-list/item-table-list/item-table-list-column'; +import { ItemListTableComponentProps } from '/@/renderer/components/item-list/types'; +import { songsQueries } from '/@/renderer/features/songs/api/songs-api'; +import { LibraryItem, SongListQuery, SongListSort, SortOrder } from '/@/shared/types/domain-types'; + +interface SongListPaginatedTableProps extends ItemListTableComponentProps {} + +export const SongListPaginatedTable = forwardRef( + ( + { + columns, + enableAlternateRowColors = false, + enableHorizontalBorders = false, + enableRowHoverHighlight = true, + enableVerticalBorders = false, + itemsPerPage = 100, + query = { + sortBy: SongListSort.NAME, + sortOrder: SortOrder.ASC, + }, + saveScrollOffset = true, + serverId, + size = 'default', + }, + ref, + ) => { + const listCountQuery = songsQueries.listCount({ + query: { ...query }, + serverId: serverId, + }) as UseSuspenseQueryOptions; + + const listQueryFn = api.controller.getSongList; + + const { currentPage, onChange } = useItemListPagination(); + + const { data, pageCount, totalItemCount } = useItemListPaginatedLoader({ + currentPage, + itemsPerPage, + listCountQuery, + listQueryFn, + query, + serverId, + }); + + const { handleOnScrollEnd, scrollOffset } = useItemListScrollPersist({ + enabled: saveScrollOffset, + }); + + return ( + + + + ); + }, +); diff --git a/src/renderer/features/songs/components/song-list-table-view.tsx b/src/renderer/features/songs/components/song-list-table-view.tsx deleted file mode 100644 index bce2c448d..000000000 --- a/src/renderer/features/songs/components/song-list-table-view.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; - -import { RowDoubleClickedEvent } from '@ag-grid-community/core'; -import { MutableRefObject } from 'react'; - -import { VirtualGridAutoSizerContainer } from '/@/renderer/components/virtual-grid/virtual-grid-wrapper'; -import { VirtualTable } from '/@/renderer/components/virtual-table'; -import { useCurrentSongRowStyles } from '/@/renderer/components/virtual-table/hooks/use-current-song-row-styles'; -import { useVirtualTable } from '/@/renderer/components/virtual-table/hooks/use-virtual-table'; -import { useListContext } from '/@/renderer/context/list-context'; -import { SONG_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/context-menu-items'; -import { useAppFocus } from '/@/renderer/hooks'; -import { - useCurrentServer, - usePlayerSong, - usePlayerStatus, - usePlayButtonBehavior, -} from '/@/renderer/store'; -import { LibraryItem, QueueSong, SongListQuery } from '/@/shared/types/domain-types'; - -interface SongListTableViewProps { - itemCount?: number; - tableRef: MutableRefObject; -} - -export const SongListTableView = ({ itemCount, tableRef }: SongListTableViewProps) => { - const server = useCurrentServer(); - const { customFilters, handlePlay, id, pageKey } = useListContext(); - const isFocused = useAppFocus(); - const currentSong = usePlayerSong(); - const status = usePlayerStatus(); - - const { rowClassRules } = useCurrentSongRowStyles({ tableRef }); - - const tableProps = useVirtualTable({ - columnType: 'generic', - contextMenu: SONG_CONTEXT_MENU_ITEMS, - customFilters, - isSearchParams: Boolean(id), - itemCount, - itemType: LibraryItem.SONG, - pageKey, - server, - tableRef, - }); - - const playButtonBehavior = usePlayButtonBehavior(); - const handleRowDoubleClick = (e: RowDoubleClickedEvent) => { - if (!e.data) return; - handlePlay?.({ initialSongId: e.data.id, playType: playButtonBehavior }); - }; - - return ( - - - - ); -}; diff --git a/src/renderer/features/songs/components/subsonic-song-filter.tsx b/src/renderer/features/songs/components/subsonic-song-filters.tsx similarity index 50% rename from src/renderer/features/songs/components/subsonic-song-filter.tsx rename to src/renderer/features/songs/components/subsonic-song-filters.tsx index da7732109..e3f4927f6 100644 --- a/src/renderer/features/songs/components/subsonic-song-filter.tsx +++ b/src/renderer/features/songs/components/subsonic-song-filters.tsx @@ -1,34 +1,27 @@ import { useQuery } from '@tanstack/react-query'; import debounce from 'lodash/debounce'; -import { ChangeEvent, useMemo } from 'react'; +import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; +import { SelectWithInvalidData } from '/@/renderer/components/select-with-invalid-data'; import { genresQueries } from '/@/renderer/features/genres/api/genres-api'; -import { SongListFilter, useListFilterByKey, useListStoreActions } from '/@/renderer/store'; +import { useSongListFilters } from '/@/renderer/features/songs/hooks/use-song-list-filters'; +import { SongListFilter, useCurrentServer } from '/@/renderer/store'; import { Divider } from '/@/shared/components/divider/divider'; import { Group } from '/@/shared/components/group/group'; -import { Select } from '/@/shared/components/select/select'; import { Stack } from '/@/shared/components/stack/stack'; -import { Switch } from '/@/shared/components/switch/switch'; import { Text } from '/@/shared/components/text/text'; -import { GenreListSort, LibraryItem, SongListQuery, SortOrder } from '/@/shared/types/domain-types'; +import { YesNoSelect } from '/@/shared/components/yes-no-select/yes-no-select'; +import { GenreListSort, SortOrder } from '/@/shared/types/domain-types'; interface SubsonicSongFiltersProps { customFilters?: Partial; - onFilterChange: (filters: SongListFilter) => void; - pageKey: string; - serverId: string; } -export const SubsonicSongFilters = ({ - customFilters, - onFilterChange, - pageKey, - serverId, -}: SubsonicSongFiltersProps) => { +export const SubsonicSongFilters = ({ customFilters }: SubsonicSongFiltersProps) => { + const server = useCurrentServer(); const { t } = useTranslation(); - const { setFilter } = useListStoreActions(); - const filter = useListFilterByKey({ key: pageKey }); + const { query, setFavorite, setGenreId } = useSongListFilters(); const isGenrePage = customFilters?.genreIds !== undefined; @@ -39,7 +32,7 @@ export const SubsonicSongFilters = ({ sortOrder: SortOrder.ASC, startIndex: 0, }, - serverId, + serverId: server.id, }), ); @@ -52,35 +45,16 @@ export const SubsonicSongFilters = ({ }, [genreListQuery.data]); const handleGenresFilter = debounce((e: null | string) => { - const updatedFilters = setFilter({ - customFilters, - data: { - genreIds: e ? [e] : undefined, - }, - itemType: LibraryItem.SONG, - key: pageKey, - }) as SongListFilter; - - onFilterChange(updatedFilters); + setGenreId(e ? [e] : null); }, 250); const toggleFilters = [ { - disabled: filter.genreIds !== undefined || isGenrePage || !!filter.searchTerm, label: t('filter.isFavorited', { postProcess: 'sentenceCase' }), - onChange: (e: ChangeEvent) => { - const updatedFilters = setFilter({ - customFilters, - data: { - favorite: e.target.checked ? true : undefined, - }, - itemType: LibraryItem.SONG, - key: pageKey, - }) as SongListFilter; - - onFilterChange(updatedFilters); + onChange: (favorite: boolean | undefined) => { + setFavorite(favorite ?? null); }, - value: filter.favorite, + value: query.favorite, }, ]; @@ -89,22 +63,16 @@ export const SubsonicSongFilters = ({ {toggleFilters.map((filter) => ( {filter.label} - + ))} {!isGenrePage && ( -