using DeepDrftModels.DTOs; 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 TrackDto? _selectedTrack = null; private IStreamingPlayerService? _subscribedService; private PersistingComponentStateSubscription _persistingSubscription; 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 Stop/Close buttons on the player bar reset the player directly, // bypassing PlayTrack — so the gallery's selection must follow player // state rather than only its own clicks. Subscribe to the multicast // side-channel (the cascade is IsFixed, so provider re-renders don't // reach us) and clear the highlight when nothing is loaded. if (PlayerService != null && !ReferenceEquals(PlayerService, _subscribedService)) { if (_subscribedService != null) _subscribedService.StateChanged -= OnPlayerStateChanged; PlayerService.StateChanged += OnPlayerStateChanged; _subscribedService = PlayerService; } } private void OnPlayerStateChanged() { // Sync the gallery selection to the player. When the player is no longer // loaded (stopped/closed/ended) drop the highlight; guard against a // redundant re-render when nothing actually changed. if (!PlayerService.IsLoaded && _selectedTrack != null) { _selectedTrack = null; 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) { if (track == null && _selectedTrack == null || track?.Id == _selectedTrack?.Id) return; if (track is null) { await PlayerService.Unload(); } else { await PlayerService.SelectTrack(track); } _selectedTrack = track; } public void Dispose() { _persistingSubscription.Dispose(); if (_subscribedService != null) { _subscribedService.StateChanged -= OnPlayerStateChanged; _subscribedService = null; } } }