mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 14:35:41 +00:00
refactor(player-bar): H7 — split PlayerBar.tsx 802 → 354 LOC across 10 files (#670)
* refactor(player-bar): H7 — extract PlaybackTime + RemainingTime + formatTime The two memoized clock components (which update the DOM imperatively from the playbackProgress store without re-rendering PlayerBar) move into components/playerBar/PlaybackClock.tsx. formatTime helper → utils/playerBarHelpers.ts. PlayerBar.tsx: 802 → 765 LOC. * refactor(player-bar): H7 — extract PlayerTrackInfo The cover-art wrap + title/artist marquees + star + last.fm love buttons move into their own component. The new file uses PlayerState['openContextMenu'] for prop typing so the union literal type carries through. PlayerBar.tsx: 765 → 674 LOC. * refactor(player-bar): H7 — extract PlayerTransportControls Stop/Prev/Play/Next/Repeat buttons (with the preview-ring + schedule-badge overlays around play/pause) move into PlayerTransportControls.tsx. The component uses ReturnType<...> on the source hooks to derive its playPauseBind + scheduleRemaining prop types so the new file stays in lockstep with usePlaybackDelayPress + usePlaybackScheduleRemaining. PlayerBar.tsx: 674 → 621 LOC. * refactor(player-bar): H7 — extract PlayerSeekbarSection The waveform / radio progress / time-label block moves into its own component. PlayerSeekbarSection branches on isRadio (AzuraCast progress bar with elapsed+duration when available; LIVE badge otherwise) vs. regular track (WaveformSeek or perf-flag fallback + duration ↔ remaining toggle). PlayerBar.tsx: 621 → 582 LOC. * refactor(player-bar): H7 — extract PlayerVolume + PlayerOverflowMenu + 2 hooks PlayerVolume.tsx is the reusable volume button + slider combo, used in three layouts (inline, full menu, volume-only menu) — `inputId` / `sectionModifier` / `wrapModifier` props handle the variants without class duplication. PlayerOverflowMenu.tsx is the portaled Ellipsis-button menu that hosts EQ / mini-player buttons + a PlayerVolume instance. useFloatingPlayerBar owns the docked/floating layout computation (ResizeObserver on sidebar + queue panel). useUtilityOverflowMenu owns the overflow detection, menu open/mode state, close-on-outside-click / Escape, position recompute on resize/scroll, and the wheel-menu timer. PlayerBar.tsx: 582 → 354 LOC.
This commit is contained in:
committed by
GitHub
parent
383bbbd75f
commit
988806e6b1
@@ -0,0 +1,167 @@
|
||||
import { Cast, Heart, Maximize2, Music } from 'lucide-react';
|
||||
import type { TFunction } from 'i18next';
|
||||
import { setRating } from '../../api/subsonicStarRating';
|
||||
import type { InternetRadioStation, SubsonicAlbum } from '../../api/subsonicTypes';
|
||||
import type { PlayerState, Track } from '../../store/playerStoreTypes';
|
||||
import type { RadioMetadata } from '../../hooks/useRadioMetadata';
|
||||
import type { PreviewingTrack } from '../../store/previewStore';
|
||||
import CachedImage from '../CachedImage';
|
||||
import LastfmIcon from '../LastfmIcon';
|
||||
import MarqueeText from '../MarqueeText';
|
||||
import StarRating from '../StarRating';
|
||||
|
||||
interface Props {
|
||||
currentTrack: Track | null;
|
||||
currentRadio: InternetRadioStation | null;
|
||||
isRadio: boolean;
|
||||
radioMeta: RadioMetadata;
|
||||
radioCoverSrc: string;
|
||||
radioCoverKey: string;
|
||||
coverSrc: string;
|
||||
coverKey: string;
|
||||
displayCoverArt: string | undefined;
|
||||
displayTitle: string;
|
||||
displayArtist: string;
|
||||
showPreviewMeta: boolean;
|
||||
previewingTrack: PreviewingTrack | null;
|
||||
isStarred: boolean;
|
||||
toggleStar: () => void;
|
||||
lastfmSessionKey: string | null;
|
||||
lastfmLoved: boolean;
|
||||
toggleLastfmLove: () => void;
|
||||
userRatingOverrides: Record<string, number>;
|
||||
setUserRatingOverride: (id: string, r: number) => void;
|
||||
toggleFullscreen: () => void;
|
||||
navigate: (to: string) => void;
|
||||
openContextMenu: PlayerState['openContextMenu'];
|
||||
t: TFunction;
|
||||
}
|
||||
|
||||
export function PlayerTrackInfo({
|
||||
currentTrack, currentRadio, isRadio, radioMeta, radioCoverSrc, radioCoverKey,
|
||||
coverSrc, coverKey, displayCoverArt, displayTitle, displayArtist,
|
||||
showPreviewMeta, previewingTrack, isStarred, toggleStar,
|
||||
lastfmSessionKey, lastfmLoved, toggleLastfmLove,
|
||||
userRatingOverrides, setUserRatingOverride, toggleFullscreen,
|
||||
navigate, openContextMenu, t,
|
||||
}: Props) {
|
||||
return (
|
||||
<div className="player-track-info">
|
||||
<div
|
||||
className={`player-album-art-wrap ${currentTrack && !isRadio && !showPreviewMeta ? 'clickable' : ''}`}
|
||||
onClick={() => !isRadio && !showPreviewMeta && currentTrack && toggleFullscreen()}
|
||||
data-tooltip={!isRadio && !showPreviewMeta && currentTrack ? t('player.openFullscreen') : undefined}
|
||||
>
|
||||
{isRadio ? (
|
||||
currentRadio?.coverArt ? (
|
||||
<CachedImage
|
||||
className="player-album-art"
|
||||
src={radioCoverSrc}
|
||||
cacheKey={radioCoverKey}
|
||||
alt={currentRadio.name}
|
||||
/>
|
||||
) : (
|
||||
<div className="player-album-art-placeholder">
|
||||
<Cast size={20} />
|
||||
</div>
|
||||
)
|
||||
) : displayCoverArt ? (
|
||||
<CachedImage
|
||||
className="player-album-art"
|
||||
src={coverSrc}
|
||||
cacheKey={coverKey}
|
||||
alt={showPreviewMeta ? `${previewingTrack!.title} Cover` : `${currentTrack?.album ?? ''} Cover`}
|
||||
/>
|
||||
) : (
|
||||
<div className="player-album-art-placeholder">
|
||||
<Music size={22} />
|
||||
</div>
|
||||
)}
|
||||
{currentTrack && !isRadio && !showPreviewMeta && (
|
||||
<div className="player-art-expand-hint" aria-hidden="true">
|
||||
<Maximize2 size={16} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="player-track-meta">
|
||||
{showPreviewMeta && (
|
||||
<span className="player-preview-label" aria-label={t('player.previewActive')}>
|
||||
{t('player.previewLabel')}
|
||||
</span>
|
||||
)}
|
||||
<MarqueeText
|
||||
text={isRadio
|
||||
? (radioMeta.currentTitle
|
||||
? (radioMeta.currentArtist
|
||||
? `${radioMeta.currentArtist} — ${radioMeta.currentTitle}`
|
||||
: radioMeta.currentTitle)
|
||||
: (currentRadio?.name ?? '—'))
|
||||
: displayTitle}
|
||||
className="player-track-name"
|
||||
style={{ cursor: !isRadio && !showPreviewMeta && currentTrack?.albumId ? 'pointer' : 'default' }}
|
||||
onClick={() => !isRadio && !showPreviewMeta && currentTrack?.albumId && navigate(`/album/${currentTrack.albumId}`)}
|
||||
onContextMenu={!isRadio && !showPreviewMeta && currentTrack?.albumId
|
||||
? (e) => {
|
||||
e.preventDefault();
|
||||
const album: SubsonicAlbum = {
|
||||
id: currentTrack.albumId!,
|
||||
name: currentTrack.album,
|
||||
artist: currentTrack.artist,
|
||||
artistId: currentTrack.artistId ?? '',
|
||||
coverArt: currentTrack.coverArt,
|
||||
songCount: 0,
|
||||
duration: 0,
|
||||
};
|
||||
openContextMenu(e.clientX, e.clientY, album, 'album');
|
||||
}
|
||||
: undefined}
|
||||
/>
|
||||
<MarqueeText
|
||||
text={isRadio
|
||||
? (radioMeta.currentTitle && currentRadio?.name
|
||||
? currentRadio.name
|
||||
: t('radio.liveStream'))
|
||||
: displayArtist}
|
||||
className="player-track-artist"
|
||||
style={{ cursor: !isRadio && !showPreviewMeta && currentTrack?.artistId ? 'pointer' : 'default' }}
|
||||
onClick={() => !isRadio && !showPreviewMeta && currentTrack?.artistId && navigate(`/artist/${currentTrack.artistId}`)}
|
||||
/>
|
||||
{currentTrack && !isRadio && !showPreviewMeta && (
|
||||
<StarRating
|
||||
value={userRatingOverrides[currentTrack.id] ?? currentTrack.userRating ?? 0}
|
||||
onChange={r => { setUserRatingOverride(currentTrack.id, r); setRating(currentTrack.id, r).catch(() => {}); }}
|
||||
className="player-track-rating"
|
||||
ariaLabel={t('albumDetail.ratingLabel')}
|
||||
/>
|
||||
)}
|
||||
{isRadio && radioMeta.listeners != null && (
|
||||
<span className="player-radio-listeners">
|
||||
{t('radio.listenerCount', { count: radioMeta.listeners })}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{currentTrack && !isRadio && (
|
||||
<button
|
||||
className={`player-btn player-btn-sm player-star-btn${isStarred ? ' is-starred' : ''}`}
|
||||
onClick={toggleStar}
|
||||
aria-label={isStarred ? t('contextMenu.unfavorite') : t('contextMenu.favorite')}
|
||||
data-tooltip={isStarred ? t('contextMenu.unfavorite') : t('contextMenu.favorite')}
|
||||
style={{ flexShrink: 0 }}
|
||||
>
|
||||
<Heart size={15} fill={isStarred ? 'currentColor' : 'none'} />
|
||||
</button>
|
||||
)}
|
||||
{currentTrack && !isRadio && lastfmSessionKey && (
|
||||
<button
|
||||
className="player-btn player-btn-sm player-love-btn"
|
||||
onClick={toggleLastfmLove}
|
||||
aria-label={lastfmLoved ? t('contextMenu.lfmUnlove') : t('contextMenu.lfmLove')}
|
||||
data-tooltip={lastfmLoved ? t('contextMenu.lfmUnlove') : t('contextMenu.lfmLove')}
|
||||
style={{ color: lastfmLoved ? '#e31c23' : 'var(--text-muted)', flexShrink: 0 }}
|
||||
>
|
||||
<LastfmIcon size={15} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user