import React from 'react'; import { AudioLines, ChevronRight, Heart, Play, Square } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import type { ColDef } from '../../utils/useTracklistColumns'; import type { SubsonicSong } from '../../api/subsonicTypes'; import type { Track } from '../../store/playerStoreTypes'; import { songToTrack } from '../../utils/playback/songToTrack'; import { useSelectionStore } from '../../store/selectionStore'; import { useThemeStore } from '../../store/themeStore'; import { usePreviewStore } from '../../store/previewStore'; import StarRating from '../StarRating'; import { codecLabel, type ColKey } from '../../utils/componentHelpers/albumTrackListHelpers'; import { formatLongDuration } from '../../utils/format/formatDuration'; type ContextMenuFn = ( x: number, y: number, track: Track, type: 'song' | 'album' | 'artist' | 'queue-item' | 'album-song', ) => void; interface TrackRowProps { song: SubsonicSong; globalIdx: number; visibleCols: readonly ColDef[]; gridStyle: React.CSSProperties; currentTrackId: string | null; isPlaying: boolean; ratingValue: number; isStarred: boolean; inSelectMode: boolean; isContextMenuSong: boolean; onPlaySong: (song: SubsonicSong) => void; onDoubleClickSong?: (song: SubsonicSong) => void; onRate: (songId: string, rating: number) => void; onToggleSongStar: (song: SubsonicSong, e: React.MouseEvent) => void; onContextMenu: ContextMenuFn; onToggleSelect: (id: string, globalIdx: number, shift: boolean) => void; onDragStart: (song: SubsonicSong, me: MouseEvent) => void; setContextMenuSongId: (id: string | null) => void; } /** * Memoised tracklist row. Subscribes to its own selection + preview state * via primitive selectors so only this row re-renders when the user * toggles selection or starts/stops a preview. */ export const TrackRow = React.memo(function TrackRow({ song, globalIdx, visibleCols, gridStyle, currentTrackId, isPlaying, ratingValue, isStarred, inSelectMode, isContextMenuSong, onPlaySong, onDoubleClickSong, onRate, onToggleSongStar, onContextMenu, onToggleSelect, onDragStart, setContextMenuSongId, }: TrackRowProps) { const { t } = useTranslation(); const navigate = useNavigate(); const showBitrate = useThemeStore(s => s.showBitrate); const isSelected = useSelectionStore(s => s.selectedIds.has(song.id)); const isActive = currentTrackId === song.id; const isPreviewing = usePreviewStore(s => s.previewingId === song.id); const isPreviewAudioStarted = usePreviewStore(s => s.previewingId === song.id && s.audioStarted); const renderCell = (colDef: ColDef) => { const key = colDef.key as ColKey; switch (key) { case 'num': return (
{ e.stopPropagation(); onToggleSelect(song.id, globalIdx, e.shiftKey); }} /> {isActive && isPlaying ? ( ) : ( {song.track ?? '—'} )}
); case 'title': return (
{song.title}
); case 'artist': { const artistRefs = song.artists && song.artists.length > 0 ? song.artists : [{ id: song.artistId, name: song.artist }]; return (
{artistRefs.map((a, i) => ( {i > 0 &&  · } { if (a.id) { e.stopPropagation(); navigate(`/artist/${a.id}`); } }} > {a.name ?? song.artist} ))}
); } case 'favorite': return (
); case 'rating': return ( onRate(song.id, r)} /> ); case 'duration': return (
{formatLongDuration(song.duration)}
); case 'format': return (
{(song.suffix || (showBitrate && song.bitRate)) && ( {codecLabel(song, showBitrate)} )}
); case 'genre': return (
{song.genre ?? '—'}
); default: return null; } }; return (
{ if ((e.target as HTMLElement).closest('button, a, input')) return; if (e.ctrlKey || e.metaKey) { onToggleSelect(song.id, globalIdx, false); } else if (inSelectMode) { onToggleSelect(song.id, globalIdx, e.shiftKey); } else { onPlaySong(song); } }} onDoubleClick={onDoubleClickSong ? e => { if ((e.target as HTMLElement).closest('button, a, input')) return; if (e.ctrlKey || e.metaKey || inSelectMode) return; onDoubleClickSong(song); } : undefined} onContextMenu={e => { e.preventDefault(); setContextMenuSongId(song.id); onContextMenu(e.clientX, e.clientY, songToTrack(song), 'album-song'); }} role="row" onMouseDown={e => { if (e.button !== 0) return; e.preventDefault(); 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); onDragStart(song, me); } }; const onUp = () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); }; document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp); }} > {visibleCols.map(colDef => renderCell(colDef))}
); });