fix: WaveformSeeker resize drift and mobile fast-tap crash
- Add ResizeObserver (JS observeResize/unobserveResize + C# OnWidthChanged)
so _elementWidth stays current after window resize, fixing hover indicator drift
- Move _isSeeking = true before capturePointer await so a fast mobile tap
that fires pointerup mid-await still commits the seek
- Replace all Duration!.Value null-forgiving dereferences with explicit
Duration is > 0 guards in all four pointer event handlers
- Silence post-dispose resize callback rejections with .catch(() => {})
This commit is contained in:
@@ -32,6 +32,8 @@ public partial class WaveformSeeker : ComponentBase, IAsyncDisposable
|
|||||||
private ElementReference _seekerElement;
|
private ElementReference _seekerElement;
|
||||||
private IStreamingPlayerService? _subscribedService;
|
private IStreamingPlayerService? _subscribedService;
|
||||||
private IJSObjectReference? _jsModule;
|
private IJSObjectReference? _jsModule;
|
||||||
|
private IJSObjectReference? _resizeObserver;
|
||||||
|
private DotNetObjectReference<WaveformSeeker>? _dotNetRef;
|
||||||
|
|
||||||
// Bars currently drawn (normalized [0,1] heights). Recomputed only when the source profile
|
// Bars currently drawn (normalized [0,1] heights). Recomputed only when the source profile
|
||||||
// identity or the rendered width changes — not on every seek/progress tick.
|
// identity or the rendered width changes — not on every seek/progress tick.
|
||||||
@@ -108,12 +110,7 @@ public partial class WaveformSeeker : ComponentBase, IAsyncDisposable
|
|||||||
|
|
||||||
if (_jsModule is null) return;
|
if (_jsModule is null) return;
|
||||||
|
|
||||||
// Gate the interop call: only measure width on the first render or when the cached width
|
if (firstRender)
|
||||||
// has been reset to zero (e.g. after a track change resets state). Subsequent renders
|
|
||||||
// driven by position ticks or hover events don't need a width measurement — the element
|
|
||||||
// size hasn't changed. ResizeObserver integration is deferred; for now first-render-only
|
|
||||||
// measurement is sufficient since the bar is a fixed-height dock widget.
|
|
||||||
if (firstRender || _elementWidth == 0)
|
|
||||||
{
|
{
|
||||||
var width = await _jsModule.InvokeAsync<double>("getWidth", _seekerElement);
|
var width = await _jsModule.InvokeAsync<double>("getWidth", _seekerElement);
|
||||||
if (width > 0 && Math.Abs(width - _elementWidth) > 1)
|
if (width > 0 && Math.Abs(width - _elementWidth) > 1)
|
||||||
@@ -122,6 +119,20 @@ public partial class WaveformSeeker : ComponentBase, IAsyncDisposable
|
|||||||
RebuildBars();
|
RebuildBars();
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_dotNetRef = DotNetObjectReference.Create(this);
|
||||||
|
_resizeObserver = await _jsModule.InvokeAsync<IJSObjectReference>("observeResize", _seekerElement, _dotNetRef);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[JSInvokable]
|
||||||
|
public void OnWidthChanged(double width)
|
||||||
|
{
|
||||||
|
if (width > 0 && Math.Abs(width - _elementWidth) > 1)
|
||||||
|
{
|
||||||
|
_elementWidth = width;
|
||||||
|
RebuildBars();
|
||||||
|
InvokeAsync(StateHasChanged);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,6 +183,11 @@ public partial class WaveformSeeker : ComponentBase, IAsyncDisposable
|
|||||||
{
|
{
|
||||||
if (!CanSeek) return;
|
if (!CanSeek) return;
|
||||||
|
|
||||||
|
// Set seeking state BEFORE the async capturePointer so a fast mobile tap that fires
|
||||||
|
// pointerup before capturePointer returns doesn't miss the commit.
|
||||||
|
_isSeeking = true;
|
||||||
|
_seekFraction = FractionFromOffset(e.OffsetX);
|
||||||
|
|
||||||
// Capture the pointer so a drag that leaves the element keeps tracking until release.
|
// Capture the pointer so a drag that leaves the element keeps tracking until release.
|
||||||
if (_jsModule is not null)
|
if (_jsModule is not null)
|
||||||
{
|
{
|
||||||
@@ -185,28 +201,28 @@ public partial class WaveformSeeker : ComponentBase, IAsyncDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_isSeeking = true;
|
if (Duration is not > 0) { _isSeeking = false; return; }
|
||||||
_seekFraction = FractionFromOffset(e.OffsetX);
|
|
||||||
await OnSeekStart.InvokeAsync();
|
await OnSeekStart.InvokeAsync();
|
||||||
await OnSeekChange.InvokeAsync(_seekFraction * Duration!.Value);
|
await OnSeekChange.InvokeAsync(_seekFraction * Duration.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task HandlePointerMove(PointerEventArgs e)
|
private async Task HandlePointerMove(PointerEventArgs e)
|
||||||
{
|
{
|
||||||
if (!CanSeek) return;
|
if (!CanSeek) return;
|
||||||
|
if (Duration is not > 0) return;
|
||||||
|
|
||||||
var fraction = FractionFromOffset(e.OffsetX);
|
var fraction = FractionFromOffset(e.OffsetX);
|
||||||
|
|
||||||
if (_isSeeking)
|
if (_isSeeking)
|
||||||
{
|
{
|
||||||
_seekFraction = fraction;
|
_seekFraction = fraction;
|
||||||
await OnSeekChange.InvokeAsync(fraction * Duration!.Value);
|
await OnSeekChange.InvokeAsync(fraction * Duration.Value);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_showHover = true;
|
_showHover = true;
|
||||||
_hoverFraction = fraction;
|
_hoverFraction = fraction;
|
||||||
_hoverTime = fraction * Duration!.Value;
|
_hoverTime = fraction * Duration.Value;
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -214,10 +230,11 @@ public partial class WaveformSeeker : ComponentBase, IAsyncDisposable
|
|||||||
private async Task HandlePointerUp(PointerEventArgs e)
|
private async Task HandlePointerUp(PointerEventArgs e)
|
||||||
{
|
{
|
||||||
if (!_isSeeking) return;
|
if (!_isSeeking) return;
|
||||||
|
|
||||||
_seekFraction = FractionFromOffset(e.OffsetX);
|
|
||||||
_isSeeking = false;
|
_isSeeking = false;
|
||||||
await OnSeekEnd.InvokeAsync(_seekFraction * Duration!.Value);
|
|
||||||
|
if (Duration is not > 0) return;
|
||||||
|
_seekFraction = FractionFromOffset(e.OffsetX);
|
||||||
|
await OnSeekEnd.InvokeAsync(_seekFraction * Duration.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task HandlePointerLeave()
|
private async Task HandlePointerLeave()
|
||||||
@@ -227,7 +244,8 @@ public partial class WaveformSeeker : ComponentBase, IAsyncDisposable
|
|||||||
if (_isSeeking)
|
if (_isSeeking)
|
||||||
{
|
{
|
||||||
_isSeeking = false;
|
_isSeeking = false;
|
||||||
await OnSeekEnd.InvokeAsync(_seekFraction * Duration!.Value);
|
if (Duration is > 0)
|
||||||
|
await OnSeekEnd.InvokeAsync(_seekFraction * Duration.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_showHover)
|
if (_showHover)
|
||||||
@@ -261,6 +279,15 @@ public partial class WaveformSeeker : ComponentBase, IAsyncDisposable
|
|||||||
_subscribedService = null;
|
_subscribedService = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_resizeObserver is not null)
|
||||||
|
{
|
||||||
|
try { await (_jsModule?.InvokeVoidAsync("unobserveResize", _resizeObserver) ?? ValueTask.CompletedTask); } catch { }
|
||||||
|
try { await _resizeObserver.DisposeAsync(); } catch { }
|
||||||
|
_resizeObserver = null;
|
||||||
|
}
|
||||||
|
_dotNetRef?.Dispose();
|
||||||
|
_dotNetRef = null;
|
||||||
|
|
||||||
if (_jsModule is not null)
|
if (_jsModule is not null)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -10,3 +10,18 @@ export function capturePointer(element, pointerId) {
|
|||||||
// setPointerCapture keeps a drag tracking even when the pointer leaves the element bounds.
|
// setPointerCapture keeps a drag tracking even when the pointer leaves the element bounds.
|
||||||
element?.setPointerCapture?.(pointerId);
|
element?.setPointerCapture?.(pointerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function observeResize(element, dotNetRef) {
|
||||||
|
if (!element || typeof ResizeObserver === 'undefined') return null;
|
||||||
|
const observer = new ResizeObserver(entries => {
|
||||||
|
for (const entry of entries) {
|
||||||
|
dotNetRef.invokeMethodAsync('OnWidthChanged', entry.contentRect.width).catch(() => {});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
observer.observe(element);
|
||||||
|
return observer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unobserveResize(observer) {
|
||||||
|
observer?.disconnect();
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user