initial state: muzick music player + recommendation engine

This commit is contained in:
kami
2026-07-14 01:35:52 +04:00
commit 737bf19fd1
196 changed files with 32431 additions and 0 deletions
+114
View File
@@ -0,0 +1,114 @@
import { X, Play, Pause, SkipBack, SkipForward, Disc3 } from 'lucide-react';
import { useQuery } from '@tanstack/react-query';
import { Link } from '@tanstack/react-router';
import { usePlaybackStore } from '../store/usePlaybackStore';
import { Artwork } from './Artwork';
import { ArtistLinks } from './ArtistLinks';
import { TrackRow, formatDuration } from './TrackRow';
import { albumService } from '../services/albumService';
export function NowPlayingPanel({ onClose }: { onClose: () => void }) {
const { currentTrack, queue, isPlaying, position, duration, play, pause, next, prev, setPosition } = usePlaybackStore();
const currentIdx = currentTrack ? queue.findIndex((t) => t.id === currentTrack.id) : -1;
const upNext = currentIdx >= 0 ? queue.slice(currentIdx + 1) : queue;
const albumQ = useQuery({
queryKey: ['album', currentTrack?.album_id],
queryFn: () => albumService.getAlbum(currentTrack!.album_id),
enabled: !!currentTrack?.album_id,
staleTime: 5 * 60 * 1000,
});
const artwork = albumQ.data?.artwork_id ?? currentTrack?.artwork_id ?? null;
return (
<aside className="w-96 flex flex-col border-l border-border/70 bg-bg1/80 backdrop-blur-sm overflow-hidden shrink-0">
<div className="flex items-center justify-between px-4 py-3 border-b border-border/70">
<span className="text-sm font-semibold text-text">Now Playing</span>
<button onClick={onClose} className="text-muted hover:text-text p-1 rounded">
<X size={16} />
</button>
</div>
<div className="p-4 space-y-4">
{currentTrack?.album_id ? (
<Link to="/albums/$albumId" params={{ albumId: currentTrack.album_id }}
className="group block aspect-square rounded-xl overflow-hidden relative shadow-lg shadow-black/40" title="Go to album">
<Artwork seed={`${currentTrack.title} ${currentTrack.artist}`} src={artwork} className="w-full h-full transition-transform group-hover:scale-105" rounded="xl" />
<div className="absolute inset-0 flex items-center justify-center bg-black/0 group-hover:bg-black/40 transition-colors">
<Disc3 size={28} className="text-on-accent opacity-0 group-hover:opacity-100 transition-opacity" />
</div>
</Link>
) : (
<div className="aspect-square rounded-xl overflow-hidden shadow-lg shadow-black/40">
<Artwork seed={currentTrack ? `${currentTrack.title} ${currentTrack.artist}` : 'empty'} src={artwork} className="w-full h-full" rounded="xl" />
</div>
)}
{currentTrack ? (
<div className="text-center space-y-0.5">
{currentTrack.album_id ? (
<Link to="/albums/$albumId" params={{ albumId: currentTrack.album_id }} className="font-bold text-text truncate block hover:underline">
{currentTrack.title}
</Link>
) : (
<div className="font-bold text-text truncate">{currentTrack.title}</div>
)}
<ArtistLinks artists={currentTrack.artists} fallback={currentTrack.artist} className="block text-sm text-muted truncate" />
</div>
) : (
<div className="text-center text-sm text-muted italic">No track playing</div>
)}
<div className="space-y-1">
<input
type="range" min={0} max={Math.max(duration, 0.1)} step={0.1}
value={Math.min(position, duration || 0)}
onChange={(e) => setPosition(Number(e.target.value))}
disabled={!currentTrack || duration <= 0}
className="w-full cursor-pointer"
/>
<div className="flex justify-between text-xs text-muted tabular-nums">
<span>{formatDuration(position)}</span>
<span>{formatDuration(duration)}</span>
</div>
</div>
<div className="flex items-center justify-center gap-6">
<button onClick={prev} className="text-muted hover:text-text"><SkipBack size={20} /></button>
<button
onClick={() => isPlaying ? pause() : play()}
disabled={!currentTrack}
className="transport-btn"
>
{isPlaying ? <Pause size={18} fill="currentColor" /> : <Play size={18} fill="currentColor" />}
</button>
<button onClick={next} className="text-muted hover:text-text"><SkipForward size={20} /></button>
</div>
</div>
<div className="px-4 py-2 border-t border-border/70 text-xs font-semibold uppercase tracking-wide text-muted">
Up Next ({upNext.length})
</div>
<div className="flex-1 overflow-y-auto px-2 py-2">
{upNext.length === 0 ? (
<div className="px-2 py-4 text-sm text-muted italic">Queue is empty.</div>
) : (
<div className="space-y-0.5">
{upNext.map((track, i) => (
<TrackRow
key={`${track.id}-${i}`}
track={track}
queue={upNext.slice(i)}
index={0}
showActions={false}
variant="compact"
/>
))}
</div>
)}
</div>
</aside>
);
}