diff --git a/DeepDrftPublic.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor b/DeepDrftPublic.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor
index f570315..dae7314 100644
--- a/DeepDrftPublic.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor
+++ b/DeepDrftPublic.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor
@@ -9,7 +9,7 @@
}
else
{
-
+
diff --git a/DeepDrftPublic.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor.cs b/DeepDrftPublic.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor.cs
index d2f8366..44aa136 100644
--- a/DeepDrftPublic.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor.cs
+++ b/DeepDrftPublic.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor.cs
@@ -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 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 GetSpacerModuleAsync()
+ {
+ try
+ {
+ return _spacerModule ??= await JsRuntime.InvokeAsync(
+ "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;
+ }
}
}
\ No newline at end of file
diff --git a/DeepDrftPublic.Client/DeepDrftPublic.Client.csproj b/DeepDrftPublic.Client/DeepDrftPublic.Client.csproj
index 715e8e0..cf4daaf 100644
--- a/DeepDrftPublic.Client/DeepDrftPublic.Client.csproj
+++ b/DeepDrftPublic.Client/DeepDrftPublic.Client.csproj
@@ -21,10 +21,4 @@
-
-
- true
-
-
-
diff --git a/DeepDrftPublic.Client/Layout/DeepDrftFooter.razor b/DeepDrftPublic.Client/Layout/DeepDrftFooter.razor
index 88653d8..ec9fd51 100644
--- a/DeepDrftPublic.Client/Layout/DeepDrftFooter.razor
+++ b/DeepDrftPublic.Client/Layout/DeepDrftFooter.razor
@@ -4,5 +4,5 @@
About
Contact
-
+
\ No newline at end of file
diff --git a/DeepDrftPublic.Client/Layout/DeepDrftFooter.razor.css b/DeepDrftPublic.Client/Layout/DeepDrftFooter.razor.css
index 668e192..7cfb9fe 100644
--- a/DeepDrftPublic.Client/Layout/DeepDrftFooter.razor.css
+++ b/DeepDrftPublic.Client/Layout/DeepDrftFooter.razor.css
@@ -47,15 +47,20 @@
color: var(--deepdrft-muted);
}
-@media (max-width: 599px) {
+@media (max-width: 440px) {
.deepdrft-footer {
padding: 1.5rem;
- flex-wrap: wrap;
+ /*flex-wrap: wrap;*/
gap: 1rem;
}
+
+ .deepdrft-footer-links {
+ flex-direction: column;
+ gap: 0.25rem;
+ }
.deepdrft-footer-copy {
- width: 100%;
- text-align: right;
+ /*width: 100%;*/
+ justify-self: right;
}
}
diff --git a/DeepDrftPublic.Client/Layout/MainLayout.razor b/DeepDrftPublic.Client/Layout/MainLayout.razor
index c24c59b..cca6ea2 100644
--- a/DeepDrftPublic.Client/Layout/MainLayout.razor
+++ b/DeepDrftPublic.Client/Layout/MainLayout.razor
@@ -21,10 +21,12 @@
-
+
- @* Spacer to prevent content overlap *@
-
+ @* Spacer to prevent content overlap. Height tracks the fixed player
+ dock's live size via the --player-height var the player publishes
+ (see AudioPlayerBar / Interop/layout/spacer.ts). *@
+
@@ -37,6 +39,7 @@
@code {
+ private string _audioPlayerClass = "minimized";
private const string DarkModeKey = "darkMode";
private bool _isDarkMode = false;
private PersistingComponentStateSubscription _persistingSubscription;
@@ -76,6 +79,12 @@
{
_persistingSubscription.Dispose();
}
+
+ private void ToggleAudioPlayerMinimized(bool isMinimized)
+ {
+ _audioPlayerClass = isMinimized ? "minimized" : "expanded";
+ }
+
}
diff --git a/DeepDrftPublic.Client/Layout/MainLayout.razor.css b/DeepDrftPublic.Client/Layout/MainLayout.razor.css
index 574c16e..1273952 100644
--- a/DeepDrftPublic.Client/Layout/MainLayout.razor.css
+++ b/DeepDrftPublic.Client/Layout/MainLayout.razor.css
@@ -1,10 +1,17 @@
/* Spacer to prevent content overlap */
.player-spacer {
- height: 100px;
width: 100%;
flex-shrink: 0;
}
+.player-spacer.expanded {
+ height: var(--player-height, 60px);
+}
+
+.player-spacer.minimized {
+ height: 60px;
+}
+
#blazor-error-ui {
color-scheme: light only;
background: lightyellow;
diff --git a/DeepDrftPublic/Interop/layout/spacer.ts b/DeepDrftPublic/Interop/layout/spacer.ts
new file mode 100644
index 0000000..72173be
--- /dev/null
+++ b/DeepDrftPublic/Interop/layout/spacer.ts
@@ -0,0 +1,46 @@
+/**
+ * Player-height spacer observer.
+ *
+ * The audio player docks `position: fixed` to the viewport bottom, so it sits
+ * outside normal flow and would overlap page content. A spacer div in the layout
+ * reserves the equivalent space — but the player's height is not constant: it
+ * reflows across the four breakpoints and grows when an error banner appears. A
+ * static height can't track that, so we mirror the player's live border-box
+ * height into the `--player-height` custom property on :root and let the spacer
+ * read it. One observer at a time, re-pointed on each `observe` call; the var
+ * resets to 0 on `unobserve` (player minimized / disposed) so the spacer
+ * collapses.
+ */
+
+const HEIGHT_VAR = '--player-height';
+let observer: ResizeObserver | null = null;
+
+function writeHeight(px: number): void {
+ // Round up so sub-pixel heights never leave a hairline of overlap.
+ document.documentElement.style.setProperty(HEIGHT_VAR, `${Math.ceil(px)}px`);
+}
+
+export function observe(element: Element): void {
+ unobserve();
+ if (!element) return;
+
+ observer = new ResizeObserver(entries => {
+ const entry = entries[0];
+ if (!entry) return;
+ // Prefer the border-box measurement; fall back to contentRect on the
+ // (older) engines that don't populate borderBoxSize.
+ const box = entry.borderBoxSize?.[0];
+ writeHeight(box ? box.blockSize : entry.contentRect.height);
+ });
+ observer.observe(element);
+
+ // Seed synchronously so the spacer is correct on this frame, before the
+ // first ResizeObserver callback fires.
+ writeHeight(element.getBoundingClientRect().height);
+}
+
+export function unobserve(): void {
+ observer?.disconnect();
+ observer = null;
+ writeHeight(0);
+}
diff --git a/DeepDrftPublic/Program.cs b/DeepDrftPublic/Program.cs
index d96d438..dff19a3 100644
--- a/DeepDrftPublic/Program.cs
+++ b/DeepDrftPublic/Program.cs
@@ -116,5 +116,7 @@ app.MapRazorComponents()
.AddInteractiveWebAssemblyRenderMode()
.AddAdditionalAssemblies(typeof(DeepDrftPublic.Client._Imports).Assembly);
+app.UseStatusCodePagesWithReExecute("/404", createScopeForStatusCodePages: true);
+
app.Run();