mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
2ddc2a2345
Five-cut cluster pulling the dashboard subcards out of NowPlaying.tsx. 1119 → 711 LOC (−408). Each card was already a top-level React.memo'd block with a clean prop interface, so this is straight file-per-card extraction with no behaviour change. Hero — full hero strip: cover image, title, artist/album/year/age sub, format/codec/sample-rate/bit-depth/duration badges with the Hi-Res chip, the favourite + Last.fm-love + lyrics action buttons, play-count line, and the Last.fm track + artist stats rows (with their per-row `you played N×` highlight). renderStars (5-star inline indicator) moves inline as a private helper since only Hero calls it. ArtistCard — about-this-artist card: optional hero image (filtered through isRealArtistImage so we don't render the well-known "2a96…" placeholder), name, sanitized + clamp-with-Read-more bio, similar-artist chip row. Owns its bioExpanded / bioOverflows state and the useLayoutEffect that measures overflow. AlbumCard — from-this-album card: meta line (year / track position / total duration / play count), sliding-window tracklist anchored on the current track (top 10 by default, or the 10 ending at the running track if it's beyond position 10), and the show-all toggle. TopSongsCard — top-songs-by-artist card: rank-ordered list (max 8), each row clicks into playerStore via the onPlay callback. CreditsCard — contributor / song-info card: role-grouped list with i18n role label lookup (falls back to the raw role string). NowPlaying drops the now-unused imports (useLayoutEffect, LastfmIcon, Star, MicVocal, Heart, Headphones, TrendingUp from lucide, sanitizeHtml + isRealArtistImage + formatTotalDuration from nowPlayingHelpers) plus the inline renderStars helper. Pure code move otherwise.
59 lines
2.2 KiB
TypeScript
59 lines
2.2 KiB
TypeScript
import React, { memo } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { ExternalLink, Play, TrendingUp } from 'lucide-react';
|
|
import type { SubsonicSong } from '../../api/subsonicTypes';
|
|
import { formatTime } from '../../utils/nowPlayingHelpers';
|
|
|
|
interface TopSongsCardProps {
|
|
artistName: string;
|
|
artistId?: string;
|
|
songs: SubsonicSong[];
|
|
currentTrackId: string;
|
|
onNavigate: (path: string) => void;
|
|
onPlay: (song: SubsonicSong) => void;
|
|
}
|
|
|
|
const TopSongsCard = memo(function TopSongsCard({ artistName, artistId, songs, currentTrackId, onNavigate, onPlay }: TopSongsCardProps) {
|
|
const { t } = useTranslation();
|
|
const top = songs.slice(0, 8);
|
|
if (top.length === 0) return null;
|
|
|
|
return (
|
|
<div className="np-info-card np-dash-card">
|
|
<div className="np-card-header">
|
|
<h3 className="np-card-title">
|
|
<TrendingUp size={13} style={{ marginRight: 5, verticalAlign: '-2px' }} />
|
|
{t('nowPlaying.topSongs', { defaultValue: 'Most played by this artist' })}
|
|
</h3>
|
|
{artistId && (
|
|
<button className="np-card-link" onClick={() => onNavigate(`/artist/${artistId}`)}>
|
|
{t('nowPlaying.goToArtist')} <ExternalLink size={12} />
|
|
</button>
|
|
)}
|
|
</div>
|
|
<div className="np-dash-top-list">
|
|
{top.map((s, idx) => {
|
|
const isActive = s.id === currentTrackId;
|
|
return (
|
|
<div key={s.id}
|
|
className={`np-dash-top-row${isActive ? ' active' : ''}`}
|
|
onClick={() => onPlay(s)}
|
|
data-tooltip={t('contextMenu.playNow')}>
|
|
<span className="np-dash-top-rank">{idx + 1}</span>
|
|
<div className="np-dash-top-body">
|
|
<span className="np-dash-top-title truncate">{s.title}</span>
|
|
{s.album && <span className="np-dash-top-sub truncate">{s.album}</span>}
|
|
</div>
|
|
<span className="np-dash-top-dur">{formatTime(s.duration)}</span>
|
|
<Play size={14} className="np-dash-top-play" />
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
<div className="np-dash-top-credit">{t('nowPlaying.topSongsCredit', { name: artistName, defaultValue: 'Top tracks from {{name}}' })}</div>
|
|
</div>
|
|
);
|
|
});
|
|
|
|
export default TopSongsCard;
|