feature: Home Page & Footer Mobile Friendly
Deploy DeepDrftAPI / Build, Publish & Bundle (push) Successful in 1m56s
Deploy DeepDrftManager / Build & Publish (push) Successful in 1m3s
Deploy DeepDrftPublic / Build & Publish (push) Successful in 3m22s
Deploy DeepDrftAPI / Deploy (push) Successful in 1m33s
Deploy DeepDrftManager / Deploy (push) Successful in 1m27s
Deploy DeepDrftPublic / Deploy (push) Successful in 1m29s

This commit is contained in:
daniel-c-harvey
2026-06-07 13:48:12 -04:00
parent 4072197313
commit bd15b66aee
9 changed files with 147 additions and 20 deletions
@@ -1,6 +1,7 @@
using DeepDrftModels.DTOs;
using DeepDrftPublic.Client.Services;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using MudBlazor;
namespace DeepDrftPublic.Client.Controls.AudioPlayerBar;
@@ -9,12 +10,25 @@ public partial class AudioPlayerBar : ComponentBase, IAsyncDisposable
{
[CascadingParameter] public IStreamingPlayerService? PlayerService { get; set; }
[Parameter] public bool Fixed { get; set; } = false;
[Parameter] public EventCallback<bool> OnMinimized { get; set; }
[Inject] private IJSRuntime JsRuntime { get; set; } = default!;
private bool _isMinimized = true;
private bool _isSeeking = false;
private double _seekPosition = 0;
private IStreamingPlayerService? _subscribedService;
// Spacer-height bridge: the expanded dock is position:fixed, so MainLayout's
// spacer reserves its space. We mirror this element's live height into a CSS
// var via a ResizeObserver (see Interop/layout/spacer.ts) rather than a static
// value, because the player reflows across breakpoints and grows with the
// error banner.
private ElementReference _playerRoot;
private IJSObjectReference? _spacerModule;
private bool _spacerObserved;
private bool IsLoaded => PlayerService?.IsLoaded ?? false;
private bool IsLoading => PlayerService?.IsLoading ?? false;
@@ -66,6 +80,41 @@ public partial class AudioPlayerBar : ComponentBase, IAsyncDisposable
private void OnPlayerStateChanged() => InvokeAsync(StateHasChanged);
protected override async Task OnAfterRenderAsync(bool firstRender)
{
// Only the docked, expanded shape needs a spacer: the Fixed embed is
// already in normal flow, and the minimized FAB floats clear of content.
// Toggle the observer on the minimized/expanded transition only — the
// ResizeObserver itself handles every size change in between.
var shouldObserve = !_isMinimized && !Fixed;
if (shouldObserve == _spacerObserved) return;
var module = await GetSpacerModuleAsync();
if (module is null) return;
if (shouldObserve)
await module.InvokeVoidAsync("observe", _playerRoot);
else
await module.InvokeVoidAsync("unobserve");
_spacerObserved = shouldObserve;
}
private async Task<IJSObjectReference?> GetSpacerModuleAsync()
{
try
{
return _spacerModule ??= await JsRuntime.InvokeAsync<IJSObjectReference>(
"import", "./js/layout/spacer.js");
}
catch (JSException)
{
// Module failed to load — the spacer falls back to 0px (no overlap
// guard, but the player still works). Nothing actionable here.
return null;
}
}
private async Task Expand()
{
if (_isMinimized)
@@ -127,9 +176,10 @@ public partial class AudioPlayerBar : ComponentBase, IAsyncDisposable
PlayerService?.ClearError();
}
private void ToggleMinimized()
private async Task ToggleMinimized()
{
_isMinimized = !_isMinimized;
if (OnMinimized.HasDelegate) await OnMinimized.InvokeAsync(_isMinimized);
StateHasChanged();
}
@@ -147,13 +197,27 @@ public partial class AudioPlayerBar : ComponentBase, IAsyncDisposable
}
}
public ValueTask DisposeAsync()
public async ValueTask DisposeAsync()
{
if (_subscribedService != null)
{
_subscribedService.StateChanged -= OnPlayerStateChanged;
_subscribedService = null;
}
return ValueTask.CompletedTask;
if (_spacerModule is not null)
{
try
{
// Clear the var so a torn-down player can't strand a spacer height.
await _spacerModule.InvokeVoidAsync("unobserve");
await _spacerModule.DisposeAsync();
}
catch (JSException)
{
// Runtime already gone (navigation/teardown) — nothing to clean up.
}
_spacerModule = null;
}
}
}