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
+7 -18
View File
@@ -6,7 +6,7 @@ import { Artwork } from './Artwork';
import { ArtistLinks } from './ArtistLinks';
import { formatDuration } from './TrackRow';
import { DevicePicker } from './DevicePicker';
import { usePlaybackSyncContext } from './PlaybackSyncProvider';
import { useTransport } from '../hooks/useTransport';
interface PlaybackBarProps {
queueOpen: boolean;
@@ -16,20 +16,9 @@ interface PlaybackBarProps {
}
export function PlaybackBar({ queueOpen, lyricsOpen, onToggleQueue, onToggleLyrics }: PlaybackBarProps) {
const { currentTrack, isPlaying, position, duration, volume, shuffle, repeat, play, pause, next, prev, setPosition, setVolume, toggleShuffle, cycleRepeat } = usePlaybackStore();
const { currentTrack, isPlaying, position, duration, volume, shuffle, repeat, setVolume, toggleShuffle, cycleRepeat } = usePlaybackStore();
const dislikeTrack = useDislikeTrack();
const { hasRemoteOwner, sendCommand } = usePlaybackSyncContext();
// While another device holds the audio, the transport is a remote: the press
// travels to that device instead of starting a second stream here.
const remote = {
play: () => (hasRemoteOwner ? void sendCommand({ type: 'play' }) : play()),
pause: () => (hasRemoteOwner ? void sendCommand({ type: 'pause' }) : pause()),
next: () => (hasRemoteOwner ? void sendCommand({ type: 'next' }) : next()),
prev: () => (hasRemoteOwner ? void sendCommand({ type: 'prev' }) : prev()),
seek: (seconds: number) =>
hasRemoteOwner ? void sendCommand({ type: 'seek', position: seconds }) : setPosition(seconds),
};
const transport = useTransport();
const handleDislike = () => {
if (!currentTrack) return;
@@ -94,18 +83,18 @@ export function PlaybackBar({ queueOpen, lyricsOpen, onToggleQueue, onToggleLyri
>
<Shuffle size={18} />
</button>
<button onClick={remote.prev} className="rounded-md p-2 text-muted hover:text-text hover:bg-surface0" aria-label="Previous">
<button onClick={transport.prev} className="rounded-md p-2 text-muted hover:text-text hover:bg-surface0" aria-label="Previous">
<SkipBack size={20} />
</button>
<button
onClick={() => (isPlaying ? remote.pause() : remote.play())}
onClick={transport.toggle}
disabled={!currentTrack}
className="transport-btn"
aria-label={isPlaying ? 'Pause' : 'Play'}
>
{isPlaying ? <Pause size={18} fill="currentColor" /> : <Play size={18} fill="currentColor" />}
</button>
<button onClick={remote.next} className="rounded-md p-2 text-muted hover:text-text hover:bg-surface0" aria-label="Next">
<button onClick={transport.next} className="rounded-md p-2 text-muted hover:text-text hover:bg-surface0" aria-label="Next">
<SkipForward size={20} />
</button>
<button
@@ -122,7 +111,7 @@ export function PlaybackBar({ queueOpen, lyricsOpen, onToggleQueue, onToggleLyri
<input
type="range" min={0} max={Math.max(duration, 0.1)} step={0.1}
value={Math.min(position, duration || 0)}
onChange={(e) => remote.seek(Number(e.target.value))}
onChange={(e) => transport.seek(Number(e.target.value))}
disabled={!currentTrack || duration <= 0}
className="flex-1 h-1 cursor-pointer"
aria-label="Seek"