diff --git a/src/renderer/features/genres/components/genre-list-table-view.tsx b/src/renderer/features/genres/components/genre-list-table-view.tsx index 4931ff58..925565d1 100644 --- a/src/renderer/features/genres/components/genre-list-table-view.tsx +++ b/src/renderer/features/genres/components/genre-list-table-view.tsx @@ -6,7 +6,10 @@ import { useVirtualTable } from '/@/renderer/components/virtual-table/hooks/use- import { useListContext } from '/@/renderer/context/list-context'; import { ALBUM_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/context-menu-items'; import { useCurrentServer } from '/@/renderer/store'; -import { MutableRefObject } from 'react'; +import { MutableRefObject, useCallback } from 'react'; +import { RowDoubleClickedEvent } from '@ag-grid-community/core'; +import { generatePath, useNavigate } from 'react-router'; +import { AppRoute } from '/@/renderer/router/routes'; interface GenreListTableViewProps { itemCount?: number; @@ -16,6 +19,7 @@ interface GenreListTableViewProps { export const GenreListTableView = ({ tableRef, itemCount }: GenreListTableViewProps) => { const server = useCurrentServer(); const { pageKey, customFilters } = useListContext(); + const navigate = useNavigate(); const tableProps = useVirtualTable({ contextMenu: ALBUM_CONTEXT_MENU_ITEMS, @@ -27,6 +31,16 @@ export const GenreListTableView = ({ tableRef, itemCount }: GenreListTableViewPr tableRef, }); + const onRowDoubleClicked = useCallback( + (e: RowDoubleClickedEvent) => { + const { data } = e; + if (!data) return; + + navigate(generatePath(AppRoute.LIBRARY_GENRES_SONGS, { genreId: data.id })); + }, + [navigate], + ); + return ( ); diff --git a/src/renderer/features/player/components/shuffle-all-modal.tsx b/src/renderer/features/player/components/shuffle-all-modal.tsx index 694ecc90..4d058c29 100644 --- a/src/renderer/features/player/components/shuffle-all-modal.tsx +++ b/src/renderer/features/player/components/shuffle-all-modal.tsx @@ -13,6 +13,8 @@ import { RandomSongListQuery, MusicFolderListResponse, ServerType, + GenreListSort, + SortOrder, } from '/@/renderer/api/types'; import { api } from '/@/renderer/api'; import { useAuthStore } from '/@/renderer/store'; @@ -225,7 +227,11 @@ export const openShuffleAllModal = async ( server, signal, }, - query: null, + query: { + sortBy: GenreListSort.NAME, + sortOrder: SortOrder.ASC, + startIndex: 0, + }, }), queryKey: queryKeys.genres.list(server?.id), staleTime: 1000 * 60 * 5, diff --git a/src/renderer/features/songs/components/jellyfin-song-filters.tsx b/src/renderer/features/songs/components/jellyfin-song-filters.tsx index d00daae2..102d76bb 100644 --- a/src/renderer/features/songs/components/jellyfin-song-filters.tsx +++ b/src/renderer/features/songs/components/jellyfin-song-filters.tsx @@ -23,6 +23,8 @@ export const JellyfinSongFilters = ({ const { setFilter } = useListStoreActions(); const { filter } = useListFilterByKey({ key: pageKey }); + const isGenrePage = customFilters?._custom?.jellyfin?.GenreIds !== undefined; + // TODO - eventually replace with /items/filters endpoint to fetch genres and tags specific to the selected library const genreListQuery = useGenreList({ query: { @@ -162,17 +164,19 @@ export const JellyfinSongFilters = ({ onChange={handleMaxYearFilter} /> - - - + {!isGenrePage && ( + + + + )} ); }; diff --git a/src/renderer/features/songs/components/navidrome-song-filters.tsx b/src/renderer/features/songs/components/navidrome-song-filters.tsx index 1632fb38..9b157a94 100644 --- a/src/renderer/features/songs/components/navidrome-song-filters.tsx +++ b/src/renderer/features/songs/components/navidrome-song-filters.tsx @@ -22,6 +22,8 @@ export const NavidromeSongFilters = ({ const { setFilter } = useListStoreActions(); const filter = useListFilterByKey({ key: pageKey }); + const isGenrePage = customFilters?._custom?.navidrome?.genre_id !== undefined; + const genreListQuery = useGenreList({ query: { sortBy: GenreListSort.NAME, @@ -124,15 +126,17 @@ export const NavidromeSongFilters = ({ width={50} onChange={(e) => handleYearFilter(e)} /> - + )} ); diff --git a/src/renderer/features/songs/routes/song-list-route.tsx b/src/renderer/features/songs/routes/song-list-route.tsx index 9192b51c..0b226dab 100644 --- a/src/renderer/features/songs/routes/song-list-route.tsx +++ b/src/renderer/features/songs/routes/song-list-route.tsx @@ -1,8 +1,9 @@ import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; -import { useCallback, useRef } from 'react'; +import { useCallback, useMemo, useRef } from 'react'; import { useParams, useSearchParams } from 'react-router-dom'; -import { LibraryItem, SongListQuery } from '/@/renderer/api/types'; +import { GenreListSort, LibraryItem, SongListQuery, SortOrder } from '/@/renderer/api/types'; import { ListContext } from '/@/renderer/context/list-context'; +import { useGenreList } from '/@/renderer/features/genres'; import { usePlayQueueAdd } from '/@/renderer/features/player'; import { AnimatedPage } from '/@/renderer/features/shared'; import { SongListContent } from '/@/renderer/features/songs/components/song-list-content'; @@ -10,17 +11,31 @@ import { SongListHeader } from '/@/renderer/features/songs/components/song-list- import { useSongList } from '/@/renderer/features/songs/queries/song-list-query'; import { useCurrentServer, useListFilterByKey } from '/@/renderer/store'; import { Play } from '/@/renderer/types'; +import { titleCase } from '/@/renderer/utils'; const TrackListRoute = () => { const tableRef = useRef(null); const server = useCurrentServer(); const [searchParams] = useSearchParams(); - const { albumArtistId } = useParams(); + const { albumArtistId, genreId } = useParams(); + const pageKey = albumArtistId ? `albumArtistSong` : 'song'; - const customFilters = { - ...(albumArtistId && { artistIds: [albumArtistId] }), - }; + const customFilters: Partial = useMemo(() => { + return { + ...(albumArtistId && { artistIds: [albumArtistId] }), + ...(genreId && { + _custom: { + jellyfin: { + GenreIds: genreId, + }, + navidrome: { + genre_id: genreId, + }, + }, + }), + }; + }, [albumArtistId, genreId]); const handlePlayQueueAdd = usePlayQueueAdd(); const songListFilter = useListFilterByKey({ @@ -28,6 +43,28 @@ const TrackListRoute = () => { key: pageKey, }); + const genreList = useGenreList({ + options: { + cacheTime: 1000 * 60 * 60, + enabled: !!genreId, + }, + query: { + sortBy: GenreListSort.NAME, + sortOrder: SortOrder.ASC, + startIndex: 0, + }, + serverId: server?.id, + }); + + const genreTitle = useMemo(() => { + if (!genreList.data) return ''; + const genre = genreList.data.items.find((g) => g.id === genreId); + + if (!genre) return 'Unknown'; + + return genre?.name; + }, [genreId, genreList.data]); + const itemCountCheck = useSongList({ options: { cacheTime: 1000 * 60, @@ -77,13 +114,26 @@ const TrackListRoute = () => { [albumArtistId, handlePlayQueueAdd, itemCount, songListFilter], ); + const providerValue = useMemo(() => { + return { + customFilters, + handlePlay, + id: albumArtistId ?? genreId, + pageKey, + }; + }, [albumArtistId, customFilters, genreId, handlePlay, pageKey]); + return ( - + { errorElement={} path={AppRoute.NOW_PLAYING} /> - } - errorElement={} - path={AppRoute.LIBRARY_GENRES} - /> + + } + errorElement={} + /> + } + path={AppRoute.LIBRARY_GENRES_ALBUMS} + /> + } + path={AppRoute.LIBRARY_GENRES_SONGS} + /> + } errorElement={} diff --git a/src/renderer/router/routes.ts b/src/renderer/router/routes.ts index 56af2402..61ec9dfb 100644 --- a/src/renderer/router/routes.ts +++ b/src/renderer/router/routes.ts @@ -13,6 +13,8 @@ export enum AppRoute { LIBRARY_ARTISTS_DETAIL = '/library/artists/:artistId', LIBRARY_FOLDERS = '/library/folders', LIBRARY_GENRES = '/library/genres', + LIBRARY_GENRES_ALBUMS = '/library/genres/:genreId/albums', + LIBRARY_GENRES_SONGS = '/library/genres/:genreId/songs', LIBRARY_SONGS = '/library/songs', NOW_PLAYING = '/now-playing', PLAYING = '/playing',