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.
164 lines
7.4 KiB
TypeScript
164 lines
7.4 KiB
TypeScript
import React, { memo } from 'react';
|
||
import { useTranslation } from 'react-i18next';
|
||
import { Headphones, Heart, MicVocal, Music, Star } from 'lucide-react';
|
||
import type { LastfmArtistStats, LastfmTrackInfo } from '../../api/lastfm';
|
||
import LastfmIcon from '../LastfmIcon';
|
||
import { formatTime } from '../../utils/nowPlayingHelpers';
|
||
|
||
interface HeroProps {
|
||
track: { title: string; artist: string; album: string; year?: number;
|
||
duration: number; suffix?: string; bitRate?: number; samplingRate?: number;
|
||
bitDepth?: number; artistId?: string; albumId?: string; id: string;
|
||
userRating?: number; };
|
||
genre?: string;
|
||
playCount?: number;
|
||
userRatingOverride?: number;
|
||
lfmTrack: LastfmTrackInfo | null;
|
||
lfmArtist: LastfmArtistStats | null;
|
||
starred: boolean;
|
||
lfmLoved: boolean;
|
||
lfmLoveEnabled: boolean;
|
||
activeLyricsTab: boolean;
|
||
coverUrl: string;
|
||
onNavigate: (path: string) => void;
|
||
onToggleStar: () => void;
|
||
onToggleLfmLove: () => void;
|
||
onOpenLyrics: () => void;
|
||
}
|
||
|
||
function renderStars(rating?: number) {
|
||
if (!rating) return null;
|
||
return (
|
||
<div className="np-stars-inline">
|
||
{[1, 2, 3, 4, 5].map(i => (
|
||
<Star key={i} size={13}
|
||
fill={i <= rating ? 'var(--ctp-yellow)' : 'none'}
|
||
color={i <= rating ? 'var(--ctp-yellow)' : 'var(--ctp-overlay1)'}
|
||
/>
|
||
))}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
const Hero = memo(function Hero({ track, genre, playCount, userRatingOverride, lfmTrack, lfmArtist, starred, lfmLoved, lfmLoveEnabled, activeLyricsTab, coverUrl, onNavigate, onToggleStar, onToggleLfmLove, onOpenLyrics }: HeroProps) {
|
||
const { t } = useTranslation();
|
||
const rating = userRatingOverride ?? track.userRating;
|
||
const hiRes = (track.bitDepth && track.bitDepth > 16) || (track.samplingRate && track.samplingRate > 48000);
|
||
const releaseAge = track.year ? new Date().getFullYear() - track.year : 0;
|
||
|
||
return (
|
||
<div className="np-dash-hero">
|
||
<div className="np-dash-hero-cover">
|
||
{coverUrl
|
||
? <img src={coverUrl} alt="" className="np-cover" />
|
||
: <div className="np-cover np-cover-fallback"><Music size={64} /></div>}
|
||
</div>
|
||
<div className="np-dash-hero-body">
|
||
<div className="np-dash-hero-title">{track.title}</div>
|
||
<div className="np-dash-hero-sub">
|
||
<span className="np-link"
|
||
onClick={() => track.artistId && onNavigate(`/artist/${track.artistId}`)}
|
||
style={{ cursor: track.artistId ? 'pointer' : 'default' }}>
|
||
{track.artist}
|
||
</span>
|
||
<span className="np-sep">·</span>
|
||
<span className="np-link"
|
||
onClick={() => track.albumId && onNavigate(`/album/${track.albumId}`)}
|
||
style={{ cursor: track.albumId ? 'pointer' : 'default' }}>
|
||
{track.album}
|
||
</span>
|
||
{track.year && <><span className="np-sep">·</span><span>{track.year}</span></>}
|
||
{releaseAge > 0 && (
|
||
<><span className="np-sep">·</span>
|
||
<span className="np-dash-hero-age">
|
||
{t('nowPlaying.releasedYearsAgo', { count: releaseAge, defaultValue: '{{count}} years ago' })}
|
||
</span></>
|
||
)}
|
||
</div>
|
||
|
||
<div className="np-dash-hero-badges">
|
||
{genre && <span className="np-badge">{genre}</span>}
|
||
{track.suffix && <span className="np-badge">{track.suffix.toUpperCase()}</span>}
|
||
{track.bitRate && <span className="np-badge">{track.bitRate} kbps</span>}
|
||
{track.samplingRate && <span className="np-badge">{(track.samplingRate / 1000).toFixed(1)} kHz</span>}
|
||
{track.bitDepth && <span className="np-badge">{track.bitDepth}-bit</span>}
|
||
{hiRes && <span className="np-badge np-badge-hires">Hi-Res</span>}
|
||
{track.duration > 0 && <span className="np-badge">{formatTime(track.duration)}</span>}
|
||
</div>
|
||
|
||
<div className="np-dash-hero-actions">
|
||
<button onClick={onToggleStar} className="np-dash-icon-btn"
|
||
data-tooltip={starred ? t('contextMenu.unfavorite') : t('contextMenu.favorite')}>
|
||
<Heart size={18} fill={starred ? 'var(--ctp-yellow)' : 'none'} color={starred ? 'var(--ctp-yellow)' : 'currentColor'} />
|
||
</button>
|
||
{lfmLoveEnabled && (
|
||
<button onClick={onToggleLfmLove}
|
||
className={`np-dash-icon-btn np-dash-lfm-btn${lfmLoved ? ' is-loved' : ''}`}
|
||
data-tooltip={lfmLoved ? t('contextMenu.lfmUnlove') : t('contextMenu.lfmLove')}>
|
||
<LastfmIcon size={18} />
|
||
</button>
|
||
)}
|
||
<button className="np-dash-icon-btn"
|
||
onClick={onOpenLyrics}
|
||
data-tooltip={t('player.lyrics')}
|
||
style={{ color: activeLyricsTab ? 'var(--accent)' : undefined }}>
|
||
<MicVocal size={18} />
|
||
</button>
|
||
{rating && renderStars(rating)}
|
||
</div>
|
||
|
||
{(playCount != null && playCount > 0) && (
|
||
<div className="np-dash-hero-stat">
|
||
<Headphones size={13} />
|
||
<span>{t('nowPlaying.playsCount', { count: playCount, defaultValue: '{{count}} plays' })}</span>
|
||
</div>
|
||
)}
|
||
|
||
{(lfmTrack || lfmArtist) && (
|
||
<div className="np-dash-hero-lfm">
|
||
<div className="np-dash-hero-lfm-heading">
|
||
<span className="np-dash-hero-lfm-badge">Last.fm</span>
|
||
</div>
|
||
{lfmTrack && (
|
||
<div className="np-dash-hero-lfm-row">
|
||
<span className="np-dash-hero-lfm-scope">{t('nowPlaying.thisTrack', 'This track')}</span>
|
||
<span className="np-dash-hero-lfm-sep">—</span>
|
||
<span>{t('nowPlaying.listenersN', { n: lfmTrack.listeners.toLocaleString(), defaultValue: '{{n}} listeners' })}</span>
|
||
<span className="np-dash-hero-lfm-dot">·</span>
|
||
<span>{t('nowPlaying.scrobblesN', { n: lfmTrack.playcount.toLocaleString(), defaultValue: '{{n}} scrobbles' })}</span>
|
||
{lfmTrack.userPlaycount != null && (
|
||
<>
|
||
<span className="np-dash-hero-lfm-dot">·</span>
|
||
<span className="np-dash-hero-lfm-you">
|
||
{t('nowPlaying.playsByYouN', { n: lfmTrack.userPlaycount.toLocaleString(), defaultValue: 'played {{n}}× by you' })}
|
||
</span>
|
||
</>
|
||
)}
|
||
</div>
|
||
)}
|
||
{lfmArtist && (
|
||
<div className="np-dash-hero-lfm-row">
|
||
<span className="np-dash-hero-lfm-scope">{t('nowPlaying.thisArtist', 'This artist')}</span>
|
||
<span className="np-dash-hero-lfm-sep">—</span>
|
||
<span>{t('nowPlaying.listenersN', { n: lfmArtist.listeners.toLocaleString(), defaultValue: '{{n}} listeners' })}</span>
|
||
<span className="np-dash-hero-lfm-dot">·</span>
|
||
<span>{t('nowPlaying.scrobblesN', { n: lfmArtist.playcount.toLocaleString(), defaultValue: '{{n}} scrobbles' })}</span>
|
||
{lfmArtist.userPlaycount != null && (
|
||
<>
|
||
<span className="np-dash-hero-lfm-dot">·</span>
|
||
<span className="np-dash-hero-lfm-you">
|
||
{t('nowPlaying.playsByYouN', { n: lfmArtist.userPlaycount.toLocaleString(), defaultValue: 'played {{n}}× by you' })}
|
||
</span>
|
||
</>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
});
|
||
|
||
export default Hero;
|