using DeepDrftModels.DTOs; using DeepDrftPublic.Client.Controls; using DeepDrftPublic.Client.Services; using DeepDrftPublic.Client.ViewModels; using Microsoft.AspNetCore.Components; using Models.Common; namespace DeepDrftPublic.Client.Pages; public partial class TracksView : ComponentBase, IDisposable { private const string PersistKey = "tracks-page"; [Inject] public required TracksViewModel ViewModel { get; set; } [Inject] public required PersistentComponentState PersistentState { get; set; } [CascadingParameter] public required IStreamingPlayerService PlayerService { get; set; } private IStreamingPlayerService? _subscribedService; private PersistingComponentStateSubscription _persistingSubscription; // Ephemeral view-mode selection — presentation-only, not persisted across navigation. private GalleryViewMode _viewMode = GalleryViewMode.Grid; protected override async Task OnInitializedAsync() { // Carry the prerendered page across the prerender -> interactive (WASM) seam. // Without this, the WASM pass gets a fresh scoped ViewModel (Page == null), // re-renders the skeleton, re-fetches, and replaces the gallery DOM a few // seconds in — replaying TrackCard entrance animations. Mirror the dark-mode // PersistentComponentState bridge: persist on the way out of prerender, // restore on the interactive pass, and only fetch on a miss. _persistingSubscription = PersistentState.RegisterOnPersisting(PersistTracks); if (PersistentState.TryTakeFromJson>(PersistKey, out var restored) && restored is not null) { ViewModel.Page = restored; ViewModel.PageNumber = restored.Page; } else { await SetPage(ViewModel.PageNumber); } } protected override void OnParametersSet() { // The gallery's per-card icons read off the player's live state (CurrentTrack / // IsPlaying / IsPaused), which mutates outside this component's render path: // the player bar's play/pause/stop/close all change it directly. The cascade is // IsFixed, so the provider's re-render never reaches us — subscribe to the // multicast side-channel and re-render on every state change. if (PlayerService != null && !ReferenceEquals(PlayerService, _subscribedService)) { if (_subscribedService != null) _subscribedService.StateChanged -= OnPlayerStateChanged; PlayerService.StateChanged += OnPlayerStateChanged; _subscribedService = PlayerService; } } private void OnPlayerStateChanged() => InvokeAsync(StateHasChanged); private Task PersistTracks() { if (ViewModel.Page is not null) { PersistentState.PersistAsJson(PersistKey, ViewModel.Page); } return Task.CompletedTask; } private async Task SetPage(int newPage) { var result = await ViewModel.TrackData.GetPage(newPage, ViewModel.PageSize, ViewModel.SortBy, ViewModel.IsDescending); if (result is { Success: true, Value: PagedResult pageResult }) { ViewModel.Page = pageResult; ViewModel.PageSize = pageResult.PageSize; } } private async Task PlayTrack(TrackDto track) { // Resume the current track if it's merely paused; otherwise stream the new selection. if (PlayerService.CurrentTrack?.Id == track.Id && PlayerService.IsPaused) { await PlayerService.TogglePlayPause(); } else { await PlayerService.SelectTrack(track); } } private async Task PauseTrack(TrackDto track) { if (PlayerService.CurrentTrack?.Id == track.Id && PlayerService.IsPlaying) { await PlayerService.TogglePlayPause(); } } public void Dispose() { _persistingSubscription.Dispose(); if (_subscribedService != null) { _subscribedService.StateChanged -= OnPlayerStateChanged; _subscribedService = null; } } }