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
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:
@@ -9,7 +9,7 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="@PlayerModeClass d-flex flex-column">
|
||||
<div class="@PlayerModeClass d-flex flex-column" @ref="_playerRoot">
|
||||
<MudContainer MaxWidth="MaxWidth.Large" Class="player-inner-container">
|
||||
<MudPaper Elevation="8" Class="player-surface pa-3">
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -10,11 +11,24 @@ 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,10 +21,4 @@
|
||||
<ProjectReference Include="..\DeepDrftShared.Client\DeepDrftShared.Client.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Update="Layout\NavMenu.razor">
|
||||
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
<li><a href="#">About</a></li>
|
||||
<li><a href="#">Contact</a></li>
|
||||
</ul>
|
||||
<div class="deepdrft-footer-copy">© 2026 Deep DRFT — Charleston, SC</div>
|
||||
<div class="deepdrft-footer-copy">© 2026 Deep DRFT</div>
|
||||
</footer>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,10 +21,12 @@
|
||||
</MudContainer>
|
||||
</MudMainContent>
|
||||
<DeepDrftFooter />
|
||||
<AudioPlayerBar />
|
||||
<AudioPlayerBar OnMinimized="ToggleAudioPlayerMinimized" />
|
||||
|
||||
@* Spacer to prevent content overlap *@
|
||||
<div class="player-spacer"></div>
|
||||
@* 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). *@
|
||||
<div class="player-spacer @_audioPlayerClass"></div>
|
||||
</AudioPlayerProvider>
|
||||
</MudLayout>
|
||||
</div>
|
||||
@@ -37,6 +39,7 @@
|
||||
</div>
|
||||
|
||||
@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";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -116,5 +116,7 @@ app.MapRazorComponents<App>()
|
||||
.AddInteractiveWebAssemblyRenderMode()
|
||||
.AddAdditionalAssemblies(typeof(DeepDrftPublic.Client._Imports).Assembly);
|
||||
|
||||
app.UseStatusCodePagesWithReExecute("/404", createScopeForStatusCodePages: true);
|
||||
|
||||
|
||||
app.Run();
|
||||
|
||||
Reference in New Issue
Block a user