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
|
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">
|
<MudContainer MaxWidth="MaxWidth.Large" Class="player-inner-container">
|
||||||
<MudPaper Elevation="8" Class="player-surface pa-3">
|
<MudPaper Elevation="8" Class="player-surface pa-3">
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using DeepDrftModels.DTOs;
|
using DeepDrftModels.DTOs;
|
||||||
using DeepDrftPublic.Client.Services;
|
using DeepDrftPublic.Client.Services;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using Microsoft.JSInterop;
|
||||||
using MudBlazor;
|
using MudBlazor;
|
||||||
|
|
||||||
namespace DeepDrftPublic.Client.Controls.AudioPlayerBar;
|
namespace DeepDrftPublic.Client.Controls.AudioPlayerBar;
|
||||||
@@ -10,11 +11,24 @@ public partial class AudioPlayerBar : ComponentBase, IAsyncDisposable
|
|||||||
[CascadingParameter] public IStreamingPlayerService? PlayerService { get; set; }
|
[CascadingParameter] public IStreamingPlayerService? PlayerService { get; set; }
|
||||||
[Parameter] public bool Fixed { get; set; } = false;
|
[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 _isMinimized = true;
|
||||||
private bool _isSeeking = false;
|
private bool _isSeeking = false;
|
||||||
private double _seekPosition = 0;
|
private double _seekPosition = 0;
|
||||||
private IStreamingPlayerService? _subscribedService;
|
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 IsLoaded => PlayerService?.IsLoaded ?? false;
|
||||||
private bool IsLoading => PlayerService?.IsLoading ?? false;
|
private bool IsLoading => PlayerService?.IsLoading ?? false;
|
||||||
|
|
||||||
@@ -66,6 +80,41 @@ public partial class AudioPlayerBar : ComponentBase, IAsyncDisposable
|
|||||||
|
|
||||||
private void OnPlayerStateChanged() => InvokeAsync(StateHasChanged);
|
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()
|
private async Task Expand()
|
||||||
{
|
{
|
||||||
if (_isMinimized)
|
if (_isMinimized)
|
||||||
@@ -127,9 +176,10 @@ public partial class AudioPlayerBar : ComponentBase, IAsyncDisposable
|
|||||||
PlayerService?.ClearError();
|
PlayerService?.ClearError();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ToggleMinimized()
|
private async Task ToggleMinimized()
|
||||||
{
|
{
|
||||||
_isMinimized = !_isMinimized;
|
_isMinimized = !_isMinimized;
|
||||||
|
if (OnMinimized.HasDelegate) await OnMinimized.InvokeAsync(_isMinimized);
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,13 +197,27 @@ public partial class AudioPlayerBar : ComponentBase, IAsyncDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public ValueTask DisposeAsync()
|
public async ValueTask DisposeAsync()
|
||||||
{
|
{
|
||||||
if (_subscribedService != null)
|
if (_subscribedService != null)
|
||||||
{
|
{
|
||||||
_subscribedService.StateChanged -= OnPlayerStateChanged;
|
_subscribedService.StateChanged -= OnPlayerStateChanged;
|
||||||
_subscribedService = null;
|
_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" />
|
<ProjectReference Include="..\DeepDrftShared.Client\DeepDrftShared.Client.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Content Update="Layout\NavMenu.razor">
|
|
||||||
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
|
||||||
</Content>
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -4,5 +4,5 @@
|
|||||||
<li><a href="#">About</a></li>
|
<li><a href="#">About</a></li>
|
||||||
<li><a href="#">Contact</a></li>
|
<li><a href="#">Contact</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="deepdrft-footer-copy">© 2026 Deep DRFT — Charleston, SC</div>
|
<div class="deepdrft-footer-copy">© 2026 Deep DRFT</div>
|
||||||
</footer>
|
</footer>
|
||||||
@@ -47,15 +47,20 @@
|
|||||||
color: var(--deepdrft-muted);
|
color: var(--deepdrft-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 599px) {
|
@media (max-width: 440px) {
|
||||||
.deepdrft-footer {
|
.deepdrft-footer {
|
||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
flex-wrap: wrap;
|
/*flex-wrap: wrap;*/
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.deepdrft-footer-links {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
.deepdrft-footer-copy {
|
.deepdrft-footer-copy {
|
||||||
width: 100%;
|
/*width: 100%;*/
|
||||||
text-align: right;
|
justify-self: right;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,10 +21,12 @@
|
|||||||
</MudContainer>
|
</MudContainer>
|
||||||
</MudMainContent>
|
</MudMainContent>
|
||||||
<DeepDrftFooter />
|
<DeepDrftFooter />
|
||||||
<AudioPlayerBar />
|
<AudioPlayerBar OnMinimized="ToggleAudioPlayerMinimized" />
|
||||||
|
|
||||||
@* Spacer to prevent content overlap *@
|
@* Spacer to prevent content overlap. Height tracks the fixed player
|
||||||
<div class="player-spacer"></div>
|
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>
|
</AudioPlayerProvider>
|
||||||
</MudLayout>
|
</MudLayout>
|
||||||
</div>
|
</div>
|
||||||
@@ -37,6 +39,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
private string _audioPlayerClass = "minimized";
|
||||||
private const string DarkModeKey = "darkMode";
|
private const string DarkModeKey = "darkMode";
|
||||||
private bool _isDarkMode = false;
|
private bool _isDarkMode = false;
|
||||||
private PersistingComponentStateSubscription _persistingSubscription;
|
private PersistingComponentStateSubscription _persistingSubscription;
|
||||||
@@ -76,6 +79,12 @@
|
|||||||
{
|
{
|
||||||
_persistingSubscription.Dispose();
|
_persistingSubscription.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ToggleAudioPlayerMinimized(bool isMinimized)
|
||||||
|
{
|
||||||
|
_audioPlayerClass = isMinimized ? "minimized" : "expanded";
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
/* Spacer to prevent content overlap */
|
/* Spacer to prevent content overlap */
|
||||||
.player-spacer {
|
.player-spacer {
|
||||||
height: 100px;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.player-spacer.expanded {
|
||||||
|
height: var(--player-height, 60px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-spacer.minimized {
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
#blazor-error-ui {
|
#blazor-error-ui {
|
||||||
color-scheme: light only;
|
color-scheme: light only;
|
||||||
background: lightyellow;
|
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()
|
.AddInteractiveWebAssemblyRenderMode()
|
||||||
.AddAdditionalAssemblies(typeof(DeepDrftPublic.Client._Imports).Assembly);
|
.AddAdditionalAssemblies(typeof(DeepDrftPublic.Client._Imports).Assembly);
|
||||||
|
|
||||||
|
app.UseStatusCodePagesWithReExecute("/404", createScopeForStatusCodePages: true);
|
||||||
|
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|||||||
Reference in New Issue
Block a user