fix(ui): one transport for every control, and a queue panel that fits

The queue panel drove the playback store directly, so its buttons played
locally while another device held the audio. Both control sets now go
through one transport that forwards a press when the audio is elsewhere.

The panel's artwork is capped against viewport height too: at full width
on a phone the square alone pushed Up Next off the screen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
kami
2026-08-08 20:56:30 +04:00
parent 4ead344aec
commit 85ca9cf543
5 changed files with 147 additions and 26 deletions
+14 -7
View File
@@ -7,6 +7,7 @@ import { Artwork } from './Artwork';
import { ArtistLinks } from './ArtistLinks';
import { TrackRow, formatDuration } from './TrackRow';
import { albumService } from '../services/albumService';
import { useTransport } from '../hooks/useTransport';
export function NowPlayingPanel({ onClose }: { onClose: () => void }) {
const panelRef = useRef<HTMLElement>(null);
@@ -34,7 +35,8 @@ export function NowPlayingPanel({ onClose }: { onClose: () => void }) {
previous?.focus();
};
}, []);
const { currentTrack, queue, isPlaying, position, duration, play, pause, next, prev, setPosition } = usePlaybackStore();
const { currentTrack, queue, isPlaying, position, duration } = usePlaybackStore();
const transport = useTransport();
const currentIdx = currentTrack ? queue.findIndex((t) => t.id === currentTrack.id) : -1;
const upNext = currentIdx >= 0 ? queue.slice(currentIdx + 1) : queue;
@@ -56,17 +58,21 @@ export function NowPlayingPanel({ onClose }: { onClose: () => void }) {
</button>
</div>
{/* The artwork below is capped against viewport height, not just width.
At full width on a phone the square alone is taller than the space
between the top bar and the transport, which pushed Up Next — the
reason the panel opens — entirely off the screen. */}
<div className="p-3 space-y-3 sm:p-4 sm:space-y-4">
{currentTrack?.album_id ? (
<Link to="/albums/$albumId" params={{ albumId: currentTrack.album_id }}
className="group mx-auto block aspect-square w-full max-w-sm rounded-xl overflow-hidden relative shadow-lg shadow-black/40" title="Go to album">
className="group mx-auto block aspect-square w-full max-w-[min(100%,34vh)] 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="mx-auto aspect-square w-full max-w-sm rounded-xl overflow-hidden shadow-lg shadow-black/40">
<div className="mx-auto aspect-square w-full max-w-[min(100%,34vh)] 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>
)}
@@ -90,7 +96,7 @@ export function NowPlayingPanel({ onClose }: { onClose: () => void }) {
<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))}
onChange={(e) => transport.seek(Number(e.target.value))}
disabled={!currentTrack || duration <= 0}
className="w-full cursor-pointer"
/>
@@ -101,15 +107,16 @@ export function NowPlayingPanel({ onClose }: { onClose: () => void }) {
</div>
<div className="flex items-center justify-center gap-4 sm:gap-6">
<button onClick={prev} aria-label="Previous" className="rounded-md p-2 text-muted hover:text-text hover:bg-surface0"><SkipBack size={20} /></button>
<button onClick={transport.prev} aria-label="Previous" className="rounded-md p-2 text-muted hover:text-text hover:bg-surface0"><SkipBack size={20} /></button>
<button
onClick={() => isPlaying ? pause() : play()}
onClick={transport.toggle}
aria-label={isPlaying ? 'Pause' : 'Play'}
disabled={!currentTrack}
className="transport-btn"
>
{isPlaying ? <Pause size={18} fill="currentColor" /> : <Play size={18} fill="currentColor" />}
</button>
<button onClick={next} aria-label="Next" className="rounded-md p-2 text-muted hover:text-text hover:bg-surface0"><SkipForward size={20} /></button>
<button onClick={transport.next} aria-label="Next" className="rounded-md p-2 text-muted hover:text-text hover:bg-surface0"><SkipForward size={20} /></button>
</div>
</div>