Files
deepdrft/DeepDrftPublic.Client/Layout/MainLayout.razor
T
daniel-c-harvey 2591710f09 refactor: replace eval dark-mode body-class with TS theme interop helper
Extracts setBodyThemeClass into DeepDrftShared.Client/Interop/theme/theme.ts;
MainLayout lazy-imports the compiled module and calls it, matching the
established knob/parallax IJSObjectReference pattern. DisposeAsync added.
2026-06-20 00:26:52 -04:00

122 lines
4.3 KiB
Plaintext

@using DeepDrftPublic.Client.Controls
@using DeepDrftPublic.Client.Controls.AudioPlayerBar
@using DeepDrftPublic.Client.Services
@using DeepDrftPublic.Client.Common
@using DeepDrftShared.Client.Common
@using Microsoft.AspNetCore.Components
@inherits LayoutComponentBase
@implements IDisposable
@implements IAsyncDisposable
<MudThemeProvider Theme="@DeepDrftPalettes.Default" IsDarkMode="_isDarkMode" />
<MudPopoverProvider />
<MudDialogProvider />
<MudSnackbarProvider />
<div class="@ThemeWrapperClass">
<MudLayout Style="display: flex; flex-direction: column; min-height: 100vh">
<AudioPlayerProvider>
<DeepDrftMenu Elevation="4" @bind-IsDarkMode="_isDarkMode" />
<MudMainContent Class="flex-grow-1 pb-8 dd-main-content">
<MudContainer MaxWidth="MaxWidth.False" Class="pa-4">
@Body
</MudContainer>
</MudMainContent>
<DeepDrftFooter />
<AudioPlayerBar OnMinimized="ToggleAudioPlayerMinimized" />
@* 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>
<div id="blazor-error-ui" data-nosnippet>
An unhandled error has occurred.
<a href="." class="reload">Reload</a>
<span class="dismiss">🗙</span>
</div>
@code {
private string _audioPlayerClass = "minimized";
private const string DarkModeKey = "darkMode";
private bool _isDarkMode = false;
private bool? _lastAppliedDarkMode = null;
private PersistingComponentStateSubscription _persistingSubscription;
private IJSObjectReference? _themeModule;
[Inject] public required PersistentComponentState PersistentState { get; set; }
[Inject] public required DarkModeSettings DarkModeSettings { get; set; }
[Inject] public required IJSRuntime JS { get; set; }
protected override void OnInitialized()
{
base.OnInitialized();
// Restore persisted dark mode state (from server prerender)
if (PersistentState.TryTakeFromJson<bool>(DarkModeKey, out var restored))
{
_isDarkMode = restored;
DarkModeSettings.IsDarkMode = restored;
}
else
{
_isDarkMode = DarkModeSettings.IsDarkMode;
}
// Register to persist state when prerendering completes
_persistingSubscription = PersistentState.RegisterOnPersisting(PersistDarkMode);
}
// Sync dark mode class on <body> so portaled MudBlazor elements (popovers, menus, selects)
// inherit --deepdrft-popover-surface from body.deepdrft-theme-dark rather than from :root only.
// Popovers portal outside the ThemeWrapperClass div, so only a body-level class can reach them.
// Gated: only fires on first render or when _isDarkMode actually changes, to avoid redundant
// JS calls on unrelated re-renders (e.g. audio player minimize/expand).
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
if (firstRender || _isDarkMode != _lastAppliedDarkMode)
{
_lastAppliedDarkMode = _isDarkMode;
_themeModule ??= await JS.InvokeAsync<IJSObjectReference>(
"import", "./_content/DeepDrftShared.Client/js/theme/theme.js");
await _themeModule.InvokeVoidAsync("setBodyThemeClass", _isDarkMode);
}
}
// Theme wrapper class for CSS targeting
private string ThemeWrapperClass => _isDarkMode ? "deepdrft-theme-dark" : "deepdrft-theme-light";
private Task PersistDarkMode()
{
PersistentState.PersistAsJson(DarkModeKey, _isDarkMode);
return Task.CompletedTask;
}
public void Dispose()
{
_persistingSubscription.Dispose();
}
public async ValueTask DisposeAsync()
{
if (_themeModule != null)
{
try { await _themeModule.DisposeAsync(); }
catch (JSDisconnectedException) { /* circuit torn down */ }
}
}
private void ToggleAudioPlayerMinimized(bool isMinimized)
{
_audioPlayerClass = isMinimized ? "minimized" : "expanded";
}
}