From ac5de29c7167593d5f30df69fd0547513533ca49 Mon Sep 17 00:00:00 2001 From: jeffvli Date: Wed, 26 Nov 2025 23:36:10 -0800 Subject: [PATCH] reimplement artist discog / tracks routes --- .../albums/components/album-list-content.tsx | 4 ++ .../albums/components/album-list-header.tsx | 69 +++++++++++++++++-- .../albums/hooks/use-album-list-filters.ts | 6 +- .../albums/routes/album-list-route.tsx | 26 ++++++- .../songs/components/song-list-content.tsx | 4 ++ .../songs/components/song-list-header.tsx | 62 +++++++++++++++-- .../songs/hooks/use-song-list-filters.ts | 6 +- .../features/songs/routes/song-list-route.tsx | 30 +++++++- src/shared/types/types.ts | 4 ++ 9 files changed, 190 insertions(+), 21 deletions(-) diff --git a/src/renderer/features/albums/components/album-list-content.tsx b/src/renderer/features/albums/components/album-list-content.tsx index 7463cd106..858ec1404 100644 --- a/src/renderer/features/albums/components/album-list-content.tsx +++ b/src/renderer/features/albums/components/album-list-content.tsx @@ -1,5 +1,6 @@ import { lazy, Suspense, useMemo } from 'react'; +import { useListContext } from '/@/renderer/context/list-context'; import { useAlbumListFilters } from '/@/renderer/features/albums/hooks/use-album-list-filters'; import { ItemListSettings, useCurrentServer, useListSettings } from '/@/renderer/store'; import { Spinner } from '/@/shared/components/spinner/spinner'; @@ -33,12 +34,15 @@ const AlbumListPaginatedTable = lazy(() => export const AlbumListContent = () => { const { display, grid, itemsPerPage, pagination, table } = useListSettings(ItemListKey.ALBUM); + const { customFilters } = useListContext(); + return ( }> diff --git a/src/renderer/features/albums/components/album-list-header.tsx b/src/renderer/features/albums/components/album-list-header.tsx index 845898f9d..87049862e 100644 --- a/src/renderer/features/albums/components/album-list-header.tsx +++ b/src/renderer/features/albums/components/album-list-header.tsx @@ -1,32 +1,35 @@ +import { useSuspenseQuery } from '@tanstack/react-query'; +import { Suspense, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { PageHeader } from '/@/renderer/components/page-header/page-header'; import { useListContext } from '/@/renderer/context/list-context'; import { AlbumListHeaderFilters } from '/@/renderer/features/albums/components/album-list-header-filters'; import { useAlbumListFilters } from '/@/renderer/features/albums/hooks/use-album-list-filters'; +import { artistsQueries } from '/@/renderer/features/artists/api/artists-api'; +import { useGenreList } from '/@/renderer/features/genres/api/genres-api'; import { FilterBar } from '/@/renderer/features/shared/components/filter-bar'; import { LibraryHeaderBar } from '/@/renderer/features/shared/components/library-header-bar'; import { ListSearchInput } from '/@/renderer/features/shared/components/list-search-input'; +import { useCurrentServerId } from '/@/renderer/store'; import { Group } from '/@/shared/components/group/group'; import { Stack } from '/@/shared/components/stack/stack'; import { LibraryItem } from '/@/shared/types/domain-types'; +import { ItemListKey } from '/@/shared/types/types'; interface AlbumListHeaderProps { title?: string; } export const AlbumListHeader = ({ title }: AlbumListHeaderProps) => { - const { t } = useTranslation(); - const { itemCount } = useListContext(); - const pageTitle = title || t('page.albumList.title', { postProcess: 'titleCase' }); return ( - {pageTitle} + {itemCount} @@ -42,13 +45,69 @@ export const AlbumListHeader = ({ title }: AlbumListHeaderProps) => { ); }; +const PageTitle = ({ title }: { title?: string }) => { + const { t } = useTranslation(); + const { pageKey } = useListContext(); + const pageTitle = title || t('page.albumList.title', { postProcess: 'titleCase' }); + + switch (pageKey) { + case ItemListKey.ALBUM_ARTIST_ALBUM: + return ( + —}> + + + ); + case ItemListKey.GENRE_ALBUM: + return ( + —}> + + + ); + } + + return {pageTitle}; +}; + +const GenreTitle = () => { + const { id } = useListContext(); + const { data: genres } = useGenreList(); + + const name = useMemo(() => { + return genres?.items.find((g) => g.id === id)?.name || '—'; + }, [id, genres]); + + return {name}; +}; + +const AlbumArtistTitle = () => { + const serverId = useCurrentServerId(); + const { id } = useListContext(); + + const { data: albumArtist } = useSuspenseQuery( + artistsQueries.albumArtistDetail({ + query: { id: id! }, + serverId: serverId, + }), + ); + + return {albumArtist?.name || '—'}; +}; + const PlayButton = () => { const { query } = useAlbumListFilters(); + const { customFilters } = useListContext(); + + const mergedQuery = useMemo(() => { + return { + ...query, + ...(customFilters ?? {}), + }; + }, [query, customFilters]); return ( ); diff --git a/src/renderer/features/albums/hooks/use-album-list-filters.ts b/src/renderer/features/albums/hooks/use-album-list-filters.ts index 711aac1d3..fc85d731b 100644 --- a/src/renderer/features/albums/hooks/use-album-list-filters.ts +++ b/src/renderer/features/albums/hooks/use-album-list-filters.ts @@ -11,13 +11,13 @@ import { useSearchTermFilter } from '/@/renderer/features/shared/hooks/use-searc import { useSortByFilter } from '/@/renderer/features/shared/hooks/use-sort-by-filter'; import { useSortOrderFilter } from '/@/renderer/features/shared/hooks/use-sort-order-filter'; import { customFiltersSchema, FILTER_KEYS } from '/@/renderer/features/shared/utils'; -import { AlbumListSort } from '/@/shared/types/domain-types'; +import { AlbumListSort, SortOrder } from '/@/shared/types/domain-types'; import { ItemListKey } from '/@/shared/types/types'; export const useAlbumListFilters = () => { - const { sortBy } = useSortByFilter(null, ItemListKey.ALBUM); + const { sortBy } = useSortByFilter(AlbumListSort.NAME, ItemListKey.ALBUM); - const { sortOrder } = useSortOrderFilter(null, ItemListKey.ALBUM); + const { sortOrder } = useSortOrderFilter(SortOrder.ASC, ItemListKey.ALBUM); const { searchTerm, setSearchTerm } = useSearchTermFilter(''); diff --git a/src/renderer/features/albums/routes/album-list-route.tsx b/src/renderer/features/albums/routes/album-list-route.tsx index 15ea87f02..e7fe0a307 100644 --- a/src/renderer/features/albums/routes/album-list-route.tsx +++ b/src/renderer/features/albums/routes/album-list-route.tsx @@ -7,21 +7,43 @@ import { AlbumListHeader } from '/@/renderer/features/albums/components/album-li import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page'; import { LibraryContainer } from '/@/renderer/features/shared/components/library-container'; import { PageErrorBoundary } from '/@/renderer/features/shared/components/page-error-boundary'; +import { AlbumListQuery } from '/@/shared/types/domain-types'; +import { ItemListKey } from '/@/shared/types/types'; + +const getPageKey = (options: { albumArtistId?: string; genreId?: string }) => { + if (options.albumArtistId) { + return ItemListKey.ALBUM_ARTIST_ALBUM; + } + + if (options.genreId) { + return ItemListKey.GENRE_ALBUM; + } + + return ItemListKey.ALBUM; +}; const AlbumListRoute = () => { const { albumArtistId, genreId } = useParams(); - const pageKey = albumArtistId ? `albumArtistAlbum` : 'album'; + const pageKey = getPageKey({ albumArtistId, genreId }); const [itemCount, setItemCount] = useState(undefined); + const customFilters: Partial = useMemo(() => { + return { + artistIds: albumArtistId ? [albumArtistId] : undefined, + genreIds: genreId ? [genreId] : undefined, + }; + }, [albumArtistId, genreId]); + const providerValue = useMemo(() => { return { + customFilters, id: albumArtistId ?? genreId, itemCount, pageKey, setItemCount, }; - }, [albumArtistId, genreId, itemCount, pageKey, setItemCount]); + }, [albumArtistId, customFilters, genreId, itemCount, pageKey]); return ( diff --git a/src/renderer/features/songs/components/song-list-content.tsx b/src/renderer/features/songs/components/song-list-content.tsx index 2ea57a2a0..38a5b4c06 100644 --- a/src/renderer/features/songs/components/song-list-content.tsx +++ b/src/renderer/features/songs/components/song-list-content.tsx @@ -1,5 +1,6 @@ import { lazy, Suspense, useMemo } from 'react'; +import { useListContext } from '/@/renderer/context/list-context'; import { useSongListFilters } from '/@/renderer/features/songs/hooks/use-song-list-filters'; import { ItemListSettings, useCurrentServer, useListSettings } from '/@/renderer/store'; import { Spinner } from '/@/shared/components/spinner/spinner'; @@ -30,12 +31,15 @@ const SongListPaginatedTable = lazy(() => export const SongListContent = () => { const { display, grid, itemsPerPage, pagination, table } = useListSettings(ItemListKey.SONG); + const { customFilters } = useListContext(); + return ( }> diff --git a/src/renderer/features/songs/components/song-list-header.tsx b/src/renderer/features/songs/components/song-list-header.tsx index 8c370d618..4570007d1 100644 --- a/src/renderer/features/songs/components/song-list-header.tsx +++ b/src/renderer/features/songs/components/song-list-header.tsx @@ -1,16 +1,22 @@ +import { useSuspenseQuery } from '@tanstack/react-query'; +import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { PageHeader } from '/@/renderer/components/page-header/page-header'; import { useListContext } from '/@/renderer/context/list-context'; +import { artistsQueries } from '/@/renderer/features/artists/api/artists-api'; +import { useGenreList } from '/@/renderer/features/genres/api/genres-api'; import { FilterBar } from '/@/renderer/features/shared/components/filter-bar'; import { LibraryHeaderBar } from '/@/renderer/features/shared/components/library-header-bar'; import { ListSearchInput } from '/@/renderer/features/shared/components/list-search-input'; import { SongListHeaderFilters } from '/@/renderer/features/songs/components/song-list-header-filters'; import { useSongListFilters } from '/@/renderer/features/songs/hooks/use-song-list-filters'; +import { useCurrentServerId } from '/@/renderer/store'; import { Flex } from '/@/shared/components/flex/flex'; import { Group } from '/@/shared/components/group/group'; import { Stack } from '/@/shared/components/stack/stack'; import { LibraryItem } from '/@/shared/types/domain-types'; +import { ItemListKey } from '/@/shared/types/types'; interface SongListHeaderProps { genreId?: string; @@ -18,10 +24,7 @@ interface SongListHeaderProps { } export const SongListHeader = ({ title }: SongListHeaderProps) => { - const { t } = useTranslation(); - const { itemCount } = useListContext(); - const pageTitle = title || t('page.trackList.title', { postProcess: 'titleCase' }); return ( @@ -29,7 +32,7 @@ export const SongListHeader = ({ title }: SongListHeaderProps) => { - {pageTitle} + @@ -49,7 +52,56 @@ export const SongListHeader = ({ title }: SongListHeaderProps) => { }; const PlayButton = () => { + const { customFilters } = useListContext(); const { query } = useSongListFilters(); - return ; + const mergedQuery = useMemo(() => { + return { + ...query, + ...(customFilters ?? {}), + }; + }, [query, customFilters]); + + return ; +}; + +const PageTitle = ({ title }: { title?: string }) => { + const { t } = useTranslation(); + const { pageKey } = useListContext(); + const pageTitle = title || t('page.trackList.title', { postProcess: 'titleCase' }); + + switch (pageKey) { + case ItemListKey.ALBUM_ARTIST_SONG: + return ; + case ItemListKey.GENRE_SONG: + return ; + } + + return {pageTitle}; +}; + +const AlbumArtistTitle = () => { + const { id } = useListContext(); + const serverId = useCurrentServerId(); + + const { data: albumArtist } = useSuspenseQuery( + artistsQueries.albumArtistDetail({ + query: { id: id! }, + serverId: serverId, + }), + ); + + return {albumArtist?.name || '—'}; +}; + +const GenreTitle = () => { + const { id } = useListContext(); + + const { data: genre } = useGenreList(); + + const name = useMemo(() => { + return genre?.items.find((g) => g.id === id)?.name || '—'; + }, [id, genre]); + + return {name || '—'}; }; diff --git a/src/renderer/features/songs/hooks/use-song-list-filters.ts b/src/renderer/features/songs/hooks/use-song-list-filters.ts index 997a38ce1..fc77089ab 100644 --- a/src/renderer/features/songs/hooks/use-song-list-filters.ts +++ b/src/renderer/features/songs/hooks/use-song-list-filters.ts @@ -11,13 +11,13 @@ import { useSearchTermFilter } from '/@/renderer/features/shared/hooks/use-searc import { useSortByFilter } from '/@/renderer/features/shared/hooks/use-sort-by-filter'; import { useSortOrderFilter } from '/@/renderer/features/shared/hooks/use-sort-order-filter'; import { customFiltersSchema, FILTER_KEYS } from '/@/renderer/features/shared/utils'; -import { SongListSort } from '/@/shared/types/domain-types'; +import { SongListSort, SortOrder } from '/@/shared/types/domain-types'; import { ItemListKey } from '/@/shared/types/types'; export const useSongListFilters = () => { - const { sortBy } = useSortByFilter(null, ItemListKey.SONG); + const { sortBy } = useSortByFilter(SongListSort.NAME, ItemListKey.SONG); - const { sortOrder } = useSortOrderFilter(null, ItemListKey.SONG); + const { sortOrder } = useSortOrderFilter(SortOrder.ASC, ItemListKey.SONG); const { searchTerm, setSearchTerm } = useSearchTermFilter(''); diff --git a/src/renderer/features/songs/routes/song-list-route.tsx b/src/renderer/features/songs/routes/song-list-route.tsx index 0157beb0c..bf89fc982 100644 --- a/src/renderer/features/songs/routes/song-list-route.tsx +++ b/src/renderer/features/songs/routes/song-list-route.tsx @@ -1,4 +1,5 @@ import { useMemo, useState } from 'react'; +import { useParams } from 'react-router'; import { ListContext } from '/@/renderer/context/list-context'; import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page'; @@ -6,20 +7,43 @@ import { LibraryContainer } from '/@/renderer/features/shared/components/library import { PageErrorBoundary } from '/@/renderer/features/shared/components/page-error-boundary'; import { SongListContent } from '/@/renderer/features/songs/components/song-list-content'; import { SongListHeader } from '/@/renderer/features/songs/components/song-list-header'; +import { SongListQuery } from '/@/shared/types/domain-types'; +import { ItemListKey } from '/@/shared/types/types'; + +const getPageKey = (options: { albumArtistId?: string; genreId?: string }) => { + if (options.albumArtistId) { + return ItemListKey.ALBUM_ARTIST_SONG; + } + + if (options.genreId) { + return ItemListKey.GENRE_SONG; + } + + return ItemListKey.SONG; +}; const SongListRoute = () => { - const pageKey = 'song'; + const { albumArtistId, genreId } = useParams(); + const pageKey = getPageKey({ albumArtistId, genreId }); const [itemCount, setItemCount] = useState(undefined); + const customFilters: Partial = useMemo(() => { + return { + artistIds: albumArtistId ? [albumArtistId] : undefined, + genreIds: genreId ? [genreId] : undefined, + }; + }, [albumArtistId, genreId]); + const providerValue = useMemo(() => { return { - id: undefined, + customFilters, + id: albumArtistId ?? genreId, itemCount, pageKey, setItemCount, }; - }, [itemCount, pageKey, setItemCount]); + }, [albumArtistId, customFilters, genreId, itemCount, pageKey]); return ( diff --git a/src/shared/types/types.ts b/src/shared/types/types.ts index 95fd99f3b..e76d11467 100644 --- a/src/shared/types/types.ts +++ b/src/shared/types/types.ts @@ -16,10 +16,14 @@ import { ServerFeatures } from '/@/shared/types/features-types'; export enum ItemListKey { ALBUM = LibraryItem.ALBUM, ALBUM_ARTIST = LibraryItem.ALBUM_ARTIST, + ALBUM_ARTIST_ALBUM = 'albumArtistAlbum', + ALBUM_ARTIST_SONG = 'albumArtistSong', ALBUM_DETAIL = 'albumDetail', ARTIST = LibraryItem.ARTIST, FULL_SCREEN = 'fullScreen', GENRE = LibraryItem.GENRE, + GENRE_ALBUM = 'genreAlbum', + GENRE_SONG = 'genreSong', PLAYLIST = LibraryItem.PLAYLIST, PLAYLIST_SONG = LibraryItem.PLAYLIST_SONG, QUEUE_SONG = LibraryItem.QUEUE_SONG,