import { closestCenter, DndContext, DragEndEvent, DragOverlay, DragStartEvent, PointerSensor, TouchSensor, useSensor, useSensors, } from '@dnd-kit/core' import { SyntheticListenerMap } from '@dnd-kit/core/dist/hooks/utilities' import { restrictToVerticalAxis } from '@dnd-kit/modifiers' import { SortableContext, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable' import { CSS } from '@dnd-kit/utilities' import { HeartFillIcon } from '@primer/octicons-react' import { InfiniteData } from '@tanstack/react-query' import { ReactNode, useMemo, useState } from 'react' import { useNavigate } from 'react-router-dom' import { Virtuoso } from 'react-virtuoso' import { MediaItem } from '../api/jellyfin' import { useDropdownContext } from '../context/DropdownContext/DropdownContext' import { IMenuItems } from '../context/DropdownContext/DropdownContextProvider' import { usePlaybackContext } from '../context/PlaybackContext/PlaybackContext' import { useDisplayItems } from '../hooks/useDisplayItems' import { formatDateYear } from '../utils/formatDate' import { JellyImg } from './JellyImg' import { Loader } from './Loader' import { IReviver } from './PlaybackManager' import { Skeleton } from './Skeleton' import { Squircle } from './Squircle' import { DeletingIcon, DownloadedIcon, DownloadingIcon, PlaystateAnimationMedalist } from './SvgIcons' export const MediaList = ({ items = [], infiniteData = { pageParams: [], pages: [] }, indexOffset = 0, isLoading, type, title, reviver, loadMore, hidden: _hidden = {}, disableActions = false, albumDisplayMode = 'artist', isDraggable, }: { items: MediaItem[] | undefined infiniteData: InfiniteData | undefined indexOffset?: number isLoading: boolean type: 'song' | 'album' | 'artist' | 'playlist' title: string reviver?: IReviver | 'persistAll' loadMore?: () => void hidden?: IMenuItems disableActions?: boolean albumDisplayMode?: 'artist' | 'year' | 'both' isDraggable?: boolean }) => { const playback = usePlaybackContext() const navigate = useNavigate() const { displayItems, setRowRefs } = useDisplayItems(items, isLoading) const hidden: IMenuItems = disableActions ? { ..._hidden, add_to_favorite: true, remove_from_favorite: true, add_to_playlist: true, remove_from_playlist: true, } : _hidden const dropdown = useDropdownContext() const [activeId, setActiveId] = useState(null) const handleDragEnd = (event: DragEndEvent) => { const { active, over } = event if (over && active.id !== over.id) { const oldIndex = items.findIndex(i => (i.queueId || i.Id) === active.id) const newIndex = items.findIndex(i => (i.queueId || i.Id) === over.id) if (oldIndex !== -1 && newIndex !== -1) { const globalOldIndex = indexOffset + oldIndex const globalNewIndex = indexOffset + newIndex playback.moveItemInPlaylist(globalOldIndex, globalNewIndex) // Force update the display items const updated = Array.from(items) const [moved] = updated.splice(oldIndex, 1) updated.splice(newIndex, 0, moved) displayItems.length = 0 displayItems.push(...updated) } } setActiveId(null) } const handleDragStart = ({ active }: DragStartEvent) => { setActiveId(active.id as string) } const isEqual = (a?: MediaItem, b?: MediaItem) => { if (!a || !b) return false return isDraggable && a.queueId && b.queueId ? a.queueId === b.queueId : a.Id === b.Id } const handleSongClick = (item: MediaItem, index: number) => { if (type === 'song') { if (isEqual(playback.currentTrack, item)) { playback.togglePlayPause() } else { playback.setCurrentPlaylist({ pages: infiniteData, title, reviver }) playback.playTrack(indexOffset + index) } } } const renderItem = ( index: number, item: MediaItem | { isPlaceholder: true }, listeners?: SyntheticListenerMap | undefined ) => { if ('isPlaceholder' in item) { if (type === 'album') { return (
setRowRefs(index, el)}>
) } else if (type === 'artist') { return (
setRowRefs(index, el)}>
) } else if (type === 'playlist') { return (
setRowRefs(index, el)}>
) } else { return (
setRowRefs(index, el)}>
) } } const isActive = isEqual(dropdown.selectedItem, item) && dropdown.isOpen const itemClass = [ type === 'song' && isEqual(playback.currentTrack, item) ? (playback.isPlaying ? 'playing' : 'paused') : '', isActive ? 'active' : '', ] .filter(Boolean) .join(' ') if (type === 'album') { return (
navigate(`/album/${item.Id}`)} ref={el => setRowRefs(index, el)} onContextMenu={e => dropdown.onContextMenu(e, { item }, false, hidden)} onTouchStart={e => dropdown.onTouchStart(e, { item }, false, hidden)} onTouchMove={dropdown.onTouchClear} onTouchEnd={dropdown.onTouchClear} >
{item.Name}
{albumDisplayMode === 'year' && (
{formatDateYear(item.PremiereDate)}
)} {albumDisplayMode === 'artist' && (
{item.AlbumArtist || 'Unknown Artist'}
)} {albumDisplayMode === 'both' && ( <>
{formatDateYear(item.PremiereDate)}
{item.AlbumArtist || 'Unknown Artist'}
)}
) } else if (type === 'artist') { return (
navigate(`/artist/${item.Id}`)} ref={el => setRowRefs(index, el)} onContextMenu={e => dropdown.onContextMenu(e, { item }, false, hidden)} onTouchStart={e => dropdown.onTouchStart(e, { item }, false, hidden)} onTouchMove={dropdown.onTouchClear} onTouchEnd={dropdown.onTouchClear} >
{item.Name || 'Unknown Artist'}
) } else if (type === 'playlist') { return (
navigate(`/playlist/${item.Id}`)} ref={el => setRowRefs(index, el)} onContextMenu={e => dropdown.onContextMenu(e, { item }, false, hidden)} onTouchStart={e => dropdown.onTouchStart(e, { item }, false, hidden)} onTouchMove={dropdown.onTouchClear} onTouchEnd={dropdown.onTouchClear} >
{item.Name}
{item.ChildCount || 0}{' '} {(item.ChildCount || 0) === 1 ? 'Track' : 'Tracks'}
) } else { return (
handleSongClick(item, index)} ref={el => setRowRefs(index, el)} onContextMenu={e => dropdown.onContextMenu(e, { item }, false, hidden)} onTouchStart={e => dropdown.onTouchStart(e, { item }, false, hidden)} onTouchMove={dropdown.onTouchClear} onTouchEnd={dropdown.onTouchClear} >
{item.Name}
{item.Artists && item.Artists.length > 0 ? item.Artists.join(', ') : 'Unknown Artist'}
{item.Album || 'Unknown Album'}
) } } if (isLoading && items.length === 0) { return } if (items.length === 0 && !isLoading) { return (
{type === 'song' ? 'No tracks were found' : type === 'album' ? 'No albums were found' : type === 'artist' ? 'No artists were found' : 'No playlists were found'}
) } return (
    {isDraggable && ( { if ('isPlaceholder' in item) { return renderItem(index, item) } return ( renderItem(index, item, listeners)} /> ) }} endReached={loadMore} overscan={800} /> )} {!isDraggable && ( )}
) } const DraggableVirtuoso = ({ handleDragStart, handleDragEnd, items, renderItem, activeId, children, }: { handleDragStart: (event: DragStartEvent) => void handleDragEnd: (event: DragEndEvent) => void items: MediaItem[] renderItem: (index: number, item: MediaItem | { isPlaceholder: true }) => React.ReactNode activeId: string | null children: React.ReactNode }) => { const sensors = useSensors(useSensor(PointerSensor), useSensor(TouchSensor)) return ( i.queueId || i.Id)} strategy={verticalListSortingStrategy}> {children} {activeId ? renderItem( items.findIndex(i => (i.queueId || i.Id) === activeId), items.find(i => (i.queueId || i.Id) === activeId) || { isPlaceholder: true } ) : null} ) } const SortableItem = ({ id, cb, }: { id: string cb: (props: { listeners: SyntheticListenerMap | undefined }) => ReactNode }) => { const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id }) const style = { transform: CSS.Transform.toString(transform), transition: isDragging ? 'none' : transition, zIndex: isDragging ? 999 : undefined, opacity: isDragging ? 0 : 1, } const memoCb = useMemo(() => cb({ listeners }), [listeners, cb]) return (
  • {memoCb}
  • ) } const MediaIndicators = ({ item, disableActions, listeners, isDraggable, }: { item: MediaItem disableActions: boolean listeners?: SyntheticListenerMap | undefined isDraggable?: boolean }) => { return (
    {item.offlineState && (
    {item.offlineState === 'downloading' && (
    )} {item.offlineState === 'downloaded' && (
    )} {item.offlineState === 'deleting' && (
    )}
    )} {!disableActions && item.UserData?.IsFavorite && location.pathname !== '/favorites' && (
    )} {isDraggable && (
    )}
    ) }