mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 14:35:41 +00:00
fix(seekbar): commit seek on mouseup + ignore progress ticks while dragging
Ports the WaveformSeek UX changes from PR #219 (original fix by @cucadmuh) onto test/seekable-streaming: - Split previewFraction (during drag) / commitSeek (on mouseup) so audio_seek fires once per gesture instead of on every mousemove. - Subscribe-effect skips external progress updates while isDragging — prevents cursor flicker from streaming/recovery ticks fighting the dragged position. - Clear seekTarget in seek() catch branch so a failed seek doesn't permanently block progress updates. Skips the byte-restart fast-path from #219: would defeat RangedHttpSource's direct seek by restarting the stream after 450ms. Co-Authored-By: cucadmuh <cucadmuh@users.noreply.github.com> Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -832,6 +832,9 @@ export default function WaveformSeek({ trackId }: Props) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return usePlayerStore.subscribe((state, prev) => {
|
return usePlayerStore.subscribe((state, prev) => {
|
||||||
if (state.progress === prev.progress && state.buffered === prev.buffered) return;
|
if (state.progress === prev.progress && state.buffered === prev.buffered) return;
|
||||||
|
// While user drags, keep the local preview stable. External progress ticks
|
||||||
|
// during streaming/recovery would otherwise fight the cursor and flicker.
|
||||||
|
if (isDragging.current) return;
|
||||||
progressRef.current = state.progress;
|
progressRef.current = state.progress;
|
||||||
bufferedRef.current = state.buffered;
|
bufferedRef.current = state.buffered;
|
||||||
if (!ANIMATED_STYLES.has(styleRef.current)) {
|
if (!ANIMATED_STYLES.has(styleRef.current)) {
|
||||||
@@ -890,15 +893,23 @@ export default function WaveformSeek({ trackId }: Props) {
|
|||||||
trackIdRef.current = trackId;
|
trackIdRef.current = trackId;
|
||||||
const seekRef = useRef(seek);
|
const seekRef = useRef(seek);
|
||||||
seekRef.current = seek;
|
seekRef.current = seek;
|
||||||
|
const pendingSeekRef = useRef<number | null>(null);
|
||||||
|
|
||||||
// Seek to a 0–1 fraction: draw immediately for 1:1 responsiveness, then
|
// Preview a 0–1 fraction while dragging: draw immediately for 1:1
|
||||||
// let the store + Rust catch up asynchronously.
|
// responsiveness; the actual seek is committed on mouseup.
|
||||||
const seekToFraction = (fraction: number) => {
|
const previewFraction = (fraction: number) => {
|
||||||
progressRef.current = fraction;
|
progressRef.current = fraction;
|
||||||
|
pendingSeekRef.current = fraction;
|
||||||
const canvas = canvasRef.current;
|
const canvas = canvasRef.current;
|
||||||
if (canvas && !ANIMATED_STYLES.has(styleRef.current)) {
|
if (canvas && !ANIMATED_STYLES.has(styleRef.current)) {
|
||||||
drawSeekbar(canvas, styleRef.current, heightsRef.current, fraction, bufferedRef.current);
|
drawSeekbar(canvas, styleRef.current, heightsRef.current, fraction, bufferedRef.current);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const commitSeek = () => {
|
||||||
|
const fraction = pendingSeekRef.current;
|
||||||
|
if (fraction === null) return;
|
||||||
|
pendingSeekRef.current = null;
|
||||||
seekRef.current(fraction);
|
seekRef.current(fraction);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -907,10 +918,14 @@ export default function WaveformSeek({ trackId }: Props) {
|
|||||||
const canvas = canvasRef.current;
|
const canvas = canvasRef.current;
|
||||||
if (!canvas || !trackIdRef.current) return;
|
if (!canvas || !trackIdRef.current) return;
|
||||||
const rect = canvas.getBoundingClientRect();
|
const rect = canvas.getBoundingClientRect();
|
||||||
seekToFraction(Math.max(0, Math.min(1, (clientX - rect.left) / rect.width)));
|
previewFraction(Math.max(0, Math.min(1, (clientX - rect.left) / rect.width)));
|
||||||
};
|
};
|
||||||
const onMove = (e: MouseEvent) => { if (isDragging.current) seekFromX(e.clientX); };
|
const onMove = (e: MouseEvent) => { if (isDragging.current) seekFromX(e.clientX); };
|
||||||
const onUp = () => { isDragging.current = false; };
|
const onUp = () => {
|
||||||
|
if (!isDragging.current) return;
|
||||||
|
isDragging.current = false;
|
||||||
|
commitSeek();
|
||||||
|
};
|
||||||
window.addEventListener('mousemove', onMove);
|
window.addEventListener('mousemove', onMove);
|
||||||
window.addEventListener('mouseup', onUp);
|
window.addEventListener('mouseup', onUp);
|
||||||
return () => {
|
return () => {
|
||||||
@@ -935,7 +950,7 @@ export default function WaveformSeek({ trackId }: Props) {
|
|||||||
onMouseDown={e => {
|
onMouseDown={e => {
|
||||||
isDragging.current = true;
|
isDragging.current = true;
|
||||||
const rect = e.currentTarget.getBoundingClientRect();
|
const rect = e.currentTarget.getBoundingClientRect();
|
||||||
seekToFraction(Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width)));
|
previewFraction(Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width)));
|
||||||
}}
|
}}
|
||||||
onMouseMove={e => {
|
onMouseMove={e => {
|
||||||
if (!trackId) return;
|
if (!trackId) return;
|
||||||
|
|||||||
@@ -1427,6 +1427,9 @@ export const usePlayerStore = create<PlayerState>()(
|
|||||||
seekDebounce = null;
|
seekDebounce = null;
|
||||||
seekTarget = time;
|
seekTarget = time;
|
||||||
invoke('audio_seek', { seconds: time }).catch((err: unknown) => {
|
invoke('audio_seek', { seconds: time }).catch((err: unknown) => {
|
||||||
|
// Release the progress-tick guard so the UI doesn't freeze
|
||||||
|
// waiting for a target the engine will never reach.
|
||||||
|
seekTarget = null;
|
||||||
const msg = String(err ?? '');
|
const msg = String(err ?? '');
|
||||||
if (!msg.includes('not seekable')) {
|
if (!msg.includes('not seekable')) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|||||||
Reference in New Issue
Block a user