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:
Psychotoxical
2026-04-20 17:28:20 +02:00
parent 5aac4d4ed0
commit ccf6e6c886
2 changed files with 24 additions and 6 deletions
+21 -6
View File
@@ -832,6 +832,9 @@ export default function WaveformSeek({ trackId }: Props) {
useEffect(() => {
return usePlayerStore.subscribe((state, prev) => {
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;
bufferedRef.current = state.buffered;
if (!ANIMATED_STYLES.has(styleRef.current)) {
@@ -890,15 +893,23 @@ export default function WaveformSeek({ trackId }: Props) {
trackIdRef.current = trackId;
const seekRef = useRef(seek);
seekRef.current = seek;
const pendingSeekRef = useRef<number | null>(null);
// Seek to a 01 fraction: draw immediately for 1:1 responsiveness, then
// let the store + Rust catch up asynchronously.
const seekToFraction = (fraction: number) => {
// Preview a 01 fraction while dragging: draw immediately for 1:1
// responsiveness; the actual seek is committed on mouseup.
const previewFraction = (fraction: number) => {
progressRef.current = fraction;
pendingSeekRef.current = fraction;
const canvas = canvasRef.current;
if (canvas && !ANIMATED_STYLES.has(styleRef.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);
};
@@ -907,10 +918,14 @@ export default function WaveformSeek({ trackId }: Props) {
const canvas = canvasRef.current;
if (!canvas || !trackIdRef.current) return;
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 onUp = () => { isDragging.current = false; };
const onUp = () => {
if (!isDragging.current) return;
isDragging.current = false;
commitSeek();
};
window.addEventListener('mousemove', onMove);
window.addEventListener('mouseup', onUp);
return () => {
@@ -935,7 +950,7 @@ export default function WaveformSeek({ trackId }: Props) {
onMouseDown={e => {
isDragging.current = true;
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 => {
if (!trackId) return;
+3
View File
@@ -1427,6 +1427,9 @@ export const usePlayerStore = create<PlayerState>()(
seekDebounce = null;
seekTarget = time;
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 ?? '');
if (!msg.includes('not seekable')) {
console.error(err);