109 lines
4.4 KiB
TypeScript
109 lines
4.4 KiB
TypeScript
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||
import { Heart, Play, Plus } from "lucide-react";
|
||
import { useState } from "react";
|
||
import { Link, useParams } from "react-router-dom";
|
||
import { tracksApi } from "../lib/api";
|
||
import { formatDate, formatDuration } from "../lib/format";
|
||
import { usePlayer } from "../store/player";
|
||
import { AddToPlaylistModal } from "../components/AddToPlaylistModal";
|
||
import { ArtistLinks } from "../components/ArtistLinks";
|
||
import { Comments } from "../components/Comments";
|
||
import { Cover } from "../components/Cover";
|
||
import { LyricsView } from "../components/LyricsView";
|
||
import { ShareButton } from "../components/ShareButton";
|
||
|
||
export function TrackPage() {
|
||
const { id = "" } = useParams();
|
||
const playNow = usePlayer((s) => s.playNow);
|
||
const addToQueue = usePlayer((s) => s.addToQueue);
|
||
const qc = useQueryClient();
|
||
const [addOpen, setAddOpen] = useState(false);
|
||
|
||
const { data, isLoading } = useQuery({
|
||
queryKey: ["track", id],
|
||
queryFn: () => tracksApi.get(id),
|
||
enabled: Boolean(id),
|
||
});
|
||
|
||
const like = useMutation({
|
||
mutationFn: () => (data?.liked ? tracksApi.unlike(id) : tracksApi.like(id)),
|
||
onSuccess: () => {
|
||
void qc.invalidateQueries({ queryKey: ["track", id] });
|
||
void qc.invalidateQueries({ queryKey: ["library"] });
|
||
},
|
||
});
|
||
|
||
if (isLoading) return <p className="text-sm text-zinc-500">Загрузка...</p>;
|
||
if (!data) return <p className="text-sm text-zinc-500">Трек не найден.</p>;
|
||
const { track } = data;
|
||
|
||
return (
|
||
<div className="mx-auto max-w-5xl animate-fade-in">
|
||
<div className="card mb-6 flex flex-col gap-5 p-5 sm:flex-row sm:items-end">
|
||
<Cover src={track.coverUrl} name={track.title} className="h-40 w-40 shrink-0" rounded="rounded-2xl" />
|
||
<div className="min-w-0 flex-1">
|
||
<span className="chip">Трек</span>
|
||
<h1 className="mt-2 truncate text-3xl font-bold tracking-tight text-white">{track.title}</h1>
|
||
<p className="mt-1 text-sm text-zinc-400">
|
||
<ArtistLinks artists={track.artists} fallback={track.artist?.name} linkClassName="link" />
|
||
{track.album && (
|
||
<>
|
||
{" · "}
|
||
<Link to={`/album/${track.album.id}`} className="link">
|
||
{track.album.title}
|
||
</Link>
|
||
</>
|
||
)}
|
||
</p>
|
||
<p className="mt-1 text-sm text-zinc-500">
|
||
{formatDuration(track.durationMs)}
|
||
{track.releaseDate && ` · ${formatDate(track.releaseDate)}`}
|
||
{` · ${track.playCount} прослушиваний`}
|
||
</p>
|
||
<p className="mt-2 text-xs text-zinc-500">
|
||
Добавил в библиотеку:{" "}
|
||
{track.uploadedBy ? (
|
||
<Link to={`/u/${track.uploadedBy.username}`} className="link">
|
||
{track.uploadedBy.displayName || track.uploadedBy.username}
|
||
</Link>
|
||
) : (
|
||
"неизвестно"
|
||
)}
|
||
</p>
|
||
|
||
<div className="mt-4 flex flex-wrap gap-2">
|
||
<button className="btn-primary" onClick={() => playNow(track, [track])}>
|
||
<Play size={15} /> Играть
|
||
</button>
|
||
<button className="btn-ghost" onClick={() => addToQueue(track)}>
|
||
<Plus size={15} /> В очередь
|
||
</button>
|
||
<button className={data.liked ? "btn-danger" : "btn-ghost"} onClick={() => like.mutate()}>
|
||
<Heart size={15} fill={data.liked ? "currentColor" : "none"} />
|
||
{data.liked ? "В избранном" : "В избранное"}
|
||
</button>
|
||
<button className="btn-ghost" onClick={() => setAddOpen(true)}>
|
||
В плейлист
|
||
</button>
|
||
<a className="btn-ghost" href={`/api/tracks/${id}/stream?download=1`}>
|
||
Скачать
|
||
</a>
|
||
<ShareButton path={`/track/${id}`} />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid gap-6 lg:grid-cols-2">
|
||
<section className="card max-h-[60vh] overflow-hidden p-2">
|
||
<h2 className="section-title px-3 pb-2 pt-3">Текст песни</h2>
|
||
<LyricsView trackId={id} />
|
||
</section>
|
||
|
||
<Comments trackId={id} />
|
||
</div>
|
||
|
||
<AddToPlaylistModal track={track} open={addOpen} onClose={() => setAddOpen(false)} />
|
||
</div>
|
||
);
|
||
}
|