diff --git a/src/renderer/api/subsonic/subsonic-controller.ts b/src/renderer/api/subsonic/subsonic-controller.ts index 8caa00416..1caa79411 100644 --- a/src/renderer/api/subsonic/subsonic-controller.ts +++ b/src/renderer/api/subsonic/subsonic-controller.ts @@ -38,6 +38,7 @@ const ALBUM_LIST_SORT_MAPPING: Record void; onRangeChanged?: (range: { startIndex: number; stopIndex: number }) => Promise | void; onScrollEnd?: (rowIndex: number) => void; + onSongRowDoubleClick?: (params: { + index: number; + internalState: ItemListStateActions; + item: Song; + }) => void; + overrideControls?: Partial; rowHeight?: number; scrollOffset?: number; + songsByAlbumId?: Record; tableId?: string; } @@ -109,7 +117,13 @@ interface RowData { getItem?: (index: number) => unknown; internalState: ItemListStateActions; isMutatingFavorite: boolean; + onSongRowDoubleClick?: (params: { + index: number; + internalState: ItemListStateActions; + item: Song; + }) => void; registerSongs: (albumId: string, songs: Song[]) => void; + songsByAlbumId?: Record; trackColumns: ItemTableListColumnConfig[]; trackTableSize: 'compact' | 'default' | 'large'; } @@ -126,6 +140,11 @@ interface TrackRowProps { internalState: ItemListStateActions; isMutatingFavorite: boolean; isSongsLoading?: boolean; + onSongRowDoubleClick?: (params: { + index: number; + internalState: ItemListStateActions; + item: Song; + }) => void; rowIndex: number; size: 'compact' | 'default' | 'large'; song: Song; @@ -147,6 +166,7 @@ const TrackRow = memo( internalState, isMutatingFavorite, isSongsLoading, + onSongRowDoubleClick, rowIndex, size, song, @@ -167,11 +187,37 @@ const TrackRow = memo( (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); + if (onSongRowDoubleClick) { + onSongRowDoubleClick({ + index: internalState.findItemIndex(song.id), + internalState, + item: song, + }); + return; + } + if (controls?.onDoubleClick) { + controls.onDoubleClick({ + event: e, + index: internalState.findItemIndex(song.id), + internalState, + item: song, + itemType: LibraryItem.SONG, + }); + return; + } if (isSongsLoading || albumSongs.length === 0) return; internalState.setSelected([song]); playerContext.addToQueueByData(albumSongs, Play.NOW, song.id); }, - [albumSongs, internalState, isSongsLoading, playerContext, song], + [ + albumSongs, + controls, + internalState, + isSongsLoading, + onSongRowDoubleClick, + playerContext, + song, + ], ); const handleRowClick = useCallback( @@ -610,7 +656,9 @@ const RowContent = memo( index, internalState, isMutatingFavorite, + onSongRowDoubleClick, registerSongs, + songsByAlbumId, trackColumns, trackTableSize, }: RowContentProps) => { @@ -622,8 +670,10 @@ const RowContent = memo( return (data?.[index] as Album | undefined) || undefined; }, [data, getItem, index]); + const useClientSideSongs = Boolean(songsByAlbumId); + const songListQuery = useMemo(() => { - if (!item?.id || !item?._serverId) return null; + if (useClientSideSongs || !item?.id || !item?._serverId) return null; return { query: { albumIds: [item.id], @@ -634,7 +684,7 @@ const RowContent = memo( }, serverId: item?._serverId || '', }; - }, [item]); + }, [item, useClientSideSongs]); const { data: songListData, isLoading: isSongsQueryLoading } = useQuery({ enabled: !!songListQuery, @@ -646,8 +696,17 @@ const RowContent = memo( }), }); - const songItems = songListData?.items; - const isSongsLoading = !!item && isSongsQueryLoading && !songItems?.length; + const songItemsFromQuery = songListData?.items; + const songItemsFromClient = useMemo(() => { + const rowSongs = (item as { _playlistSongs?: Song[] })?._playlistSongs; + if (rowSongs?.length) return rowSongs; + if (!songsByAlbumId || !item?.id) return undefined; + return songsByAlbumId[item.id]; + }, [item, songsByAlbumId]); + + const songItems = useClientSideSongs ? songItemsFromClient : songItemsFromQuery; + const isSongsLoading = + !useClientSideSongs && !!item && isSongsQueryLoading && !songItemsFromQuery?.length; const songs = useMemo(() => { return ( @@ -705,6 +764,7 @@ const RowContent = memo( isMutatingFavorite={isMutatingFavorite} isSongsLoading={isSongsLoading} key={song.id} + onSongRowDoubleClick={onSongRowDoubleClick} rowIndex={rowIndex} size={trackTableSize} song={song as Song} @@ -729,6 +789,7 @@ const RowContent = memo( prev.isMutatingFavorite === next.isMutatingFavorite && prev.controls === next.controls && prev.registerSongs === next.registerSongs && + prev.songsByAlbumId === next.songsByAlbumId && prev.trackColumns === next.trackColumns && prev.trackTableSize === next.trackTableSize, ); @@ -1113,10 +1174,14 @@ export const ItemDetailList = ({ getItem, itemCount: externalItemCount, items, + listKey = ItemListKey.ALBUM, onColumnReordered, onColumnResized, onRangeChanged, onScrollEnd, + onSongRowDoubleClick, + overrideControls, + songsByAlbumId, tableId = DEFAULT_DETAIL_TABLE_ID, }: ItemDetailListProps) => { const containerRef = useRef(null); @@ -1127,6 +1192,7 @@ export const ItemDetailList = ({ const controls = useDefaultItemListControls({ onColumnReordered, onColumnResized, + overrides: overrideControls, }); const isMutatingCreateFavorite = useIsMutatingCreateFavorite(); const isMutatingDeleteFavorite = useIsMutatingDeleteFavorite(); @@ -1172,7 +1238,7 @@ export const ItemDetailList = ({ const internalState = useItemListState(getDataFn, extractRowIdSong); - const tableConfig = useSettingsStore((state) => state.lists[ItemListKey.ALBUM]?.detail); + const tableConfig = useSettingsStore((state) => state.lists[listKey]?.detail); const trackColumns = useMemo((): ItemTableListColumnConfig[] => { const raw = tableConfig?.columns; if (raw && raw.length > 0) { @@ -1263,8 +1329,10 @@ export const ItemDetailList = ({ getItem, internalState, isMutatingFavorite, + onSongRowDoubleClick, queryClient, registerSongs, + songsByAlbumId, trackColumns, trackTableSize, }), @@ -1279,8 +1347,10 @@ export const ItemDetailList = ({ getItem, internalState, isMutatingFavorite, + onSongRowDoubleClick, queryClient, registerSongs, + songsByAlbumId, trackColumns, trackTableSize, ], diff --git a/src/renderer/context/list-context.tsx b/src/renderer/context/list-context.tsx index 89ec02867..7f0f46bbf 100644 --- a/src/renderer/context/list-context.tsx +++ b/src/renderer/context/list-context.tsx @@ -1,9 +1,13 @@ import { createContext, useContext } from 'react'; +import { LibraryItem } from '/@/shared/types/domain-types'; import { ItemListKey } from '/@/shared/types/types'; +export type ListDisplayMode = LibraryItem.ALBUM | LibraryItem.SONG; + interface ListContextProps { customFilters?: Record; + displayMode?: ListDisplayMode; id?: string; isSidebarOpen?: boolean; isSmartPlaylist?: boolean; @@ -11,6 +15,7 @@ interface ListContextProps { listData?: unknown[]; mode?: 'edit' | 'view'; pageKey: ItemListKey | string; + setDisplayMode?: (displayMode: ListDisplayMode) => void; setIsSidebarOpen?: (isSidebarOpen: boolean) => void; setItemCount?: (itemCount: number) => void; setListData?: (items: unknown[]) => void; 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 b0910b311..dda37b632 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,14 +2,46 @@ import { useQueryClient, useSuspenseQuery } from '@tanstack/react-query'; import { lazy, Suspense, useEffect, useMemo, useRef, useState } from 'react'; import { useParams } from 'react-router'; -import { ItemListHandle } from '/@/renderer/components/item-list/types'; +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 { 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 { useCurrentServer, useListSettings } from '/@/renderer/store'; +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 { Spinner } from '/@/shared/components/spinner/spinner'; -import { PlaylistSongListQuery, PlaylistSongListResponse } from '/@/shared/types/domain-types'; -import { ItemListKey, ListDisplayType, TableColumn } from '/@/shared/types/types'; +import { + Album, + AlbumListSort, + LibraryItem, + PlaylistSongListQuery, + PlaylistSongListResponse, + Song, + SortOrder, +} from '/@/shared/types/domain-types'; +import { + ItemListKey, + ListDisplayType, + ListPaginationType, + Play, + TableColumn, +} from '/@/shared/types/types'; const PlaylistDetailSongListTable = lazy(() => import('/@/renderer/features/playlists/components/playlist-detail-song-list-table').then( @@ -38,7 +70,7 @@ const PlaylistDetailSongListGrid = lazy(() => export const PlaylistDetailSongListContent = () => { const { playlistId } = useParams() as { playlistId: string }; const server = useCurrentServer(); - const { setItemCount } = useListContext(); + const { displayMode, setItemCount } = useListContext(); const queryClient = useQueryClient(); const playlistSongsQuery = useSuspenseQuery( @@ -52,12 +84,12 @@ export const PlaylistDetailSongListContent = () => { useEffect(() => { if ( - playlistSongsQuery.data?.totalRecordCount !== undefined && - playlistSongsQuery.data.totalRecordCount !== null + displayMode !== LibraryItem.ALBUM && + playlistSongsQuery.data?.totalRecordCount != null ) { setItemCount?.(playlistSongsQuery.data.totalRecordCount); } - }, [playlistSongsQuery.data?.totalRecordCount, setItemCount]); + }, [displayMode, playlistSongsQuery.data?.totalRecordCount, setItemCount]); useEffect(() => { const handleRefresh = async (payload: { key: string }) => { @@ -94,11 +126,23 @@ export type OverridePlaylistSongListQuery = Omit, export const PlaylistDetailSongListView = ({ data }: { data: PlaylistSongListResponse }) => { const server = useCurrentServer(); - const { display, table } = useListSettings(ItemListKey.PLAYLIST_SONG); + const { display, itemsPerPage, pagination, table } = useListSettings(ItemListKey.PLAYLIST_SONG); + const { currentPage, onChange: onPageChange } = useItemListPagination(); + const isPaginated = pagination === ListPaginationType.PAGINATED; + + const paginationProps = isPaginated + ? { + currentPage, + itemsPerPage, + onPageChange, + } + : undefined; switch (display) { case ListDisplayType.GRID: { - return ; + return ( + + ); } case ListDisplayType.TABLE: { return ( @@ -113,6 +157,7 @@ export const PlaylistDetailSongListView = ({ data }: { data: PlaylistSongListRes enableVerticalBorders={table.enableVerticalBorders} serverId={server.id} size={table.size} + {...paginationProps} /> ); } @@ -252,8 +297,243 @@ 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; +} + +const PlaylistDetailAlbumList = ({ 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(); +}; + const PlaylistDetailSongList = ({ data }: { data: PlaylistSongListResponse }) => { - const { isSmartPlaylist, mode } = useListContext(); + const { displayMode, isSmartPlaylist, mode } = useListContext(); + + if (displayMode === LibraryItem.ALBUM) { + return ; + } if (isSmartPlaylist) { return ; diff --git a/src/renderer/features/playlists/components/playlist-detail-song-list-grid.tsx b/src/renderer/features/playlists/components/playlist-detail-song-list-grid.tsx index e4c5b919c..ebc847084 100644 --- a/src/renderer/features/playlists/components/playlist-detail-song-list-grid.tsx +++ b/src/renderer/features/playlists/components/playlist-detail-song-list-grid.tsx @@ -4,6 +4,7 @@ import { useEffect } from 'react'; import { useGridRows } from '/@/renderer/components/item-list/helpers/use-grid-rows'; 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 { ItemListWithPagination } from '/@/renderer/components/item-list/item-list-pagination/item-list-pagination'; import { ItemListGridComponentProps } from '/@/renderer/components/item-list/types'; import { useListContext } from '/@/renderer/context/list-context'; import { usePlaylistSongListFilters } from '/@/renderer/features/playlists/hooks/use-playlist-song-list-filters'; @@ -20,11 +21,14 @@ import { ItemListKey } from '/@/shared/types/types'; interface PlaylistDetailSongListGridProps extends Omit, 'query'> { + currentPage?: number; data: PlaylistSongListResponse; + itemsPerPage?: number; + onPageChange?: (page: number) => void; } export const PlaylistDetailSongListGrid = forwardRef( - ({ data, saveScrollOffset = true }) => { + ({ currentPage, data, itemsPerPage, onPageChange, saveScrollOffset = true }) => { const { handleOnScrollEnd, scrollOffset } = useItemListScrollPersist({ enabled: saveScrollOffset, }); @@ -59,9 +63,22 @@ export const PlaylistDetailSongListGrid = forwardRef { + if (!isPaginated || currentPage == null || itemsPerPage == null) return songData; + const start = currentPage * itemsPerPage; + return songData.slice(start, start + itemsPerPage); + }, [currentPage, isPaginated, itemsPerPage, songData]); + const dataToRender = isPaginated ? paginatedData : songData; + + const grid = ( ); + + if (isPaginated && itemsPerPage != null) { + return ( + + {grid} + + ); + } + + return grid; }, ); 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 23303aa02..72450e78e 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 @@ -5,7 +5,11 @@ import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router'; import i18n from '/@/i18n/i18n'; -import { PLAYLIST_SONG_TABLE_COLUMNS } from '/@/renderer/components/item-list/item-table-list/default-columns'; +import { + ALBUM_TABLE_COLUMNS, + PLAYLIST_SONG_TABLE_COLUMNS, + SONG_TABLE_COLUMNS, +} from '/@/renderer/components/item-list/item-table-list/default-columns'; import { useListContext } from '/@/renderer/context/list-context'; import { ContextMenuController } from '/@/renderer/features/context-menu/context-menu-controller'; import { playlistsQueries } from '/@/renderer/features/playlists/api/playlists-api'; @@ -17,7 +21,12 @@ import { ListSortByDropdown } from '/@/renderer/features/shared/components/list- import { ListSortOrderToggleButton } from '/@/renderer/features/shared/components/list-sort-order-toggle-button'; import { MoreButton } from '/@/renderer/features/shared/components/more-button'; import { useContainerQuery } from '/@/renderer/hooks'; -import { useCurrentServerId } from '/@/renderer/store'; +import { + PlaylistTarget, + useCurrentServerId, + usePlaylistTarget, + useSettingsStoreActions, +} from '/@/renderer/store'; import { ActionIcon } from '/@/shared/components/action-icon/action-icon'; import { Button } from '/@/shared/components/button/button'; import { Divider } from '/@/shared/components/divider/divider'; @@ -26,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 { LibraryItem, SongListSort, SortOrder } from '/@/shared/types/domain-types'; +import { AlbumListSort, LibraryItem, SongListSort, SortOrder } from '/@/shared/types/domain-types'; import { ItemListKey } from '/@/shared/types/types'; interface PlaylistDetailSongListHeaderFiltersProps { @@ -39,6 +48,8 @@ export const PlaylistDetailSongListHeaderFilters = ({ const { t } = useTranslation(); const { mode, setMode } = useListContext(); const { playlistId } = useParams() as { playlistId: string }; + const playlistTarget = usePlaylistTarget(); + const { setPlaylistBehavior } = useSettingsStoreActions(); const serverId = useCurrentServerId(); const detailQuery = useQuery(playlistsQueries.detail({ query: { id: playlistId }, serverId })); @@ -55,9 +66,21 @@ export const PlaylistDetailSongListHeaderFilters = ({ }); }; + const isAlbumMode = playlistTarget === PlaylistTarget.ALBUM; + const listKey = isAlbumMode ? ItemListKey.PLAYLIST_ALBUM : ItemListKey.PLAYLIST_SONG; + const toggleChoice = isAlbumMode + ? t('entity.album', { count: 2, postProcess: 'titleCase' }) + : t('entity.track', { count: 2, postProcess: 'titleCase' }); + + const handleToggleDisplayMode = useCallback(() => { + setPlaylistBehavior( + playlistTarget === PlaylistTarget.ALBUM ? PlaylistTarget.TRACK : PlaylistTarget.ALBUM, + ); + }, [playlistTarget, setPlaylistBehavior]); + const { ref: containerRef, ...breakpoints } = useContainerQuery(); - const isViewEditMode = !isSmartPlaylist && breakpoints.isSm; + const isViewEditMode = !isSmartPlaylist && (breakpoints.isSm || isAlbumMode); const isEditMode = mode === 'edit'; const [collapsed, setCollapsed] = useLocalStorage({ @@ -68,20 +91,47 @@ export const PlaylistDetailSongListHeaderFilters = ({ return ( - + - + {isAlbumMode ? ( + <> + + + + + ) : ( + <> + + + + + )} {!collapsed && } - + @@ -109,11 +159,25 @@ export const PlaylistDetailSongListHeaderFilters = ({ variant="subtle" /> - - + + {isAlbumMode ? ( + + ) : ( + + )} ); diff --git a/src/renderer/features/playlists/components/playlist-detail-song-list-table.tsx b/src/renderer/features/playlists/components/playlist-detail-song-list-table.tsx index 8cf431454..f27672fb8 100644 --- a/src/renderer/features/playlists/components/playlist-detail-song-list-table.tsx +++ b/src/renderer/features/playlists/components/playlist-detail-song-list-table.tsx @@ -4,6 +4,7 @@ import { useEffect } from 'react'; 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 { ItemListWithPagination } from '/@/renderer/components/item-list/item-list-pagination/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 { ItemControls, ItemListTableComponentProps } from '/@/renderer/components/item-list/types'; @@ -24,7 +25,10 @@ import { ItemListKey, Play } from '/@/shared/types/types'; interface PlaylistDetailSongListTableProps extends Omit, 'query'> { + currentPage?: number; data: PlaylistSongListResponse; + itemsPerPage?: number; + onPageChange?: (page: number) => void; } export const PlaylistDetailSongListTable = forwardRef( @@ -32,6 +36,7 @@ export const PlaylistDetailSongListTable = forwardRef { + if (!isPaginated || currentPage == null || itemsPerPage == null) return songData; + const start = currentPage * itemsPerPage; + return songData.slice(start, start + itemsPerPage); + }, [isPaginated, currentPage, itemsPerPage, songData]); + const dataToRender = isPaginated ? paginatedData : songData; + + const table = ( ); + + if (isPaginated && itemsPerPage != null) { + return ( + + {table} + + ); + } + + return table; }, ); diff --git a/src/renderer/features/playlists/routes/playlist-detail-song-list-route.tsx b/src/renderer/features/playlists/routes/playlist-detail-song-list-route.tsx index ae29d349c..0abda7d43 100644 --- a/src/renderer/features/playlists/routes/playlist-detail-song-list-route.tsx +++ b/src/renderer/features/playlists/routes/playlist-detail-song-list-route.tsx @@ -20,7 +20,7 @@ import { AnimatedPage } from '/@/renderer/features/shared/components/animated-pa import { JsonPreview } from '/@/renderer/features/shared/components/json-preview'; import { PageErrorBoundary } from '/@/renderer/features/shared/components/page-error-boundary'; import { AppRoute } from '/@/renderer/router/routes'; -import { useCurrentServer } from '/@/renderer/store'; +import { PlaylistTarget, useCurrentServer, usePlaylistTarget } from '/@/renderer/store'; import { Button } from '/@/shared/components/button/button'; import { Group } from '/@/shared/components/group/group'; import { Icon } from '/@/shared/components/icon/icon'; @@ -29,7 +29,7 @@ import { Spinner } from '/@/shared/components/spinner/spinner'; import { Stack } from '/@/shared/components/stack/stack'; import { Text } from '/@/shared/components/text/text'; import { toast } from '/@/shared/components/toast/toast'; -import { ServerType, SongListSort } from '/@/shared/types/domain-types'; +import { LibraryItem, ServerType, SongListSort } from '/@/shared/types/domain-types'; import { ItemListKey } from '/@/shared/types/types'; interface PlaylistQueryEditorProps { @@ -154,14 +154,17 @@ const PlaylistQueryEditor = ({ }, [detailQuery?.data?.rules?.order, detailQuery?.data?.rules?.sort]); return ( -
- - +
+ +