import React from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import { AudioLines, Check, ChevronDown, ChevronRight, ListPlus, Play, RotateCcw, Square, X, } from 'lucide-react'; import type { ColDef } from '../../utils/useTracklistColumns'; import type { SubsonicSong } from '../../api/subsonicTypes'; import { usePlayerStore } from '../../store/playerStore'; import { usePreviewStore } from '../../store/previewStore'; import { useSelectionStore } from '../../store/selectionStore'; import { useDragDrop } from '../../contexts/DragDropContext'; import { useOrbitSongRowBehavior } from '../../hooks/useOrbitSongRowBehavior'; import { songToTrack } from '../../utils/playback/songToTrack'; import { formatTrackTime } from '../../utils/format/formatDuration'; import { formatLastSeen } from '../../utils/componentHelpers/userMgmtHelpers'; import i18n from '../../i18n'; import { AddToPlaylistSubmenu } from '../ContextMenu'; import StarRating from '../StarRating'; const SORTABLE_COLUMNS = new Set(['title', 'artist', 'album', 'rating', 'duration', 'playCount', 'lastPlayed', 'bpm']); interface Props { visibleSongs: SubsonicSong[]; selectedIds: Set; selectedCount: number; inSelectMode: boolean; toggleSelect: (id: string, idx: number, shift: boolean) => void; showPlPicker: boolean; setShowPlPicker: React.Dispatch>; allColumns: readonly ColDef[]; visibleCols: ColDef[]; gridStyle: React.CSSProperties; colVisible: Set; toggleColumn: (key: string) => void; resetColumns: () => void; pickerOpen: boolean; setPickerOpen: React.Dispatch>; pickerRef: React.RefObject; tracklistRef: React.RefObject; startResize: (e: React.MouseEvent, colIndex: number, direction?: 1 | -1) => void; handleSortClick: (key: string) => void; getSortIndicator: (key: string) => React.ReactNode; ratings: Record; handleRate: (songId: string, rating: number) => void; removeSong: (id: string) => void; hasFilters: boolean; } export default function FavoritesSongsTracklist({ visibleSongs, selectedIds, selectedCount, inSelectMode, toggleSelect, showPlPicker, setShowPlPicker, allColumns, visibleCols, gridStyle, colVisible, toggleColumn, resetColumns, pickerOpen, setPickerOpen, pickerRef, tracklistRef, startResize, handleSortClick, getSortIndicator, ratings, handleRate, removeSong, hasFilters, }: Props) { const { t } = useTranslation(); const navigate = useNavigate(); const currentTrack = usePlayerStore(s => s.currentTrack); const isPlaying = usePlayerStore(s => s.isPlaying); const playTrack = usePlayerStore(s => s.playTrack); const openContextMenu = usePlayerStore(s => s.openContextMenu); const userRatingOverrides = usePlayerStore(s => s.userRatingOverrides); const previewingId = usePreviewStore(s => s.previewingId); const previewAudioStarted = usePreviewStore(s => s.audioStarted); const psyDrag = useDragDrop(); const { orbitActive, queueHint, addTrackToOrbit } = useOrbitSongRowBehavior(); return (
{ if (inSelectMode && e.target === e.currentTarget) useSelectionStore.getState().clearAll(); }}> {/* ── Bulk action bar ── */} {inSelectMode && (
{t('common.bulkSelected', { count: selectedCount })}
{showPlPicker && ( { setShowPlPicker(false); useSelectionStore.getState().clearAll(); }} dropDown /> )}
)} {/* Column visibility picker */}
{pickerOpen && (
{t('albumDetail.columns')}
{allColumns.filter(c => !c.required).map(c => { const label = c.i18nKey ? t(`albumDetail.${c.i18nKey}`) : c.key; const isOn = colVisible.has(c.key); return ( ); })}
)}
{visibleCols.map((colDef, colIndex) => { const key = colDef.key; const isLastCol = colIndex === visibleCols.length - 1; const label = colDef.i18nKey ? t(`albumDetail.${colDef.i18nKey}`) : ''; if (key === 'num') { const allSelected = selectedCount === visibleSongs.length && visibleSongs.length > 0; return (
{ e.stopPropagation(); if (allSelected) { useSelectionStore.getState().clearAll(); } else { useSelectionStore.getState().setSelectedIds(() => new Set(visibleSongs.map(s => s.id))); } }} /> #
); } if (key === 'title') { const hasNextCol = colIndex + 1 < visibleCols.length; const canSort = SORTABLE_COLUMNS.has('title'); return (
handleSortClick('title')} > {label} {canSort && getSortIndicator('title')}
{hasNextCol &&
startResize(e, colIndex + 1, -1)} />}
); } if (key === 'remove') return
; const isCentered = key === 'duration' || key === 'rating'; const canSort = SORTABLE_COLUMNS.has(key); return (
canSort && handleSortClick(key)} > {label} {canSort && getSortIndicator(key)}
{!isLastCol &&
startResize(e, colIndex, 1)} />}
); })}
{visibleSongs.map((song, i) => { const track = songToTrack(song); const isSelected = selectedIds.has(song.id); return (
{ if ((e.target as HTMLElement).closest('button, a, input')) return; if (e.ctrlKey || e.metaKey) { toggleSelect(song.id, i, false); } else if (inSelectMode) { toggleSelect(song.id, i, e.shiftKey); } else if (orbitActive) { queueHint(); } else { playTrack(track, visibleSongs.map(songToTrack)); } }} onDoubleClick={orbitActive ? e => { if ((e.target as HTMLElement).closest('button, a, input')) return; if (e.ctrlKey || e.metaKey || inSelectMode) return; addTrackToOrbit(song.id); } : undefined} onContextMenu={e => { e.preventDefault(); openContextMenu(e.clientX, e.clientY, track, 'favorite-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); const { selectedIds: selIds } = useSelectionStore.getState(); if (selIds.has(song.id) && selIds.size > 1) { const bulkTracks = visibleSongs.filter(s => selIds.has(s.id)).map(songToTrack); psyDrag.startDrag({ data: JSON.stringify({ type: 'songs', tracks: bulkTracks }), label: `${bulkTracks.length} Songs` }, me.clientX, me.clientY); } else { psyDrag.startDrag({ data: JSON.stringify({ type: 'song', track }), label: song.title }, me.clientX, me.clientY); } } }; const onUp = () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); }; document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp); }} > {visibleCols.map(colDef => { switch (colDef.key) { case 'num': return (
{ e.stopPropagation(); toggleSelect(song.id, i, e.shiftKey); }} /> {currentTrack?.id === song.id && isPlaying ? ( ) : ( {i + 1} )}
); case 'title': return (
{song.title}
); case 'artist': return (
song.artistId && navigate(`/artist/${song.artistId}`)}>{song.artist}
); case 'album': return (
{song.albumId ? ( { e.stopPropagation(); navigate(`/album/${song.albumId}`); }} > {song.album} ) : ( {song.album} )}
); case 'genre': return (
{song.genre ?? '—'}
); case 'format': return (
{(song.suffix || song.bitRate) && ( {song.suffix?.toUpperCase()} {song.suffix && song.bitRate && ' · '} {song.bitRate && `${song.bitRate} kbps`} )}
); case 'rating': return ( handleRate(song.id, r)} /> ); case 'duration': return (
{formatTrackTime(song.duration)}
); case 'playCount': return (
{song.playCount ?? '—'}
); case 'lastPlayed': return (
{song.played ? formatLastSeen(song.played, i18n.language, '—') : '—'}
); case 'bpm': return (
{song.bpm && song.bpm > 0 ? song.bpm : '—'}
); case 'remove': return (
); default: return null; } })}
); })} {/* Empty state when filters return no results */} {visibleSongs.length === 0 && hasFilters && (
{t('favorites.noFilterResults')}
)}
); }