Files
mcma-webui/src/features/playlist-detail/PlaylistDetailPage.tsx
T
2026-06-06 15:23:07 +03:00

165 lines
4.5 KiB
TypeScript

import { useParams, useNavigate } from 'react-router';
import { useTranslation } from 'react-i18next';
import { ScrollArea, IconButton, Button } from '@olly/modern-sk';
import {
useGetPlaylistQuery,
useGetPlaylistTracksQuery,
} from '../../api/endpoints/playlists';
import { TrackRow } from '../../components/track/TrackRow';
import { LoadingSkeleton } from '../../components/common/LoadingSkeleton';
import { ErrorState } from '../../components/common/ErrorState';
import { EmptyState } from '../../components/common/EmptyState';
import { useAppDispatch } from '../../hooks/useAppDispatch';
import { setQueue } from '../../store/slices/queue';
import { formatDuration } from '../../lib/format';
export function PlaylistDetailPage() {
const { t } = useTranslation();
const { playlistId } = useParams<{ playlistId: string }>();
const navigate = useNavigate();
const dispatch = useAppDispatch();
const playlistQuery = useGetPlaylistQuery(playlistId ?? '', {
skip: !playlistId,
});
const tracksQuery = useGetPlaylistTracksQuery(playlistId ?? '', {
skip: !playlistId,
});
if (playlistQuery.isLoading) {
return (
<div style={{ padding: '1.5rem' }}>
<LoadingSkeleton rows={10} />
</div>
);
}
if (playlistQuery.isError) {
return (
<ErrorState
message={t('playlist.error')}
onRetry={() => playlistQuery.refetch()}
/>
);
}
const playlist = playlistQuery.data;
const tracks = tracksQuery.data ?? [];
const handlePlayAll = () => {
if (!tracks.length || !playlist) return;
dispatch(
setQueue({
entries: tracks.map((t) => ({
trackId: t.id,
title: t.title,
artistName: t.artistName,
albumTitle: t.albumTitle,
durationMs: t.durationMs,
albumArtUrl: t.albumArtUrl,
})),
source: 'playlist',
sourceId: playlist.id,
sourceName: playlist.name,
}),
);
};
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<div
style={{
padding: '1.25rem 1.5rem',
borderBottom: '1px solid var(--color-border)',
display: 'flex',
alignItems: 'center',
gap: '1rem',
flexShrink: 0,
}}
>
<IconButton
variant="ghost"
size="sm"
onClick={() => navigate(-1)}
aria-label={t('common.back')}
>
</IconButton>
<div style={{ flex: 1 }}>
<p
style={{
margin: 0,
fontSize: '0.75rem',
color: 'var(--color-text-3)',
textTransform: 'uppercase',
letterSpacing: '0.05em',
}}
>
{t('playlist.type')}
</p>
<h1
style={{ margin: '0.25rem 0', fontSize: '1.5rem', fontWeight: 700 }}
>
{playlist?.name}
</h1>
{playlist?.description && (
<p
style={{
margin: 0,
color: 'var(--color-text-2)',
fontSize: '0.875rem',
}}
>
{playlist.description}
</p>
)}
<p
style={{
margin: '0.25rem 0 0',
color: 'var(--color-text-3)',
fontSize: '0.8125rem',
}}
>
{playlist &&
`${playlist.trackCount} · ${formatDuration(playlist.totalDurationMs)}`}
</p>
</div>
<Button
variant="primary"
onClick={handlePlayAll}
disabled={!tracks.length}
>
{t('playlist.play')}
</Button>
</div>
<ScrollArea style={{ flex: 1 }}>
{tracksQuery.isLoading && <LoadingSkeleton rows={10} />}
{tracksQuery.isError && (
<ErrorState
message={t('playlist.tracksError')}
onRetry={() => tracksQuery.refetch()}
/>
)}
{!tracksQuery.isLoading &&
!tracksQuery.isError &&
tracks.length === 0 && (
<EmptyState
icon="♫"
title={t('playlist.empty.title')}
description={t('playlist.empty.description')}
/>
)}
{tracks.map((track, i) => (
<TrackRow
key={`${track.id}-${i}`}
track={track}
index={i}
showAlbum
/>
))}
</ScrollArea>
</div>
);
}