diff --git a/src/renderer/features/playlists/components/playlist-detail-album-view.tsx b/src/renderer/features/playlists/components/playlist-detail-album-view.tsx new file mode 100644 index 000000000..fb247ba77 --- /dev/null +++ b/src/renderer/features/playlists/components/playlist-detail-album-view.tsx @@ -0,0 +1,195 @@ +import { useEffect, useMemo } from 'react'; + +import { useGridRows } from '/@/renderer/components/item-list/helpers/use-grid-rows'; +import { useItemListColumnReorder } from '/@/renderer/components/item-list/helpers/use-item-list-column-reorder'; +import { useItemListColumnResize } from '/@/renderer/components/item-list/helpers/use-item-list-column-resize'; +import { useItemListScrollPersist } from '/@/renderer/components/item-list/helpers/use-item-list-scroll-persist'; +import { ItemDetailList } from '/@/renderer/components/item-list/item-detail-list/item-detail-list'; +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 { 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 { DefaultItemControlProps, ItemControls } from '/@/renderer/components/item-list/types'; +import { useListContext } from '/@/renderer/context/list-context'; +import { usePlayer } from '/@/renderer/features/player/context/player-context'; +import { usePlaylistSongListFilters } from '/@/renderer/features/playlists/hooks/use-playlist-song-list-filters'; +import { type PlaylistAlbumRow, playlistSongsToAlbums } from '/@/renderer/features/playlists/utils'; +import { useSearchTermFilter } from '/@/renderer/features/shared/hooks/use-search-term-filter'; +import { searchLibraryItems } from '/@/renderer/features/shared/utils'; +import { useGeneralSettings, useListSettings } from '/@/renderer/store'; +import { sortSongList } from '/@/shared/api/utils'; +import { + LibraryItem, + PlaylistSongListResponse, + SongListSort, + SortOrder, +} from '/@/shared/types/domain-types'; +import { ItemListKey, ListDisplayType, ListPaginationType, Play } from '/@/shared/types/types'; + +export const PlaylistDetailAlbumView = ({ data }: { data: PlaylistSongListResponse }) => { + const player = usePlayer(); + const { setItemCount, setListData } = useListContext(); + const { detail, display, grid, itemsPerPage, pagination, table } = useListSettings( + ItemListKey.PLAYLIST_ALBUM, + ); + const { enableGridMultiSelect } = useGeneralSettings(); + const { currentPage, onChange: onPageChange } = useItemListPagination(); + const { searchTerm } = useSearchTermFilter(); + const { query } = usePlaylistSongListFilters(); + + const sortedAlbums = useMemo(() => { + let songs = data?.items ?? []; + if (searchTerm?.trim()) { + songs = searchLibraryItems(songs, searchTerm, LibraryItem.SONG); + } + const sortedSongs = sortSongList( + songs, + (query.sortBy as SongListSort) ?? SongListSort.ID, + (query.sortOrder as SortOrder) ?? SortOrder.ASC, + ); + return playlistSongsToAlbums(sortedSongs); + }, [data?.items, searchTerm, query.sortBy, query.sortOrder]); + + const isPaginated = pagination === ListPaginationType.PAGINATED; + const totalAlbumCount = sortedAlbums.length; + const albumPageCount = Math.max(1, Math.ceil(totalAlbumCount / itemsPerPage)); + const paginatedAlbums = useMemo(() => { + if (!isPaginated) return sortedAlbums; + const start = currentPage * itemsPerPage; + return sortedAlbums.slice(start, start + itemsPerPage); + }, [isPaginated, currentPage, itemsPerPage, sortedAlbums]); + const albumsToRender = isPaginated ? paginatedAlbums : sortedAlbums; + + const playlistSongs = useMemo(() => data?.items ?? [], [data?.items]); + + const albumControlOverrides = useMemo>(() => { + return { + onPlay: ({ + item, + itemType, + playType, + }: DefaultItemControlProps & { playType: Play }) => { + if (!item) return; + const rowSongs = (item as PlaylistAlbumRow)._playlistSongs; + if (itemType === LibraryItem.ALBUM && rowSongs?.length) { + player.addToQueueByData(rowSongs, playType); + return; + } + player.addToQueueByFetch(item._serverId, [item.id], itemType, playType); + }, + }; + }, [player]); + + useEffect(() => { + setItemCount?.(totalAlbumCount); + }, [setItemCount, totalAlbumCount]); + + useEffect(() => { + setListData?.(data?.items ?? []); + }, [data?.items, setListData]); + + const { handleOnScrollEnd, scrollOffset } = useItemListScrollPersist({ enabled: true }); + const { handleColumnReordered } = useItemListColumnReorder({ + itemListKey: ItemListKey.PLAYLIST_ALBUM, + }); + const { handleColumnResized } = useItemListColumnResize({ + itemListKey: ItemListKey.PLAYLIST_ALBUM, + }); + const { handleColumnReordered: handleDetailColumnReordered } = useItemListColumnReorder({ + itemListKey: ItemListKey.PLAYLIST_ALBUM, + tableKey: 'detail', + }); + const { handleColumnResized: handleDetailColumnResized } = useItemListColumnResize({ + itemListKey: ItemListKey.PLAYLIST_ALBUM, + tableKey: 'detail', + }); + const rows = useGridRows(LibraryItem.ALBUM, ItemListKey.PLAYLIST_ALBUM, grid.size); + + const renderAlbumList = () => { + switch (display) { + case ListDisplayType.DETAIL: + return ( + { + if (playlistSongs.length === 0) return; + internalState?.setSelected([item]); + player.addToQueueByData(playlistSongs, Play.NOW, item.id); + }} + overrideControls={albumControlOverrides} + scrollOffset={scrollOffset ?? 0} + songsByAlbumId={{}} + tableId="album-detail" + /> + ); + case ListDisplayType.GRID: + return ( + + ); + case ListDisplayType.TABLE: + return ( + + ); + default: + return null; + } + }; + + if (isPaginated) { + return ( + + {renderAlbumList()} + + ); + } + + return renderAlbumList(); +}; diff --git a/src/renderer/features/playlists/components/playlist-detail-song-list-content.tsx b/src/renderer/features/playlists/components/playlist-detail-song-list-content.tsx index 7203da4cc..2b95ed4d5 100644 --- a/src/renderer/features/playlists/components/playlist-detail-song-list-content.tsx +++ b/src/renderer/features/playlists/components/playlist-detail-song-list-content.tsx @@ -2,45 +2,25 @@ import { useQueryClient, useSuspenseQuery } from '@tanstack/react-query'; import { lazy, Suspense, useEffect, useMemo, useRef, useState } from 'react'; import { useParams } from 'react-router'; -import { useGridRows } from '/@/renderer/components/item-list/helpers/use-grid-rows'; -import { useItemListColumnReorder } from '/@/renderer/components/item-list/helpers/use-item-list-column-reorder'; -import { useItemListColumnResize } from '/@/renderer/components/item-list/helpers/use-item-list-column-resize'; -import { useItemListScrollPersist } from '/@/renderer/components/item-list/helpers/use-item-list-scroll-persist'; -import { ItemDetailList } from '/@/renderer/components/item-list/item-detail-list/item-detail-list'; -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 { 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 { - DefaultItemControlProps, - ItemControls, - ItemListHandle, -} from '/@/renderer/components/item-list/types'; +import { ItemListHandle } from '/@/renderer/components/item-list/types'; import { useListContext } from '/@/renderer/context/list-context'; import { eventEmitter } from '/@/renderer/events/event-emitter'; -import { usePlayer } from '/@/renderer/features/player/context/player-context'; import { playlistsQueries } from '/@/renderer/features/playlists/api/playlists-api'; +import { PlaylistDetailAlbumView } from '/@/renderer/features/playlists/components/playlist-detail-album-view'; import { usePlaylistTrackList } from '/@/renderer/features/playlists/hooks/use-playlist-track-list'; -import { useSortByFilter } from '/@/renderer/features/shared/hooks/use-sort-by-filter'; -import { useSortOrderFilter } from '/@/renderer/features/shared/hooks/use-sort-order-filter'; -import { useCurrentServer, useGeneralSettings, useListSettings } from '/@/renderer/store'; -import { sortAlbumList } from '/@/shared/api/utils'; +import { useCurrentServer, useListSettings } from '/@/renderer/store'; import { Spinner } from '/@/shared/components/spinner/spinner'; import { - Album, - AlbumListSort, LibraryItem, PlaylistSongListQuery, PlaylistSongListResponse, Song, - SortOrder, } from '/@/shared/types/domain-types'; import { ItemListKey, ListDisplayType, ListPaginationType, - Play, TableColumn, } from '/@/shared/types/types'; @@ -303,237 +283,6 @@ export const PlaylistDetailSongListEdit = ({ data }: { data: PlaylistSongListRes } }; -export type PlaylistAlbumRow = Album & { _playlistSongs?: Song[] }; - -export function playlistSongsToAlbums(songs: Song[]): PlaylistAlbumRow[] { - if (songs.length === 0) return []; - - const rows: PlaylistAlbumRow[] = []; - let group: Song[] = [songs[0]]; - let prevAlbumId = songs[0].albumId; - - const pushRow = (song: Song, groupSongs: Song[]) => { - rows.push({ - _itemType: LibraryItem.ALBUM, - _playlistSongs: groupSongs, - _serverId: song._serverId, - _serverType: song._serverType, - albumArtistName: song.albumArtistName, - albumArtists: song.albumArtists, - artists: song.artists, - comment: song.comment, - createdAt: song.createdAt, - duration: null, - explicitStatus: song.explicitStatus, - genres: song.genres, - id: song.albumId, - imageId: song.imageId, - imageUrl: song.imageUrl, - isCompilation: song.compilation, - lastPlayedAt: song.lastPlayedAt, - mbzId: null, - mbzReleaseGroupId: null, - name: song.album ?? '', - originalDate: null, - originalYear: null, - participants: song.participants, - playCount: null, - recordLabels: [], - releaseDate: song.releaseDate, - releaseType: null, - releaseTypes: [], - releaseYear: song.releaseYear, - size: null, - songCount: null, - sortName: song.album ?? '', - tags: song.tags, - updatedAt: song.updatedAt, - userFavorite: false, - userRating: null, - version: null, - }); - }; - - for (let i = 1; i < songs.length; i++) { - const song = songs[i]; - if (song.albumId === prevAlbumId) { - group.push(song); - } else { - pushRow(group[0], group); - group = [song]; - prevAlbumId = song.albumId; - } - } - pushRow(group[0], group); - - return rows; -} - -export const PlaylistDetailAlbumView = ({ data }: { data: PlaylistSongListResponse }) => { - const player = usePlayer(); - const { setItemCount, setListData } = useListContext(); - const { detail, display, grid, itemsPerPage, pagination, table } = useListSettings( - ItemListKey.PLAYLIST_ALBUM, - ); - const { enableGridMultiSelect } = useGeneralSettings(); - const { currentPage, onChange: onPageChange } = useItemListPagination(); - const { sortBy } = useSortByFilter(AlbumListSort.ID, ItemListKey.PLAYLIST_ALBUM); - const { sortOrder } = useSortOrderFilter(SortOrder.ASC, ItemListKey.PLAYLIST_ALBUM); - - const albums = useMemo(() => playlistSongsToAlbums(data?.items ?? []), [data?.items]); - const sortedAlbums = useMemo( - () => - sortAlbumList( - albums, - (sortBy as AlbumListSort) ?? AlbumListSort.ID, - sortOrder ?? SortOrder.ASC, - ), - [albums, sortBy, sortOrder], - ); - - const isPaginated = pagination === ListPaginationType.PAGINATED; - const totalAlbumCount = sortedAlbums.length; - const albumPageCount = Math.max(1, Math.ceil(totalAlbumCount / itemsPerPage)); - const paginatedAlbums = useMemo(() => { - if (!isPaginated) return sortedAlbums; - const start = currentPage * itemsPerPage; - return sortedAlbums.slice(start, start + itemsPerPage); - }, [isPaginated, currentPage, itemsPerPage, sortedAlbums]); - const albumsToRender = isPaginated ? paginatedAlbums : sortedAlbums; - - const playlistSongs = useMemo(() => data?.items ?? [], [data?.items]); - - const albumControlOverrides = useMemo>(() => { - return { - onPlay: ({ - item, - itemType, - playType, - }: DefaultItemControlProps & { playType: Play }) => { - if (!item) return; - const rowSongs = (item as PlaylistAlbumRow)._playlistSongs; - if (itemType === LibraryItem.ALBUM && rowSongs?.length) { - player.addToQueueByData(rowSongs, playType); - return; - } - player.addToQueueByFetch(item._serverId, [item.id], itemType, playType); - }, - }; - }, [player]); - - useEffect(() => { - setItemCount?.(totalAlbumCount); - }, [setItemCount, totalAlbumCount]); - - useEffect(() => { - setListData?.(data?.items ?? []); - }, [data?.items, setListData]); - - const { handleOnScrollEnd, scrollOffset } = useItemListScrollPersist({ enabled: true }); - const { handleColumnReordered } = useItemListColumnReorder({ - itemListKey: ItemListKey.PLAYLIST_ALBUM, - }); - const { handleColumnResized } = useItemListColumnResize({ - itemListKey: ItemListKey.PLAYLIST_ALBUM, - }); - const { handleColumnReordered: handleDetailColumnReordered } = useItemListColumnReorder({ - itemListKey: ItemListKey.PLAYLIST_ALBUM, - tableKey: 'detail', - }); - const { handleColumnResized: handleDetailColumnResized } = useItemListColumnResize({ - itemListKey: ItemListKey.PLAYLIST_ALBUM, - tableKey: 'detail', - }); - const rows = useGridRows(LibraryItem.ALBUM, ItemListKey.PLAYLIST_ALBUM, grid.size); - - const renderAlbumList = () => { - switch (display) { - case ListDisplayType.DETAIL: - return ( - { - if (playlistSongs.length === 0) return; - internalState?.setSelected([item]); - player.addToQueueByData(playlistSongs, Play.NOW, item.id); - }} - overrideControls={albumControlOverrides} - scrollOffset={scrollOffset ?? 0} - songsByAlbumId={{}} - tableId="album-detail" - /> - ); - case ListDisplayType.GRID: - return ( - - ); - case ListDisplayType.TABLE: - return ( - - ); - default: - return null; - } - }; - - if (isPaginated) { - return ( - - {renderAlbumList()} - - ); - } - - return renderAlbumList(); -}; - /** Track view: view mode uses centralized list derivation; edit mode uses local reorder state. */ const PlaylistDetailTrackView = ({ data }: { data: PlaylistSongListResponse }) => { const { isSmartPlaylist, mode } = useListContext(); diff --git a/src/renderer/features/playlists/components/playlist-detail-song-list-header-filters.tsx b/src/renderer/features/playlists/components/playlist-detail-song-list-header-filters.tsx index b7f5076d8..dc15d93c9 100644 --- a/src/renderer/features/playlists/components/playlist-detail-song-list-header-filters.tsx +++ b/src/renderer/features/playlists/components/playlist-detail-song-list-header-filters.tsx @@ -35,7 +35,7 @@ import { Group } from '/@/shared/components/group/group'; import { Icon } from '/@/shared/components/icon/icon'; import { Tooltip } from '/@/shared/components/tooltip/tooltip'; import { useLocalStorage } from '/@/shared/hooks/use-local-storage'; -import { AlbumListSort, LibraryItem, SongListSort, SortOrder } from '/@/shared/types/domain-types'; +import { LibraryItem, SongListSort, SortOrder } from '/@/shared/types/domain-types'; import { ItemListKey } from '/@/shared/types/types'; interface PlaylistDetailSongListHeaderFiltersProps { @@ -103,37 +103,18 @@ export const PlaylistDetailSongListHeaderFilters = ({ {toggleChoice} - {isAlbumMode ? ( - <> - - - - - ) : ( - <> - - - - - )} + + + {!collapsed && } diff --git a/src/renderer/features/playlists/utils.ts b/src/renderer/features/playlists/utils.ts index aa5b44c52..dc23ad657 100644 --- a/src/renderer/features/playlists/utils.ts +++ b/src/renderer/features/playlists/utils.ts @@ -1,8 +1,75 @@ import { nanoid } from 'nanoid/non-secure'; import { NDSongQueryFields } from '/@/shared/api/navidrome/navidrome-types'; +import { Album, LibraryItem, Song } from '/@/shared/types/domain-types'; import { QueryBuilderGroup } from '/@/shared/types/types'; +export type PlaylistAlbumRow = Album & { _playlistSongs?: Song[] }; + +export function playlistSongsToAlbums(songs: Song[]): PlaylistAlbumRow[] { + if (songs.length === 0) return []; + + const rows: PlaylistAlbumRow[] = []; + let group: Song[] = [songs[0]]; + let prevAlbumId = songs[0].albumId; + + const pushRow = (song: Song, groupSongs: Song[]) => { + rows.push({ + _itemType: LibraryItem.ALBUM, + _playlistSongs: groupSongs, + _serverId: song._serverId, + _serverType: song._serverType, + albumArtistName: song.albumArtistName, + albumArtists: song.albumArtists, + artists: song.artists, + comment: song.comment, + createdAt: song.createdAt, + duration: null, + explicitStatus: song.explicitStatus, + genres: song.genres, + id: song.albumId, + imageId: song.imageId, + imageUrl: song.imageUrl, + isCompilation: song.compilation, + lastPlayedAt: song.lastPlayedAt, + mbzId: null, + mbzReleaseGroupId: null, + name: song.album ?? '', + originalDate: null, + originalYear: null, + participants: song.participants, + playCount: null, + recordLabels: [], + releaseDate: song.releaseDate, + releaseType: null, + releaseTypes: [], + releaseYear: song.releaseYear, + size: null, + songCount: null, + sortName: song.album ?? '', + tags: song.tags, + updatedAt: song.updatedAt, + userFavorite: false, + userRating: null, + version: null, + }); + }; + + for (let i = 1; i < songs.length; i++) { + const song = songs[i]; + if (song.albumId === prevAlbumId) { + group.push(song); + } else { + pushRow(group[0], group); + group = [song]; + prevAlbumId = song.albumId; + } + } + pushRow(group[0], group); + + return rows; +} + export const parseQueryBuilderChildren = (groups: QueryBuilderGroup[], data: any[]) => { if (groups.length === 0) { return data;