feat: enhance discovery, vibe sessions, and library enrichment
Typecheck / typecheck (backend) (push) Has been cancelled
Typecheck / typecheck (workers) (push) Has been cancelled

This commit is contained in:
kami
2026-08-01 14:40:48 +04:00
parent a0c9f42a89
commit 4c48d11e9d
54 changed files with 4136 additions and 521 deletions
+33 -14
View File
@@ -3,7 +3,7 @@ import { Link, useRouter } from '@tanstack/react-router';
import type { Track } from '../types';
import { usePlaybackStore } from '../store/usePlaybackStore';
import { useDislikeTrack } from '../hooks/useDislikeTrack';
import { vibeService } from '../services/vibeService';
import { startVibeSession } from '../services/vibeSession';
import { Artwork } from './Artwork';
import { ArtistLinks } from './ArtistLinks';
@@ -24,9 +24,11 @@ interface TrackRowProps {
variant?: TrackRowVariant;
/** Show a "Vibe by track" button that starts a vibe session seeded from this track. */
showVibe?: boolean;
/** Override ordinary queue playback, for contextual actions such as Vibe seed rows. */
onSelect?: (track: Track) => void;
}
export function TrackRow({ track, queue, showActions = true, variant = 'default', showVibe = false }: TrackRowProps) {
export function TrackRow({ track, queue, showActions = true, variant = 'default', showVibe = false, onSelect }: TrackRowProps) {
const { setQueue, playTrack, play, pause, currentTrack, isPlaying } = usePlaybackStore();
const dislikeTrack = useDislikeTrack();
const router = useRouter();
@@ -34,6 +36,10 @@ export function TrackRow({ track, queue, showActions = true, variant = 'default'
const compact = variant === 'compact';
const handlePlay = () => {
if (onSelect) {
onSelect(track);
return;
}
if (isCurrent) { isPlaying ? pause() : play(); return; }
// Queue the whole list and start at this track, so Previous can walk back
// into the tracks before it.
@@ -41,6 +47,10 @@ export function TrackRow({ track, queue, showActions = true, variant = 'default'
playTrack(track);
};
const playLabel = isCurrent && isPlaying
? `Pause ${track.title || 'track'}`
: `Play ${track.title || 'track'}`;
const handleDislike = (e: React.MouseEvent) => {
e.stopPropagation();
dislikeTrack(track.id);
@@ -48,8 +58,8 @@ export function TrackRow({ track, queue, showActions = true, variant = 'default'
const handleVibe = (e: React.MouseEvent) => {
e.stopPropagation();
// Start a vibe session then navigate to the vibe page.
vibeService.start(track.id).then(() => {
// Start and hydrate the V2 plan before showing the Vibe page.
startVibeSession(track).then(() => {
router.navigate({ to: '/vibe' });
}).catch(() => {
// Session failed — still navigate so the user can try manually.
@@ -59,8 +69,7 @@ export function TrackRow({ track, queue, showActions = true, variant = 'default'
return (
<div
onClick={handlePlay}
className={`group flex w-full cursor-pointer items-center gap-3 rounded-lg border transition-colors ${
className={`group flex w-full items-center gap-3 rounded-lg border transition-colors ${
compact ? 'p-2' : 'p-2.5'
} ${
isCurrent
@@ -69,22 +78,31 @@ export function TrackRow({ track, queue, showActions = true, variant = 'default'
}`}
>
{/* Artwork + play overlay */}
<div className={`relative flex flex-none items-center justify-center rounded overflow-hidden ${
<button
type="button"
onClick={handlePlay}
aria-label={playLabel}
className={`relative flex flex-none items-center justify-center rounded overflow-hidden focus-visible:z-30 ${
compact ? 'h-9 w-9' : 'h-10 w-10'
}`}>
<Artwork seed={`${track.title} ${track.artist}`} src={track.artwork_id} className="absolute inset-0 w-full h-full" />
{isCurrent && isPlaying ? (
<Pause size={compact ? 14 : 18} className="absolute z-20 text-text opacity-100" />
) : (
<Play size={compact ? 14 : 18} className="absolute z-20 text-text opacity-0 group-hover:opacity-100" />
<Play size={compact ? 14 : 18} className="absolute z-20 text-text opacity-70 transition-opacity group-hover:opacity-100" />
)}
</div>
</button>
{/* Title + artist */}
<div className="min-w-0 flex-1">
<div className={`truncate font-medium ${isCurrent ? 'text-accent' : 'text-text'} ${compact ? 'text-sm' : 'text-sm'}`}>
<button
type="button"
onClick={handlePlay}
className={`block max-w-full truncate rounded text-left font-medium hover:underline ${isCurrent ? 'text-accent' : 'text-text'} ${compact ? 'text-sm' : 'text-sm'}`}
aria-label={playLabel}
>
{track.title || 'Untitled'}
</div>
</button>
<ArtistLinks
artists={track.artists}
fallback={track.artist}
@@ -95,9 +113,9 @@ export function TrackRow({ track, queue, showActions = true, variant = 'default'
{/* Actions (vibe → album link → dislike) */}
{showActions && !compact && (
<div className="flex flex-none items-center gap-1 opacity-0 transition-opacity group-hover:opacity-100">
<div className="track-row-actions flex flex-none items-center gap-1">
{showVibe && (
<button onClick={handleVibe} title="Vibe by track" className="rounded p-1.5 text-muted hover:bg-surface1 hover:text-accent">
<button type="button" onClick={handleVibe} aria-label="Start a Vibe from this track" title="Vibe by track" className="rounded p-1.5 text-muted hover:bg-surface1 hover:text-accent">
<Sparkles size={16} />
</button>
)}
@@ -106,13 +124,14 @@ export function TrackRow({ track, queue, showActions = true, variant = 'default'
to="/albums/$albumId"
params={{ albumId: track.album_id }}
onClick={(e) => e.stopPropagation()}
aria-label="Go to album"
title="Go to album"
className="rounded p-1.5 text-muted hover:bg-surface1 hover:text-text"
>
<Disc3 size={16} />
</Link>
)}
<button onClick={handleDislike} title="Dislike" className="rounded p-1.5 text-muted hover:bg-surface1 hover:text-red-400">
<button type="button" onClick={handleDislike} aria-label={`Dislike ${track.title || 'track'}`} title="Dislike" className="rounded p-1.5 text-muted hover:bg-surface1 hover:text-red-400">
<ThumbsDown size={16} />
</button>
</div>