diff --git a/src/renderer/context/list-context.tsx b/src/renderer/context/list-context.tsx index 7f0f46bbf..f07cad07a 100644 --- a/src/renderer/context/list-context.tsx +++ b/src/renderer/context/list-context.tsx @@ -13,6 +13,7 @@ interface ListContextProps { isSmartPlaylist?: boolean; itemCount?: number; listData?: unknown[]; + listKey?: ItemListKey; mode?: 'edit' | 'view'; pageKey: ItemListKey | string; setDisplayMode?: (displayMode: ListDisplayMode) => 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 dda37b632..7203da4cc 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 @@ -21,6 +21,7 @@ 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 { 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'; @@ -70,7 +71,6 @@ const PlaylistDetailSongListGrid = lazy(() => export const PlaylistDetailSongListContent = () => { const { playlistId } = useParams() as { playlistId: string }; const server = useCurrentServer(); - const { displayMode, setItemCount } = useListContext(); const queryClient = useQueryClient(); const playlistSongsQuery = useSuspenseQuery( @@ -82,18 +82,12 @@ export const PlaylistDetailSongListContent = () => { }), ); - useEffect(() => { - if ( - displayMode !== LibraryItem.ALBUM && - playlistSongsQuery.data?.totalRecordCount != null - ) { - setItemCount?.(playlistSongsQuery.data.totalRecordCount); - } - }, [displayMode, playlistSongsQuery.data?.totalRecordCount, setItemCount]); - useEffect(() => { const handleRefresh = async (payload: { key: string }) => { - if (payload.key !== ItemListKey.PLAYLIST_SONG) { + if ( + payload.key !== ItemListKey.PLAYLIST_SONG && + payload.key !== ItemListKey.PLAYLIST_ALBUM + ) { return; } @@ -113,7 +107,7 @@ export const PlaylistDetailSongListContent = () => { return () => { eventEmitter.off('ITEM_LIST_REFRESH', handleRefresh); }; - }, [playlistId, queryClient, server.id]); + }, [playlistId, queryClient, server?.id]); return ( }> @@ -124,7 +118,13 @@ export const PlaylistDetailSongListContent = () => { export type OverridePlaylistSongListQuery = Omit, 'id'>; -export const PlaylistDetailSongListView = ({ data }: { data: PlaylistSongListResponse }) => { +interface PlaylistDetailSongListViewProps { + data: PlaylistSongListResponse; + /** When provided, table/grid use this instead of computing from data (avoids duplicate filter/sort). */ + items?: Song[]; +} + +export const PlaylistDetailSongListView = ({ data, items }: PlaylistDetailSongListViewProps) => { const server = useCurrentServer(); const { display, itemsPerPage, pagination, table } = useListSettings(ItemListKey.PLAYLIST_SONG); const { currentPage, onChange: onPageChange } = useItemListPagination(); @@ -141,7 +141,12 @@ export const PlaylistDetailSongListView = ({ data }: { data: PlaylistSongListRes switch (display) { case ListDisplayType.GRID: { return ( - + ); } case ListDisplayType.TABLE: { @@ -155,6 +160,7 @@ export const PlaylistDetailSongListView = ({ data }: { data: PlaylistSongListRes enableHorizontalBorders={table.enableHorizontalBorders} enableRowHoverHighlight={table.enableRowHoverHighlight} enableVerticalBorders={table.enableVerticalBorders} + items={items} serverId={server.id} size={table.size} {...paginationProps} @@ -363,7 +369,7 @@ export function playlistSongsToAlbums(songs: Song[]): PlaylistAlbumRow[] { return rows; } -const PlaylistDetailAlbumList = ({ data }: { data: PlaylistSongListResponse }) => { +export const PlaylistDetailAlbumView = ({ data }: { data: PlaylistSongListResponse }) => { const player = usePlayer(); const { setItemCount, setListData } = useListContext(); const { detail, display, grid, itemsPerPage, pagination, table } = useListSettings( @@ -528,23 +534,33 @@ const PlaylistDetailAlbumList = ({ data }: { data: PlaylistSongListResponse }) = return renderAlbumList(); }; -const PlaylistDetailSongList = ({ data }: { data: PlaylistSongListResponse }) => { - const { displayMode, isSmartPlaylist, mode } = useListContext(); - - if (displayMode === LibraryItem.ALBUM) { - return ; - } +/** Track view: view mode uses centralized list derivation; edit mode uses local reorder state. */ +const PlaylistDetailTrackView = ({ data }: { data: PlaylistSongListResponse }) => { + const { isSmartPlaylist, mode } = useListContext(); if (isSmartPlaylist) { - return ; + return ; } - switch (mode) { - case 'edit': - return ; - case 'view': - return ; - default: - return null; + if (mode === 'edit') { + return ; } + + return ; +}; + +/** Uses usePlaylistTrackList once and passes derived items to the list view. */ +const PlaylistDetailTrackViewContent = ({ data }: { data: PlaylistSongListResponse }) => { + const { sortedAndFilteredSongs } = usePlaylistTrackList(data); + return ; +}; + +const PlaylistDetailSongList = ({ data }: { data: PlaylistSongListResponse }) => { + const { displayMode } = useListContext(); + + if (displayMode === LibraryItem.ALBUM) { + return ; + } + + 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 ebc847084..2500df74b 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 @@ -16,6 +16,7 @@ import { LibraryItem, PlaylistSongListQuery, PlaylistSongListResponse, + Song, } from '/@/shared/types/domain-types'; import { ItemListKey } from '/@/shared/types/types'; @@ -23,36 +24,44 @@ interface PlaylistDetailSongListGridProps extends Omit, 'query'> { currentPage?: number; data: PlaylistSongListResponse; + items?: Song[]; itemsPerPage?: number; onPageChange?: (page: number) => void; } export const PlaylistDetailSongListGrid = forwardRef( - ({ currentPage, data, itemsPerPage, onPageChange, saveScrollOffset = true }) => { + ({ + currentPage, + data, + items: itemsProp, + itemsPerPage, + onPageChange, + saveScrollOffset = true, + }) => { const { handleOnScrollEnd, scrollOffset } = useItemListScrollPersist({ enabled: saveScrollOffset, }); const { searchTerm } = useSearchTermFilter(); const { query } = usePlaylistSongListFilters(); - const { setListData } = useListContext(); - - const songData = useMemo(() => { - let items = data?.items || []; + const songDataFromData = useMemo(() => { + let list = data?.items || []; if (searchTerm) { - items = searchLibraryItems(items, searchTerm, LibraryItem.SONG); - return items; + list = searchLibraryItems(list, searchTerm, LibraryItem.SONG); + return list; } - - return sortSongList(items, query.sortBy, query.sortOrder); + return sortSongList(list, query.sortBy, query.sortOrder); }, [data?.items, searchTerm, query.sortBy, query.sortOrder]); + const { setListData } = useListContext(); + const songData = itemsProp ?? songDataFromData; + useEffect(() => { - if (setListData) { - setListData(songData); + if (itemsProp == null && setListData) { + setListData(songDataFromData); } - }, [songData, setListData]); + }, [itemsProp, songDataFromData, setListData]); const gridProps = useListSettings(ItemListKey.PLAYLIST_SONG).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 72450e78e..b7f5076d8 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 @@ -46,7 +46,7 @@ export const PlaylistDetailSongListHeaderFilters = ({ isSmartPlaylist, }: PlaylistDetailSongListHeaderFiltersProps) => { const { t } = useTranslation(); - const { mode, setMode } = useListContext(); + const { listKey: listKeyFromContext, mode, setMode } = useListContext(); const { playlistId } = useParams() as { playlistId: string }; const playlistTarget = usePlaylistTarget(); const { setPlaylistBehavior } = useSettingsStoreActions(); @@ -66,8 +66,12 @@ export const PlaylistDetailSongListHeaderFilters = ({ }); }; - const isAlbumMode = playlistTarget === PlaylistTarget.ALBUM; - const listKey = isAlbumMode ? ItemListKey.PLAYLIST_ALBUM : ItemListKey.PLAYLIST_SONG; + const listKey = + listKeyFromContext ?? + (playlistTarget === PlaylistTarget.ALBUM + ? ItemListKey.PLAYLIST_ALBUM + : ItemListKey.PLAYLIST_SONG); + const isAlbumMode = listKey === ItemListKey.PLAYLIST_ALBUM; const toggleChoice = isAlbumMode ? t('entity.album', { count: 2, postProcess: 'titleCase' }) : t('entity.track', { count: 2, postProcess: 'titleCase' }); 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 f27672fb8..14989fe15 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 @@ -27,6 +27,7 @@ interface PlaylistDetailSongListTableProps extends Omit, 'query'> { currentPage?: number; data: PlaylistSongListResponse; + items?: Song[]; itemsPerPage?: number; onPageChange?: (page: number) => void; } @@ -44,6 +45,7 @@ export const PlaylistDetailSongListTable = forwardRef { - let items = data?.items || []; + const songDataFromData = useMemo(() => { + let list = data?.items || []; if (searchTerm) { - items = searchLibraryItems(items, searchTerm, LibraryItem.SONG); - return items; + list = searchLibraryItems(list, searchTerm, LibraryItem.SONG); + return list; } - - return sortSongList(items, query.sortBy, query.sortOrder); + return sortSongList(list, query.sortBy, query.sortOrder); }, [data?.items, searchTerm, query.sortBy, query.sortOrder]); + const { setListData } = useListContext(); + const songData = itemsProp ?? songDataFromData; + useEffect(() => { - if (setListData) { - setListData(songData); + if (itemsProp == null && setListData) { + setListData(songDataFromData); } - }, [songData, setListData]); + }, [itemsProp, songDataFromData, setListData]); const player = usePlayer(); diff --git a/src/renderer/features/playlists/hooks/use-playlist-track-list.ts b/src/renderer/features/playlists/hooks/use-playlist-track-list.ts new file mode 100644 index 000000000..c5f961cd4 --- /dev/null +++ b/src/renderer/features/playlists/hooks/use-playlist-track-list.ts @@ -0,0 +1,36 @@ +import { useEffect, useMemo } from 'react'; + +import { useListContext } from '/@/renderer/context/list-context'; +import { usePlaylistSongListFilters } from '/@/renderer/features/playlists/hooks/use-playlist-song-list-filters'; +import { useSearchTermFilter } from '/@/renderer/features/shared/hooks/use-search-term-filter'; +import { searchLibraryItems } from '/@/renderer/features/shared/utils'; +import { sortSongList } from '/@/shared/api/utils'; +import { LibraryItem, PlaylistSongListResponse, Song } from '/@/shared/types/domain-types'; + +export function usePlaylistTrackList(data: PlaylistSongListResponse | undefined): { + sortedAndFilteredSongs: Song[]; + totalCount: number; +} { + const { setItemCount, setListData } = useListContext(); + const { searchTerm } = useSearchTermFilter(); + const { query } = usePlaylistSongListFilters(); + + const sortedAndFilteredSongs = useMemo(() => { + const raw = data?.items ?? []; + + if (searchTerm) { + return searchLibraryItems(raw, searchTerm, LibraryItem.SONG); + } + + return sortSongList(raw, query.sortBy, query.sortOrder); + }, [data?.items, searchTerm, query.sortBy, query.sortOrder]); + + const totalCount = sortedAndFilteredSongs.length; + + useEffect(() => { + setListData?.(sortedAndFilteredSongs); + setItemCount?.(totalCount); + }, [sortedAndFilteredSongs, totalCount, setListData, setItemCount]); + + return { sortedAndFilteredSongs, totalCount }; +} 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 0abda7d43..fbf839785 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 @@ -402,6 +402,8 @@ const PlaylistDetailSongListRoute = () => { const playlistTarget = usePlaylistTarget(); const displayMode: LibraryItem.ALBUM | LibraryItem.SONG = playlistTarget === PlaylistTarget.ALBUM ? LibraryItem.ALBUM : LibraryItem.SONG; + const listKey = + displayMode === LibraryItem.ALBUM ? ItemListKey.PLAYLIST_ALBUM : ItemListKey.PLAYLIST_SONG; const [itemCount, setItemCount] = useState(undefined); const [listData, setListData] = useState([]); @@ -415,13 +417,14 @@ const PlaylistDetailSongListRoute = () => { isSmartPlaylist, itemCount, listData, + listKey, mode, - pageKey: ItemListKey.PLAYLIST_SONG, + pageKey: listKey, setItemCount, setListData, setMode, }; - }, [playlistId, isSmartPlaylist, displayMode, itemCount, listData, mode]); + }, [playlistId, isSmartPlaylist, displayMode, listKey, itemCount, listData, mode]); return (