Files
mcma-webui/src/components/track/TrackRow.tsx
T
Senko-san f5767ff55e
Docker Build & Publish / build (push) Has been cancelled
Docker Build & Publish / push (push) Has been cancelled
Docker Build & Publish / Prune old image versions (push) Has been cancelled
feat(track): show now-playing bars overlay on cover art
Overlay the accent-coloured "hopping bars" PlayingIndicator on a
track's cover art wherever TrackRow appears when it's the active
player track, and reuse the same component/overlay for the current
entry's cover in the queue panel.
2026-06-13 17:49:06 +03:00

164 lines
4.7 KiB
TypeScript

import { Row } from '@olly/modern-sk';
import { useTranslation } from 'react-i18next';
import { TrackContextMenu } from './TrackContextMenu';
import { AvailabilityBadge } from './AvailabilityBadge';
import { MetadataStatusBadge } from './MetadataStatusBadge';
import { Icon } from '../common/Icon';
import { PlayingIndicator } from '../common/PlayingIndicator';
import { formatDuration } from '../../lib/format';
import { useAppDispatch, useAppSelector } from '../../hooks/useAppDispatch';
import { playNow } from '../../store/slices/queue';
import type { Track } from '../../api/types';
import { getCoverUrl, getTrackCoverUrl } from '../../api/endpoints/streaming';
interface Props {
track: Track;
index?: number;
showAlbum?: boolean;
onAddToPlaylist?: (track: Track) => void;
onEditMetadata?: (track: Track) => void;
onDelete?: (track: Track) => void;
}
export function TrackRow({
track,
index,
showAlbum = false,
onAddToPlaylist,
onEditMetadata,
onDelete,
}: Props) {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const currentTrackId = useAppSelector((s) => s.player.currentTrackId);
const isPlaying = useAppSelector((s) => s.player.isPlaying);
const token = useAppSelector((s) => s.auth.accessToken);
const isActive = currentTrackId === track.id;
// Prefer an explicit album art URL; otherwise serve the track's own cover
// (needs the token in the query string — `<img>` can't send a header).
const artUrl =
getCoverUrl(track.albumArtUrl) ??
(token ? getTrackCoverUrl(track.id, token, track.hasCover) : undefined);
const handlePlayNow = () => {
dispatch(
playNow({
trackId: track.id,
title: track.title,
artistName: track.artistName,
albumTitle: track.albumTitle,
durationMs: track.durationMs,
albumArtUrl: track.albumArtUrl,
}),
);
};
return (
<Row
selected={isActive}
style={{
display: 'grid',
gridTemplateColumns: '2rem 2.5rem 1fr auto auto',
gap: '0.75rem',
alignItems: 'center',
padding: '0.375rem 0.75rem',
cursor: 'default',
}}
>
<span
style={{
fontSize: '0.75rem',
color: 'var(--color-text-3)',
textAlign: 'right',
}}
>
{isActive && isPlaying ? '▶' : index !== undefined ? index + 1 : ''}
</span>
<div className="track-art">
{artUrl ? (
<img
src={artUrl}
alt=""
width={36}
height={36}
style={{ borderRadius: 4, objectFit: 'cover' }}
/>
) : (
<div
style={{
width: 36,
height: 36,
borderRadius: 4,
background: 'var(--color-surface-3)',
}}
/>
)}
{isActive && (
<div className="cover-playing">
<PlayingIndicator animate={isPlaying} />
</div>
)}
<button
type="button"
className="track-art-play"
onClick={handlePlayNow}
aria-label={t('track.menu.playNow')}
title={t('track.menu.playNow')}
>
<Icon name="play" fill />
</button>
</div>
<div style={{ minWidth: 0 }}>
<div
style={{
fontWeight: isActive ? 600 : 400,
color: isActive ? 'var(--color-accent)' : 'var(--color-text-1)',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{track.title}
</div>
<div
style={{
fontSize: '0.8125rem',
color: 'var(--color-text-2)',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{track.artistName}
{showAlbum && ` · ${track.albumTitle}`}
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<MetadataStatusBadge
status={track.metadataStatus}
error={track.metadataError}
/>
<AvailabilityBadge availability={track.availability} />
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<span
style={{
fontSize: '0.8125rem',
color: 'var(--color-text-3)',
minWidth: '3rem',
textAlign: 'right',
}}
>
{formatDuration(track.durationMs)}
</span>
<TrackContextMenu
track={track}
onAddToPlaylist={onAddToPlaylist}
onEditMetadata={onEditMetadata}
onDelete={onDelete}
/>
</div>
</Row>
);
}