mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 14:35:41 +00:00
refactor(now-playing): G.72 — extract Hero + ArtistCard + AlbumCard + TopSongsCard + CreditsCard (cluster) (#639)
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.
This commit is contained in:
committed by
GitHub
parent
1c34cc04c7
commit
2ddc2a2345
@@ -0,0 +1,98 @@
|
||||
import React, { memo, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ExternalLink } from 'lucide-react';
|
||||
import type { SubsonicArtistInfo } from '../../api/subsonicTypes';
|
||||
import { isRealArtistImage, sanitizeHtml } from '../../utils/nowPlayingHelpers';
|
||||
import CachedImage from '../CachedImage';
|
||||
|
||||
interface ArtistCardProps {
|
||||
artistName: string;
|
||||
artistId?: string;
|
||||
artistInfo: SubsonicArtistInfo | null;
|
||||
onNavigate: (path: string) => void;
|
||||
}
|
||||
|
||||
const ArtistCard = memo(function ArtistCard({ artistName, artistId, artistInfo, onNavigate }: ArtistCardProps) {
|
||||
const { t } = useTranslation();
|
||||
const [bioExpanded, setBioExpanded] = useState(false);
|
||||
const [bioOverflows, setBioOverflows] = useState(false);
|
||||
const bioRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useEffect(() => { setBioExpanded(false); }, [artistId]);
|
||||
|
||||
const bioHtml = useMemo(() => artistInfo?.biography ? sanitizeHtml(artistInfo.biography) : '', [artistInfo?.biography]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const el = bioRef.current;
|
||||
if (!el) { setBioOverflows(false); return; }
|
||||
setBioOverflows(el.scrollHeight - el.clientHeight > 1);
|
||||
}, [bioHtml]);
|
||||
|
||||
const similar = artistInfo?.similarArtist ?? [];
|
||||
const rawLarge = artistInfo?.largeImageUrl;
|
||||
const rawMed = artistInfo?.mediumImageUrl;
|
||||
const heroImage = isRealArtistImage(rawLarge)
|
||||
? rawLarge!
|
||||
: isRealArtistImage(rawMed) ? rawMed! : '';
|
||||
const heroCacheKey = artistId ? `artistInfo:${artistId}:hero` : '';
|
||||
|
||||
if (!bioHtml && similar.length === 0 && !heroImage) return null;
|
||||
|
||||
return (
|
||||
<div className="np-info-card np-dash-card">
|
||||
<div className="np-card-header">
|
||||
<h3 className="np-card-title">{t('nowPlaying.aboutArtist')}</h3>
|
||||
{artistId && (
|
||||
<button className="np-card-link" onClick={() => onNavigate(`/artist/${artistId}`)}>
|
||||
{t('nowPlaying.goToArtist')} <ExternalLink size={12} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="np-dash-artist-body">
|
||||
{heroImage && heroCacheKey && (
|
||||
<CachedImage
|
||||
src={heroImage}
|
||||
cacheKey={heroCacheKey}
|
||||
alt={artistName}
|
||||
className="np-dash-artist-image"
|
||||
onError={e => { (e.currentTarget as HTMLImageElement).style.display = 'none'; }}
|
||||
/>
|
||||
)}
|
||||
<div className="np-dash-artist-text">
|
||||
<div className="np-dash-artist-name">{artistName}</div>
|
||||
{bioHtml && (
|
||||
<>
|
||||
<div
|
||||
ref={bioRef}
|
||||
className={`np-bio-text${bioExpanded ? ' expanded' : ''}`}
|
||||
dangerouslySetInnerHTML={{ __html: bioHtml }}
|
||||
/>
|
||||
{(bioOverflows || bioExpanded) && (
|
||||
<button className="np-bio-toggle" onClick={() => setBioExpanded(v => !v)}>
|
||||
{bioExpanded ? t('nowPlayingInfo.bioReadLess', 'Show less') : t('nowPlayingInfo.bioReadMore', 'Read more')}
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{similar.length > 0 && (
|
||||
<div className="np-dash-similar">
|
||||
<div className="np-dash-chip-row">
|
||||
{similar.slice(0, 12).map(a => (
|
||||
<span key={a.id} className="np-chip"
|
||||
onClick={() => a.id && onNavigate(`/artist/${a.id}`)}
|
||||
data-tooltip={t('nowPlaying.goToArtist')}>
|
||||
{a.name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default ArtistCard;
|
||||
Reference in New Issue
Block a user