165 lines
4.5 KiB
TypeScript
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>
|
|
);
|
|
}
|