@using DeepDrftPublic.Client.Common @using DeepDrftPublic.Client.Services @* Desktop Menu *@
@* Mobile Menu *@
@implements IDisposable @code { [Inject] public required DarkModeCookieService DarkModeCookieService { get; set; } [Inject] public required ITrackDataService TrackData { get; set; } [CascadingParameter] public IStreamingPlayerService? PlayerService { get; set; } // Elevation is vestigial under the frosted-glass design but kept on the parameter // surface so MainLayout's call site stays intact. [Parameter] public int Elevation { get; set; } [Parameter] public required bool IsDarkMode { get; set; } [Parameter] public required EventCallback IsDarkModeChanged { get; set; } private bool _mobileMenuOpen; private bool _streamLoading; private bool _findingTrack; private string? _streamMessage; private CancellationTokenSource? _messageCts; private const string EmptyLibraryMessage = "No tracks yet — check back soon."; private const string FetchFailedMessage = "Couldn't reach the library — try again."; private Task StreamNow() => StreamNowCore(closeMobileMenu: false); private Task StreamNowMobile() => StreamNowCore(closeMobileMenu: true); private async Task StreamNowCore(bool closeMobileMenu) { // Re-entrancy guard: the button is disabled while loading, but guard in code too so a // double-dispatch can never start two concurrent streams. if (_streamLoading) return; _streamLoading = true; _findingTrack = true; _streamMessage = null; // Warm the AudioContext FIRST, inside the gesture's call stack and before the network // await below. Safari only lets a suspended AudioContext resume while the originating // user gesture is still active; awaiting GetRandomTrack() first would consume the gesture // and leave playback silently refused. PlayerService is null only outside the // AudioPlayerProvider cascade (it should always be present in the public layout). var warmTask = PlayerService?.WarmAudioContext() ?? Task.CompletedTask; try { await warmTask; var result = await TrackData.GetRandomTrack(); if (!result.Success) { ShowTransientMessage(FetchFailedMessage); return; } if (result.Value is not { } track) { ShowTransientMessage(EmptyLibraryMessage); return; } if (closeMobileMenu) _mobileMenuOpen = false; // Track is found — flip only the label flag so the button reverts to // "Stream Now ▶" before the stream begins, while _streamLoading stays true // to keep the button disabled and the re-entrancy guard intact. _findingTrack = false; StateHasChanged(); if (PlayerService is not null) await PlayerService.SelectTrackStreaming(track); } catch (Exception) { ShowTransientMessage(FetchFailedMessage); } finally { _streamLoading = false; _findingTrack = false; } } private void ShowTransientMessage(string message) { _streamMessage = message; // Cancel any in-flight clear timer so the newest message gets its full display window. _messageCts?.Cancel(); _messageCts?.Dispose(); _messageCts = new CancellationTokenSource(); var token = _messageCts.Token; _ = ClearMessageAfterDelayAsync(token); } private async Task ClearMessageAfterDelayAsync(CancellationToken token) { try { await Task.Delay(TimeSpan.FromSeconds(4), token); } catch (TaskCanceledException) { return; } _streamMessage = null; await InvokeAsync(StateHasChanged); } public void Dispose() { _messageCts?.Cancel(); _messageCts?.Dispose(); } protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { // Runs here (not OnInitializedAsync) because OnAfterRenderAsync(firstRender) // is guaranteed not to execute during any SSR prerender pass. During prerender // (now interactive across all of Routes), awaiting IsDarkModeChanged.InvokeAsync // triggers a parent re-render cycle that cannot complete on the prerender renderer // and hangs the response. Server-side DarkModeService has already seeded // DarkModeSettings via PersistentComponentState, so the prerender paint is // already correct; this call reads that persisted value and propagates it to the // parent so the menu's IsDarkMode parameter stays consistent with DarkModeSettings. IsDarkMode = DarkModeCookieService.GetDarkMode(); await IsDarkModeChanged.InvokeAsync(IsDarkMode); } } private string NavClass => IsDarkMode ? "dd-nav dd-nav-dark" : "dd-nav dd-nav-light"; private string DarkLightModeIconSvg => IsDarkMode ? DDIcons.GasLampLit : DDIcons.GasLamp; private async Task DarkModeToggle() { IsDarkMode = !IsDarkMode; await DarkModeCookieService.SetDarkModeAsync(IsDarkMode); await IsDarkModeChanged.InvokeAsync(IsDarkMode); } private void ToggleMobileMenu() => _mobileMenuOpen = !_mobileMenuOpen; private void CloseMobileMenu() => _mobileMenuOpen = false; }