feat: enhance discovery, vibe sessions, and library enrichment
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user