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:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user