import { useQueryClient, useSuspenseQuery } from '@tanstack/react-query'; import { lazy, Suspense, useEffect, useMemo, useRef, useState } from 'react'; import { useParams } from 'react-router'; import { useItemListPagination } from '/@/renderer/components/item-list/item-list-pagination/use-item-list-pagination'; import { ItemListHandle } from '/@/renderer/components/item-list/types'; import { useListContext } from '/@/renderer/context/list-context'; import { eventEmitter } from '/@/renderer/events/event-emitter'; 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 { useCurrentServer, useListSettings } from '/@/renderer/store'; import { Spinner } from '/@/shared/components/spinner/spinner'; import { LibraryItem, PlaylistSongListQuery, PlaylistSongListResponse, Song, } from '/@/shared/types/domain-types'; import { ItemListKey, ListDisplayType, ListPaginationType, TableColumn, } from '/@/shared/types/types'; const PlaylistDetailSongListTable = lazy(() => import('/@/renderer/features/playlists/components/playlist-detail-song-list-table').then( (module) => ({ default: module.PlaylistDetailSongListTable, }), ), ); const PlaylistDetailSongListEditTable = lazy(() => import('/@/renderer/features/playlists/components/playlist-detail-song-list-table').then( (module) => ({ default: module.PlaylistDetailSongListEditTable, }), ), ); const PlaylistDetailSongListGrid = lazy(() => import('/@/renderer/features/playlists/components/playlist-detail-song-list-grid').then( (module) => ({ default: module.PlaylistDetailSongListGrid, }), ), ); export const PlaylistDetailSongListContent = () => { const { playlistId } = useParams() as { playlistId: string }; const server = useCurrentServer(); const queryClient = useQueryClient(); const playlistSongsQuery = useSuspenseQuery( playlistsQueries.songList({ query: { id: playlistId, }, serverId: server?.id, }), ); useEffect(() => { const handleRefresh = async (payload: { key: string }) => { if ( payload.key !== ItemListKey.PLAYLIST_SONG && payload.key !== ItemListKey.PLAYLIST_ALBUM ) { return; } const queryKey = playlistsQueries.songList({ query: { id: playlistId, }, serverId: server?.id, }).queryKey; await queryClient.invalidateQueries({ queryKey }); await queryClient.refetchQueries({ queryKey }); }; eventEmitter.on('ITEM_LIST_REFRESH', handleRefresh); return () => { eventEmitter.off('ITEM_LIST_REFRESH', handleRefresh); }; }, [playlistId, queryClient, server?.id]); return ( }> ); }; export type OverridePlaylistSongListQuery = Omit, 'id'>; 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(); const isPaginated = pagination === ListPaginationType.PAGINATED; const paginationProps = isPaginated ? { currentPage, itemsPerPage, onPageChange, } : undefined; switch (display) { case ListDisplayType.GRID: { return ( ); } case ListDisplayType.TABLE: { return ( ); } default: return null; } }; export const PlaylistDetailSongListEdit = ({ data }: { data: PlaylistSongListResponse }) => { const { playlistId } = useParams() as { playlistId: string }; const server = useCurrentServer(); const { display, table } = useListSettings(ItemListKey.PLAYLIST_SONG); const [localData, setLocalData] = useState(data); const tableRef = useRef(null); // Listen for playlist reorder events useEffect(() => { const handleReorder = (payload: { edge: 'bottom' | 'top' | null; playlistId: string; sourceIds: string[]; targetId: string; }) => { // Only handle events for this playlist if (payload.playlistId !== playlistId) { return; } setLocalData((prev) => { if (!prev?.items || !payload.edge) { return prev; } // Create a list of IDs in current order const currentIds = prev.items.map((item) => item.id); // Find the target index const targetIndex = currentIds.indexOf(payload.targetId); if (targetIndex === -1) { return prev; } // Remove all source IDs from their current positions const idsWithoutSources = currentIds.filter( (id) => !payload.sourceIds.includes(id), ); // Calculate the insertion index based on the original target position const sourcesBeforeTarget = payload.sourceIds.filter((id) => { const sourceIndex = currentIds.indexOf(id); return sourceIndex !== -1 && sourceIndex < targetIndex; }).length; // Calculate the insert index in the filtered list const insertIndexInFiltered = payload.edge === 'top' ? targetIndex - sourcesBeforeTarget : targetIndex - sourcesBeforeTarget + 1; // Ensure insertIndex is within bounds const insertIndex = Math.max( 0, Math.min(insertIndexInFiltered, idsWithoutSources.length), ); // Insert source IDs at the calculated position const reorderedIds = [ ...idsWithoutSources.slice(0, insertIndex), ...payload.sourceIds, ...idsWithoutSources.slice(insertIndex), ]; // Create a map for quick lookup const itemMap = new Map(prev.items.map((item) => [item.id, item])); // Reorder items based on new ID order const reorderedItems = reorderedIds .map((id) => itemMap.get(id)) .filter((item): item is NonNullable => item !== undefined); return { ...prev, items: reorderedItems, }; }); }; eventEmitter.on('PLAYLIST_REORDER', handleReorder); return () => { eventEmitter.off('PLAYLIST_REORDER', handleReorder); }; }, [playlistId]); const columns = useMemo(() => { return [ { align: 'center' as 'center' | 'end' | 'start', id: TableColumn.PLAYLIST_REORDER, isEnabled: true, pinned: 'left' as 'left' | 'right' | null, width: 100, }, ...table.columns, ]; }, [table.columns]); const { setListData } = useListContext(); useEffect(() => { setListData?.(localData.items); }, [localData, setListData]); switch (display) { case ListDisplayType.GRID: case ListDisplayType.TABLE: { return ( ); } default: return null; } }; /** 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 ; } 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 ; };