74 lines
2.8 KiB
TypeScript
74 lines
2.8 KiB
TypeScript
import { unwrap } from '@pipu/core';
|
|
import { useQuery } from '@tanstack/react-query';
|
|
import { Link } from '@tanstack/react-router';
|
|
import { useTranslation } from 'react-i18next';
|
|
|
|
import { useSession } from '../app/session';
|
|
import { Cover, TrackList } from '../entities/catalog';
|
|
import { AddToPlaylist } from '../entities/playlists';
|
|
import { usePlayer } from '../player/context';
|
|
import { formatDuration } from '../shared/lib/format';
|
|
import { playIcon, shuffleIcon } from '../shared/ui/icons';
|
|
import { Button } from '../shared/ui/ui';
|
|
import { NotFound } from './NotFound';
|
|
import s from './page.module.css';
|
|
import c from './catalog.module.css';
|
|
|
|
export function AlbumPage({ id }: { id: string }) {
|
|
const { t } = useTranslation();
|
|
const session = useSession();
|
|
const player = usePlayer();
|
|
const album = useQuery({
|
|
queryKey: ['album', id],
|
|
queryFn: async () => unwrap(await session.client.GET('/api/v1/albums/{id}', { params: { path: { id } } })),
|
|
});
|
|
|
|
if (album.isError) return <NotFound error={album.error} />;
|
|
const a = album.data;
|
|
const colors = a?.cover_colors.length === 3 ? a.cover_colors : null;
|
|
const ctx = { type: 'album' as const, id, label: a?.title };
|
|
|
|
return (
|
|
<div className={s.page}>
|
|
<header className={c.hero}>
|
|
{colors && <span className={c.tint} style={{ background: `linear-gradient(180deg, ${colors[0]}66, transparent)` }} aria-hidden />}
|
|
<Cover album={a ?? { id }} size={200} />
|
|
<div className={c.heroBody}>
|
|
<span className={c.kind}>{t('catalog.album')}</span>
|
|
<h1 className={c.heroTitle}>{a?.title ?? '…'}</h1>
|
|
{a && (
|
|
<span className={c.heroMeta}>
|
|
<Link to="/artist/$id" params={{ id: a.artist.id }}>
|
|
{a.artist.name}
|
|
</Link>
|
|
{a.year ? ` · ${a.year}` : ''} · {t('catalog.track_count', { count: a.tracks.length })} · {formatDuration(a.duration_ms)}
|
|
</span>
|
|
)}
|
|
{a && a.tracks.some((tr) => tr.available) && (
|
|
<div className={c.heroActions}>
|
|
<AddToPlaylist trackIds={a.tracks.map(tr => tr.id)} />
|
|
<Button variant="primary" className={c.pill} icon={playIcon} onClick={() => player.playList(a.tracks, 0, ctx)}>
|
|
{t('catalog.play')}
|
|
</Button>
|
|
<Button
|
|
className={c.pill}
|
|
icon={shuffleIcon}
|
|
onClick={() =>
|
|
player.playList(
|
|
[...a.tracks].sort(() => Math.random() - 0.5),
|
|
0,
|
|
ctx,
|
|
)
|
|
}
|
|
>
|
|
{t('collection.shuffle')}
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</header>
|
|
{a && <TrackList tracks={a.tracks} numbered context={ctx} />}
|
|
</div>
|
|
);
|
|
}
|