import React from 'react'; import { Play, Star } from 'lucide-react'; import { SubsonicSong } from '../api/subsonic'; import { Track } from '../store/playerStore'; import { useTranslation } from 'react-i18next'; function formatDuration(seconds: number): string { const m = Math.floor(seconds / 60); const s = seconds % 60; return `${m}:${s.toString().padStart(2, '0')}`; } function codecLabel(song: { suffix?: string; bitRate?: number }): string { const parts: string[] = []; if (song.suffix) parts.push(song.suffix.toUpperCase()); if (song.bitRate) parts.push(`${song.bitRate} kbps`); return parts.join(' · '); } function StarRating({ value, onChange }: { value: number; onChange: (r: number) => void }) { const { t } = useTranslation(); const [hover, setHover] = React.useState(0); return (
{[1, 2, 3, 4, 5].map(n => ( ))}
); } interface AlbumTrackListProps { songs: SubsonicSong[]; hasVariousArtists: boolean; currentTrack: Track | null; isPlaying: boolean; hoveredSongId: string | null; setHoveredSongId: (id: string | null) => void; ratings: Record; starredSongs: Set; onPlaySong: (song: SubsonicSong) => void; onRate: (songId: string, rating: number) => void; onToggleSongStar: (song: SubsonicSong, e: React.MouseEvent) => void; onContextMenu: (x: number, y: number, track: Track, type: 'song' | 'album' | 'artist' | 'queue-item' | 'album-song') => void; } export default function AlbumTrackList({ songs, hasVariousArtists, currentTrack, isPlaying, hoveredSongId, setHoveredSongId, ratings, starredSongs, onPlaySong, onRate, onToggleSongStar, onContextMenu, }: AlbumTrackListProps) { const { t } = useTranslation(); const totalDuration = songs.reduce((acc, s) => acc + s.duration, 0); const discs = new Map(); songs.forEach(song => { const disc = song.discNumber ?? 1; if (!discs.has(disc)) discs.set(disc, []); discs.get(disc)!.push(song); }); const discNums = Array.from(discs.keys()).sort((a, b) => a - b); const isMultiDisc = discNums.length > 1; const makeTrack = (song: SubsonicSong): Track => ({ id: song.id, title: song.title, artist: song.artist, album: song.album, albumId: song.albumId, artistId: song.artistId, duration: song.duration, coverArt: song.coverArt, track: song.track, year: song.year, bitRate: song.bitRate, suffix: song.suffix, userRating: song.userRating, starred: song.starred, }); return (
#
{t('albumDetail.trackTitle')}
{hasVariousArtists &&
{t('albumDetail.trackArtist')}
}
{t('albumDetail.trackFavorite')}
{t('albumDetail.trackRating')}
{t('albumDetail.trackDuration')}
{t('albumDetail.trackFormat')}
{discNums.map(discNum => (
{isMultiDisc && (
💿 CD {discNum}
)} {discs.get(discNum)!.map((song, i) => (
setHoveredSongId(song.id)} onMouseLeave={() => setHoveredSongId(null)} onDoubleClick={() => onPlaySong(song)} onContextMenu={e => { e.preventDefault(); onContextMenu(e.clientX, e.clientY, makeTrack(song), 'album-song'); }} role="row" draggable onDragStart={e => { e.dataTransfer.effectAllowed = 'copy'; e.dataTransfer.setData('text/plain', JSON.stringify({ type: 'song', track: makeTrack(song) })); }} >
onPlaySong(song)} > {hoveredSongId === song.id && currentTrack?.id !== song.id ? : currentTrack?.id === song.id && isPlaying ?
: currentTrack?.id === song.id ? : (song.track ?? i + 1)}
{song.title}
{hasVariousArtists && (
{song.artist}
)}
onRate(song.id, r)} />
{formatDuration(song.duration)}
{(song.suffix || song.bitRate) && ( {codecLabel(song)} )}
))}
))}
{t('albumDetail.trackTotal')} {formatDuration(totalDuration)}
); }