diff --git a/CHANGELOG.md b/CHANGELOG.md index 97a7947e..13e9d384 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -255,6 +255,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * **Catch Up button stays clickable on high-latency sessions** (PR [#527](https://github.com/Psychotoxical/psysonic/pull/527)). On a real-world high-latency session the genuine drift fluctuates between ~1 s and ~8 s in lockstep with both sides' chunked `currentTime` updates — the previous single-stage debounce hid the button as soon as drift briefly dipped below 3 s even though the baseline drift was still 5–8 s, so the button "appeared and vanished too fast to click". Two-stage hysteresis now: show after drift > 3 s for 3 s, hide only after drift < 1 s for 1 s. * **Initial-sync seek visually sticks on join** (PR [#528](https://github.com/Psychotoxical/psysonic/pull/528)). On join the waveform briefly showed the host's live position then snapped back to 0:00 with audio playing from the start — the post-`playTrack` poll fired `applyMirror`'s seek against `playTrack`'s optimistic `isPlaying: true`, before the Tauri `audio_play` had actually produced any samples. The seek's store update landed (waveform at 70 %) but the `audio_seek` debounced behind it no-oped on the not-ready engine, and the engine's first progress events from 0:00 overwrote the optimistic position. Now the poll waits for `currentTime > 0.1` before applying the seek, and `applyMirror` defers the play-state mirror by 200 ms so the seek's invoke wins the IPC ordering race against pause/resume. * **Host single-track plays no longer wipe the Orbit queue** (PR [#529](https://github.com/Psychotoxical/psysonic/pull/529)). A `playTrack(track, [track])` call from any UI that passes an explicit 1-track replacement queue (e.g. OfflineLibrary's "Play this album" on a single-track album) slipped past the orbit bulk-guard (which only fires for `queue.length > 1`) and replaced the host's `playerStore.queue`. Since the host's queue *is* the shared Orbit queue, that one click destroyed every accepted guest suggestion + every upcoming track. Now intercepted: when role is host and the incoming queue is a single track, append + jump instead of replacing. +* **Host pause / resume reaches guests immediately** (PR [#537](https://github.com/Psychotoxical/psysonic/pull/537), reported by xrexy on Discord). The host previously pushed state only on a 2.5 s timer; combined with the guest's 2.5 s poll plus network latency, a `pause` could take up to ~5 s to land — long enough for the guest to noticeably run past the host. The host now also pushes state on every `isPlaying` flip, in addition to the timer. Non-flip ticks are unchanged, so baseline traffic stays the same. +* **Guest seekbar is read-only inside an Orbit session** (PR [#537](https://github.com/Psychotoxical/psysonic/pull/537), reported by xrexy on Discord). Drag / click / wheel / hover are all disabled on the waveform while you're a guest; the bar shows as dimmed with a `not-allowed` cursor so the disabled state is unambiguous. Previously a guest seek would jump the local player and either snap back at the next host poll (inconsistent UX) or push the guest into a diverged state where Catch Up was the only way back. Hosts and non-orbit users see no change. ### Context menu — render above the floating player bar diff --git a/src/App.tsx b/src/App.tsx index 7b625469..f50309ef 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -197,8 +197,12 @@ function AppShell() { && (orbitPhase === 'active' || orbitPhase === 'joining' || orbitPhase === 'starting'); if (inOrbit) { document.documentElement.setAttribute('data-orbit-active', 'true'); + // Also expose the role so CSS can target host-vs-guest UI states + // (e.g. guest seekbar is read-only — sync follows the host). + document.documentElement.setAttribute('data-orbit-role', orbitRole as string); } else { document.documentElement.removeAttribute('data-orbit-active'); + document.documentElement.removeAttribute('data-orbit-role'); } }, [orbitRole, orbitPhase]); diff --git a/src/components/WaveformSeek.tsx b/src/components/WaveformSeek.tsx index 206f0a54..b0abbf6f 100644 --- a/src/components/WaveformSeek.tsx +++ b/src/components/WaveformSeek.tsx @@ -1350,7 +1350,7 @@ export default function WaveformSeek({ trackId }: Props) { }, []); return ( -
+
{hoverPct !== null && duration > 0 && ( { void pushState(); }, STATE_TICK_MS); - return () => window.clearInterval(id); + + // Event-driven push on play/pause flips. Without this the worst-case + // delay between "host hits pause" and "guest stops" is two full polling + // windows (host tick + guest tick = up to 5 s) — long enough for the + // guest to noticeably run ahead. Subscribing here adds at most one + // extra write per flip; non-flip state ticks still ride the 2.5 s + // timer. Listener filters on isPlaying so currentTime ticks don't + // trigger spurious pushes. + let prevIsPlaying = usePlayerStore.getState().isPlaying; + const unsubPlayPause = usePlayerStore.subscribe((state) => { + if (state.isPlaying === prevIsPlaying) return; + prevIsPlaying = state.isPlaying; + pushOrbitEvent('host:event-push', `isPlaying flip → ${state.isPlaying}`); + void pushState(); + }); + + return () => { + window.clearInterval(id); + unsubPlayPause(); + }; }, [active, sessionPlaylistId]); useEffect(() => { diff --git a/src/styles/components.css b/src/styles/components.css index 2de59241..d81bebcc 100644 --- a/src/styles/components.css +++ b/src/styles/components.css @@ -1915,6 +1915,18 @@ display: none; } +/* Guest in an Orbit session: seekbar is read-only — sync follows the host. + `pointer-events: none` blocks click / drag / wheel / hover; the dimmed + look + not-allowed cursor (set on the parent so it shows when hovered + over the now-inert child) signal "this control is not yours". */ +[data-orbit-role="guest"] .waveform-seek-container { + cursor: not-allowed; + opacity: 0.55; +} +[data-orbit-role="guest"] .waveform-seek-container > * { + pointer-events: none; +} + .playlist-suggestions .track-row:hover .playlist-suggestion-preview-btn, .playlist-suggestion-preview-btn:hover, .playlist-suggestion-preview-btn.is-previewing {