import type { SubsonicSong } from '../api/subsonicTypes'; import { songToTrack } from '../utils/playback/songToTrack'; import React, { memo, useMemo } from 'react'; import { Play, ListPlus, Star, Disc3 } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { usePlayerStore } from '../store/playerStore'; import { CoverArtImage } from '../cover/CoverArtImage'; import { useCoverArt } from '../cover/useCoverArt'; import { useTrackCoverRef } from '../cover/useLibraryCoverRef'; import { COVER_DENSE_RAIL_CELL_CSS_PX } from '../cover/layoutSizes'; import { enqueueAndPlay } from '../utils/playback/playSong'; import { useDragDrop } from '../contexts/DragDropContext'; import { useOrbitSongRowBehavior } from '@/features/orbit'; import { useNavigateToAlbum } from '../hooks/useNavigateToAlbum'; import { useNavigateToArtist } from '../hooks/useNavigateToArtist'; import { OpenArtistRefInline } from './OpenArtistRefInline'; import { resolveTrackArtistRefs } from '../utils/playback/trackArtistRefs'; interface SongCardProps { song: SubsonicSong; disableArtwork?: boolean; /** Layout-native cover square width in CSS px (rail cell). */ displayCssPx?: number; /** @deprecated Use displayCssPx */ artworkSize?: number; } function SongCard({ song, disableArtwork = false, displayCssPx = COVER_DENSE_RAIL_CELL_CSS_PX, artworkSize, }: SongCardProps) { const layoutPx = artworkSize ?? displayCssPx; const { t } = useTranslation(); const navigateToArtist = useNavigateToArtist(); const openContextMenu = usePlayerStore(s => s.openContextMenu); const enqueue = usePlayerStore(s => s.enqueue); const coverRef = useTrackCoverRef(song, undefined, { libraryResolve: false }); const coverHandle = useCoverArt(coverRef, layoutPx, { surface: 'dense', ensurePriority: 'middle', }); const coverUrl = coverHandle.src; const psyDrag = useDragDrop(); const { orbitActive, addTrackToOrbit } = useOrbitSongRowBehavior(); const navigateToAlbum = useNavigateToAlbum(); const handlePlay = () => { if (orbitActive) { addTrackToOrbit(song.id); return; } enqueueAndPlay(song); }; const handleEnqueue = () => { if (orbitActive) { addTrackToOrbit(song.id); return; } enqueue([songToTrack(song)]); }; const handleClick = handlePlay; const artistRefs = useMemo(() => resolveTrackArtistRefs(song), [song]); const handleAlbumClick = (e: React.MouseEvent) => { if (!song.albumId) return; e.stopPropagation(); navigateToAlbum(song.albumId); }; return (
e.key === 'Enter' && handleClick()} onContextMenu={(e) => { e.preventDefault(); openContextMenu(e.clientX, e.clientY, song, 'song'); }} onMouseDown={e => { if (e.button !== 0) return; const sx = e.clientX, sy = e.clientY; const onMove = (me: MouseEvent) => { if (Math.abs(me.clientX - sx) > 5 || Math.abs(me.clientY - sy) > 5) { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); psyDrag.startDrag( { data: JSON.stringify({ type: 'song', id: song.id, name: song.title }), label: song.title, coverUrl: coverUrl || undefined }, me.clientX, me.clientY, ); } }; const onUp = () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); }; document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp); }} >
{!disableArtwork && coverRef ? ( ) : (
)}

{song.title}

navigateToArtist(id)} as="none" linkTag="span" linkClassName="track-artist-link" />

{song.albumId && ( )} {(song.userRating ?? 0) > 0 && (
{Array.from({ length: 5 }, (_, i) => ( ))}
)}
); } export default memo(SongCard);