Phase 9 Wave 4: ARCHIVE nav + Cuts/Sessions/Mixes pages + MixWaveformVisualizer
Replaces flat RELEASES/SESSIONS/MIXES nav with ARCHIVE dropdown (PageRoute.Children,
one-level cap, dual-role node). Adds /archive overview, /cuts (AlbumsView + medium
filter; /albums redirects), /sessions + /sessions/{id} (hero-dominant), /mixes +
/mixes/{id} (MixWaveformVisualizer full-page background). Extracts ReleaseDetailScaffold
from TrackDetail (invariant trio). PersistentComponentState bridge on all new pages.
Click-to-seek seam designed on MixWaveformVisualizer (inert until wired).
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
@page "/albums"
|
||||
@inject NavigationManager Navigation
|
||||
|
||||
@* Cuts replaced the standalone /albums route in Phase 9. Old links keep working via a
|
||||
permanent redirect to /cuts rather than 404ing. *@
|
||||
|
||||
@code {
|
||||
protected override void OnInitialized()
|
||||
=> Navigation.NavigateTo("/cuts", forceLoad: false, replace: true);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
@page "/albums"
|
||||
@page "/cuts"
|
||||
|
||||
<PageTitle>DeepDrft Albums</PageTitle>
|
||||
<PageTitle>DeepDrft Cuts</PageTitle>
|
||||
|
||||
<div>
|
||||
<MudContainer MaxWidth="MaxWidth.Large" Class="albums-view-container">
|
||||
|
||||
@@ -1,26 +1,65 @@
|
||||
using DeepDrftModels.DTOs;
|
||||
using DeepDrftModels.Enums;
|
||||
using DeepDrftPublic.Client.Services;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Models.Common;
|
||||
|
||||
namespace DeepDrftPublic.Client.Pages;
|
||||
|
||||
public partial class AlbumsView : ComponentBase
|
||||
/// <summary>
|
||||
/// Medium-filtered release gallery. Routed at <c>/cuts</c> (Cut releases) and parameterized by
|
||||
/// <see cref="Medium"/> so the same component can back any medium's card grid without a fork.
|
||||
/// Cards open the track gallery filtered to that release's album title, preserving the original
|
||||
/// /albums ergonomics.
|
||||
/// </summary>
|
||||
public partial class AlbumsView : ComponentBase, IDisposable
|
||||
{
|
||||
[Inject] public required ITrackDataService TrackData { get; set; }
|
||||
private const string PersistKeyPrefix = "albums-view-";
|
||||
|
||||
[Inject] public required IReleaseDataService ReleaseData { get; set; }
|
||||
[Inject] public required PersistentComponentState PersistentState { get; set; }
|
||||
[Inject] public required NavigationManager Navigation { get; set; }
|
||||
|
||||
// The medium whose releases this grid shows. Defaults to Cut for the /cuts route; other media
|
||||
// can reuse this component by passing a different value. Drives both the fetch filter and the
|
||||
// per-medium persistence key so prerendered state never bleeds across media.
|
||||
[Parameter] public ReleaseMedium Medium { get; set; } = ReleaseMedium.Cut;
|
||||
|
||||
private bool _loading = true;
|
||||
private List<ReleaseDto> _albums = [];
|
||||
private PersistingComponentStateSubscription _persistingSubscription;
|
||||
|
||||
private string PersistKey => $"{PersistKeyPrefix}{Medium}";
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
var result = await TrackData.GetAlbums();
|
||||
if (result is { Success: true, Value: { } albums })
|
||||
_albums = albums;
|
||||
// Bridge the prerendered fetch across the prerender -> WASM seam (see TracksView). Without
|
||||
// this, the WASM pass re-fetches and replays the card entrance animations.
|
||||
_persistingSubscription = PersistentState.RegisterOnPersisting(PersistAlbums);
|
||||
|
||||
if (PersistentState.TryTakeFromJson<List<ReleaseDto>>(PersistKey, out var restored) && restored is not null)
|
||||
{
|
||||
_albums = restored;
|
||||
_loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var result = await ReleaseData.GetPaged(Medium.ToString().ToLowerInvariant(), page: 1, pageSize: 100);
|
||||
if (result is { Success: true, Value: { Items: { } items } })
|
||||
_albums = items.ToList();
|
||||
|
||||
_loading = false;
|
||||
}
|
||||
|
||||
private Task PersistAlbums()
|
||||
{
|
||||
if (_albums.Count > 0)
|
||||
PersistentState.PersistAsJson(PersistKey, _albums);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void OpenAlbum(string album)
|
||||
=> Navigation.NavigateTo($"/tracks?album={Uri.EscapeDataString(album)}");
|
||||
|
||||
public void Dispose() => _persistingSubscription.Dispose();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
@page "/archive"
|
||||
|
||||
<PageTitle>DeepDrft Archive</PageTitle>
|
||||
|
||||
<div>
|
||||
<MudContainer MaxWidth="MaxWidth.Large" Class="archive-view-container">
|
||||
<div class="archive-grid">
|
||||
@foreach (var medium in _media)
|
||||
{
|
||||
<a href="@medium.Route" class="archive-card-link">
|
||||
<div class="archive-card">
|
||||
<MudIcon Icon="@medium.Icon" Class="archive-card-icon" />
|
||||
<MudText Typo="Typo.h5" Class="archive-card-title">@medium.Title</MudText>
|
||||
<MudText Typo="Typo.body2" Class="archive-card-blurb">@medium.Blurb</MudText>
|
||||
</div>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</MudContainer>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private record MediumCard(string Title, string Blurb, string Route, string Icon);
|
||||
|
||||
private static readonly MediumCard[] _media =
|
||||
[
|
||||
new("Cuts", "Studio recordings — singles, EPs, and albums.", "/cuts", Icons.Material.Filled.Album),
|
||||
new("Sessions", "Single live takes, each with its own hero image.", "/sessions", Icons.Material.Filled.Piano),
|
||||
new("Mixes", "Long-form continuous mixes with high-resolution waveforms.", "/mixes", Icons.Material.Filled.GraphicEq),
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
.archive-view-container {
|
||||
padding-top: 16px;
|
||||
}
|
||||
|
||||
.archive-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||
gap: 2rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.archive-card-link {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.archive-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
gap: 0.75rem;
|
||||
padding: 2.5rem 1.5rem;
|
||||
border: 1px solid var(--mud-palette-lines-default);
|
||||
border-radius: 8px;
|
||||
background-color: var(--mud-palette-surface);
|
||||
transition: transform 120ms ease, box-shadow 120ms ease;
|
||||
}
|
||||
|
||||
.archive-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 28px color-mix(in srgb, var(--mud-palette-text-secondary) 18%, transparent);
|
||||
}
|
||||
|
||||
/* archive-card-icon rides on MudIcon (child Razor component); ::deep pierces its output. */
|
||||
::deep .archive-card-icon {
|
||||
font-size: 56px;
|
||||
color: var(--mud-palette-primary);
|
||||
}
|
||||
|
||||
/* archive-card-title / archive-card-blurb ride on MudText (child Razor component). */
|
||||
::deep .archive-card-title {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
::deep .archive-card-blurb {
|
||||
opacity: 0.7;
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using DeepDrftModels.DTOs;
|
||||
using DeepDrftModels.Enums;
|
||||
using DeepDrftPublic.Client.Services;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace DeepDrftPublic.Client.Pages;
|
||||
|
||||
/// <summary>
|
||||
/// Shared fetch + prerender-bridge logic for the medium browse pages (Sessions, Mixes). Subclasses
|
||||
/// supply only the <see cref="Medium"/> and <see cref="DetailRoute"/>; this base fetches the paged
|
||||
/// releases and bridges the prerendered result across the prerender -> WASM seam so the WASM pass
|
||||
/// does not re-fetch and replay the card animations (see the TracksView seam).
|
||||
/// </summary>
|
||||
public abstract class MediumBrowseBase : ComponentBase, IDisposable
|
||||
{
|
||||
[Inject] public required IReleaseDataService ReleaseData { get; set; }
|
||||
[Inject] public required PersistentComponentState PersistentState { get; set; }
|
||||
|
||||
/// <summary>The medium this page browses. Subclass-supplied constant.</summary>
|
||||
protected abstract ReleaseMedium Medium { get; }
|
||||
|
||||
protected bool Loading { get; private set; } = true;
|
||||
protected IReadOnlyList<ReleaseDto> Releases { get; private set; } = [];
|
||||
|
||||
private PersistingComponentStateSubscription _persistingSubscription;
|
||||
|
||||
private string PersistKey => $"medium-browse-{Medium}";
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
_persistingSubscription = PersistentState.RegisterOnPersisting(Persist);
|
||||
|
||||
if (PersistentState.TryTakeFromJson<List<ReleaseDto>>(PersistKey, out var restored) && restored is not null)
|
||||
{
|
||||
Releases = restored;
|
||||
Loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var result = await ReleaseData.GetPaged(Medium.ToString().ToLowerInvariant(), page: 1, pageSize: 100);
|
||||
if (result is { Success: true, Value: { Items: { } items } })
|
||||
Releases = items.ToList();
|
||||
|
||||
Loading = false;
|
||||
}
|
||||
|
||||
private Task Persist()
|
||||
{
|
||||
if (Releases.Count > 0)
|
||||
PersistentState.PersistAsJson(PersistKey, Releases.ToList());
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void Dispose() => _persistingSubscription.Dispose();
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
@page "/mixes/{Id:long}"
|
||||
@using DeepDrftPublic.Client.Controls
|
||||
@inherits ReleaseDetailBase
|
||||
|
||||
<PageTitle>@(ViewModel.Release?.Title ?? "Mix") - DeepDrft</PageTitle>
|
||||
|
||||
@if (ViewModel.IsLoading)
|
||||
{
|
||||
<div class="deepdrft-track-detail-container">
|
||||
<div class="deepdrft-track-detail-masthead">
|
||||
<MudSkeleton SkeletonType="SkeletonType.Text" Width="70%" Height="56px" />
|
||||
<MudSkeleton SkeletonType="SkeletonType.Text" Width="40%" Height="32px" />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else if (ViewModel.NotFound || ViewModel.Release is null)
|
||||
{
|
||||
<div class="deepdrft-track-detail-container">
|
||||
<div class="deepdrft-track-detail-masthead">
|
||||
<MudText Typo="Typo.h4" Align="Align.Center">Mix not found.</MudText>
|
||||
<div class="d-flex justify-center mt-4">
|
||||
<MudButton Href="/mixes"
|
||||
Variant="Variant.Text"
|
||||
StartIcon="@Icons.Material.Filled.ArrowBack">
|
||||
All mixes
|
||||
</MudButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
var release = ViewModel.Release;
|
||||
var hasGenre = release.Genre is not null;
|
||||
var hasDate = release.ReleaseDate is not null;
|
||||
|
||||
@* Full-page waveform sits behind the scaffold content. The scaffold's container is positioned
|
||||
above it via the mix-detail-foreground stacking context. *@
|
||||
<MixWaveformVisualizer ReleaseId="@release.Id" />
|
||||
|
||||
<div class="mix-detail-foreground">
|
||||
<ReleaseDetailScaffold Title="@release.Title"
|
||||
Artist="@release.Artist"
|
||||
Track="@ViewModel.Track"
|
||||
BackHref="/mixes"
|
||||
BackLabel="All mixes"
|
||||
ShowMeta="@(hasGenre || hasDate)">
|
||||
<MetaContent>
|
||||
@if (hasGenre)
|
||||
{
|
||||
<div>
|
||||
<MudChip T="string" Variant="Variant.Outlined" Color="Color.Tertiary" Class="deepdrft-genre-chip">
|
||||
@release.Genre
|
||||
</MudChip>
|
||||
</div>
|
||||
}
|
||||
@if (hasDate)
|
||||
{
|
||||
<div>
|
||||
<MudText Typo="Typo.overline">Released</MudText>
|
||||
<MudText Typo="Typo.body1">@release.ReleaseDate!.Value.ToString("MMMM yyyy")</MudText>
|
||||
</div>
|
||||
}
|
||||
</MetaContent>
|
||||
</ReleaseDetailScaffold>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
protected override string PersistKey => "mix-detail";
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
/* Lifts the detail content above the fixed waveform backdrop (z-index: 0). */
|
||||
.mix-detail-foreground {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
@page "/mixes"
|
||||
@using DeepDrftPublic.Client.Controls
|
||||
@inherits MediumBrowseBase
|
||||
|
||||
<PageTitle>DeepDrft Mixes</PageTitle>
|
||||
|
||||
<ReleaseGallery Releases="@Releases"
|
||||
Loading="@Loading"
|
||||
DetailRoute="mixes"
|
||||
EmptyMessage="No mixes yet" />
|
||||
|
||||
@code {
|
||||
protected override DeepDrftModels.Enums.ReleaseMedium Medium => DeepDrftModels.Enums.ReleaseMedium.Mix;
|
||||
protected string DetailRoute => "mixes";
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using DeepDrftModels.DTOs;
|
||||
using DeepDrftPublic.Client.ViewModels;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace DeepDrftPublic.Client.Pages;
|
||||
|
||||
/// <summary>
|
||||
/// Shared load + prerender-bridge logic for the single-release detail pages (Session, Mix).
|
||||
/// Subclasses supply only their markup; this base loads the release through
|
||||
/// <see cref="ReleaseDetailViewModel"/> and bridges the prerendered release across the prerender ->
|
||||
/// WASM seam so the WASM pass does not re-fetch (see the TracksView seam). The playable track is
|
||||
/// re-resolved on a restore miss only.
|
||||
/// </summary>
|
||||
public abstract class ReleaseDetailBase : ComponentBase, IDisposable
|
||||
{
|
||||
[Parameter] public long Id { get; set; }
|
||||
[Inject] public required ReleaseDetailViewModel ViewModel { get; set; }
|
||||
[Inject] public required PersistentComponentState PersistentState { get; set; }
|
||||
|
||||
private PersistingComponentStateSubscription _persistingSubscription;
|
||||
|
||||
// Distinct keys per medium so a Session restore never lands on a Mix page.
|
||||
protected abstract string PersistKey { get; }
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
_persistingSubscription = PersistentState.RegisterOnPersisting(Persist);
|
||||
|
||||
// The bridged payload carries both the release and its resolved track so the interactive
|
||||
// pass renders identically without a second round-trip.
|
||||
if (PersistentState.TryTakeFromJson<BridgedDetail>(PersistKey, out var restored) && restored?.Release is not null)
|
||||
{
|
||||
ViewModel.Restore(restored.Release, restored.Track);
|
||||
}
|
||||
else
|
||||
{
|
||||
await ViewModel.Load(Id);
|
||||
}
|
||||
}
|
||||
|
||||
private Task Persist()
|
||||
{
|
||||
if (ViewModel.Release is not null)
|
||||
PersistentState.PersistAsJson(PersistKey, new BridgedDetail(ViewModel.Release, ViewModel.Track));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void Dispose() => _persistingSubscription.Dispose();
|
||||
|
||||
// JSON-serializable bridge payload. Round-trips through PersistentComponentState's serializer.
|
||||
protected sealed record BridgedDetail(ReleaseDto Release, TrackDto? Track);
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
@page "/sessions/{Id:long}"
|
||||
@using DeepDrftPublic.Client.Controls
|
||||
@inherits ReleaseDetailBase
|
||||
|
||||
<PageTitle>@(ViewModel.Release?.Title ?? "Session") - DeepDrft</PageTitle>
|
||||
|
||||
@if (ViewModel.IsLoading)
|
||||
{
|
||||
<div class="deepdrft-track-detail-container">
|
||||
<div class="deepdrft-track-detail-cover">
|
||||
<MudSkeleton SkeletonType="SkeletonType.Rectangle" Width="100%" Height="320px" />
|
||||
</div>
|
||||
<div class="deepdrft-track-detail-masthead">
|
||||
<MudSkeleton SkeletonType="SkeletonType.Text" Width="70%" Height="56px" />
|
||||
<MudSkeleton SkeletonType="SkeletonType.Text" Width="40%" Height="32px" />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else if (ViewModel.NotFound || ViewModel.Release is null)
|
||||
{
|
||||
<div class="deepdrft-track-detail-container">
|
||||
<div class="deepdrft-track-detail-masthead">
|
||||
<MudText Typo="Typo.h4" Align="Align.Center">Session not found.</MudText>
|
||||
<div class="d-flex justify-center mt-4">
|
||||
<MudButton Href="/sessions"
|
||||
Variant="Variant.Text"
|
||||
StartIcon="@Icons.Material.Filled.ArrowBack">
|
||||
All sessions
|
||||
</MudButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
var release = ViewModel.Release;
|
||||
var heroKey = release.SessionMetadata?.HeroImageEntryKey;
|
||||
// Hero image precedence: the session's dedicated hero, then the release cover, then a placeholder.
|
||||
var heroImage = !string.IsNullOrEmpty(heroKey) ? heroKey : release.ImagePath;
|
||||
var hasGenre = release.Genre is not null;
|
||||
var hasDate = release.ReleaseDate is not null;
|
||||
|
||||
<ReleaseDetailScaffold Title="@release.Title"
|
||||
Artist="@release.Artist"
|
||||
Track="@ViewModel.Track"
|
||||
BackHref="/sessions"
|
||||
BackLabel="All sessions"
|
||||
ShowMeta="@(hasGenre || hasDate)">
|
||||
<Hero>
|
||||
<div class="session-detail-hero">
|
||||
@if (!string.IsNullOrEmpty(heroImage))
|
||||
{
|
||||
<MudPaper Elevation="2" Class="session-detail-hero-img"
|
||||
Style="@($"background-image: url('api/image/{Uri.EscapeDataString(heroImage)}');")" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudPaper Elevation="2" Class="deepdrft-track-detail-cover-placeholder deepdrft-gradient-soft-secondary">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Piano" Color="Color.Primary" />
|
||||
</MudPaper>
|
||||
}
|
||||
</div>
|
||||
</Hero>
|
||||
<MetaContent>
|
||||
@if (hasGenre)
|
||||
{
|
||||
<div>
|
||||
<MudChip T="string" Variant="Variant.Outlined" Color="Color.Tertiary" Class="deepdrft-genre-chip">
|
||||
@release.Genre
|
||||
</MudChip>
|
||||
</div>
|
||||
}
|
||||
@if (hasDate)
|
||||
{
|
||||
<div>
|
||||
<MudText Typo="Typo.overline">Released</MudText>
|
||||
<MudText Typo="Typo.body1">@release.ReleaseDate!.Value.ToString("MMMM yyyy")</MudText>
|
||||
</div>
|
||||
}
|
||||
</MetaContent>
|
||||
</ReleaseDetailScaffold>
|
||||
}
|
||||
|
||||
@code {
|
||||
protected override string PersistKey => "session-detail";
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/* Hero-dominant: a wide 16:9 image rather than the square cover used on track detail. */
|
||||
.session-detail-hero {
|
||||
margin: 0 auto 2rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* session-detail-hero-img rides on MudPaper (child Razor component); ::deep pierces its output. */
|
||||
::deep .session-detail-hero-img {
|
||||
width: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
@page "/sessions"
|
||||
@using DeepDrftPublic.Client.Controls
|
||||
@inherits MediumBrowseBase
|
||||
|
||||
<PageTitle>DeepDrft Sessions</PageTitle>
|
||||
|
||||
<ReleaseGallery Releases="@Releases"
|
||||
Loading="@Loading"
|
||||
DetailRoute="sessions"
|
||||
EmptyMessage="No sessions yet" />
|
||||
|
||||
@code {
|
||||
protected override DeepDrftModels.Enums.ReleaseMedium Medium => DeepDrftModels.Enums.ReleaseMedium.Session;
|
||||
protected string DetailRoute => "sessions";
|
||||
}
|
||||
@@ -40,50 +40,34 @@ else if (ViewModel.NotFound)
|
||||
else if (ViewModel.Track is not null)
|
||||
{
|
||||
var track = ViewModel.Track;
|
||||
var isThisTrackPlaying = PlayerService.CurrentTrack?.Id == track.Id
|
||||
&& PlayerService.IsPlaying
|
||||
&& !PlayerService.IsPaused;
|
||||
var release = track.Release;
|
||||
var hasMeta = release is not null
|
||||
&& (release.Title is not null || release.Genre is not null || release.ReleaseDate is not null);
|
||||
|
||||
<div class="deepdrft-track-detail-container">
|
||||
|
||||
<MudLink Href="/tracks" Typo="Typo.body2" Class="deepdrft-track-detail-back">
|
||||
← All tracks
|
||||
</MudLink>
|
||||
|
||||
<MudStack Row AlignItems="AlignItems.Start" Justify="Justify.SpaceBetween" Style="margin: 2rem 0 1.5rem;">
|
||||
<div class="deepdrft-track-detail-masthead">
|
||||
<MudText Typo="Typo.h3">@track.TrackName</MudText>
|
||||
<MudText Typo="Typo.h6" Color="Color.Primary">@release?.Artist</MudText>
|
||||
<ReleaseDetailScaffold Title="@track.TrackName"
|
||||
Artist="@release?.Artist"
|
||||
Track="@track"
|
||||
BackHref="/tracks"
|
||||
BackLabel="All tracks"
|
||||
ShowMeta="@hasMeta">
|
||||
<Hero>
|
||||
<div class="deepdrft-track-detail-cover">
|
||||
@if (!string.IsNullOrEmpty(release?.ImagePath))
|
||||
{
|
||||
<MudPaper Elevation="2" Class="deepdrft-track-detail-cover-art"
|
||||
Style="@($"background-image: url('api/image/{Uri.EscapeDataString(release.ImagePath)}');")" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudPaper Elevation="2" Class="deepdrft-track-detail-cover-placeholder deepdrft-gradient-soft-secondary">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Album" Color="Color.Primary" />
|
||||
</MudPaper>
|
||||
}
|
||||
</div>
|
||||
|
||||
<MudStack Row AlignItems="AlignItems.Center" Spacing="1">
|
||||
<SharePopover EntryKey="@track.EntryKey" />
|
||||
<PlayStateIcon Size="Size.Large" Color="Color.Secondary" OnToggle="@PlayTrack"/>
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
|
||||
<div class="deepdrft-track-detail-cover">
|
||||
@if (!string.IsNullOrEmpty(release?.ImagePath))
|
||||
</Hero>
|
||||
<MetaContent>
|
||||
@if (hasMeta)
|
||||
{
|
||||
<MudPaper Elevation="2" Class="deepdrft-track-detail-cover-art"
|
||||
Style="@($"background-image: url('api/image/{Uri.EscapeDataString(release.ImagePath)}');")" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudPaper Elevation="2" Class="deepdrft-track-detail-cover-placeholder deepdrft-gradient-soft-secondary">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Album" Color="Color.Primary" />
|
||||
</MudPaper>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (hasMeta)
|
||||
{
|
||||
<MudDivider />
|
||||
|
||||
<div class="deepdrft-track-detail-meta">
|
||||
@if (release?.Title is not null)
|
||||
{
|
||||
<div>
|
||||
@@ -111,8 +95,7 @@ else if (ViewModel.Track is not null)
|
||||
<MudText Typo="Typo.body1">@release.ReleaseDate.Value.ToString("MMMM yyyy")</MudText>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
</div>
|
||||
}
|
||||
</MetaContent>
|
||||
</ReleaseDetailScaffold>
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using DeepDrftModels.DTOs;
|
||||
using DeepDrftPublic.Client.Services;
|
||||
using DeepDrftPublic.Client.ViewModels;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
@@ -12,9 +11,7 @@ public partial class TrackDetail : ComponentBase, IDisposable
|
||||
[Parameter] public required string EntryKey { get; set; }
|
||||
[Inject] public required TrackDetailViewModel ViewModel { get; set; }
|
||||
[Inject] public required PersistentComponentState PersistentState { get; set; }
|
||||
[CascadingParameter] public required IStreamingPlayerService PlayerService { get; set; }
|
||||
|
||||
private IStreamingPlayerService? _subscribedService;
|
||||
private PersistingComponentStateSubscription _persistingSubscription;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
@@ -36,24 +33,6 @@ public partial class TrackDetail : ComponentBase, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
// The play button's icon reads off the player's live state (CurrentTrack /
|
||||
// IsPlaying / IsPaused), which mutates outside this component's render path.
|
||||
// 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 PersistTrack()
|
||||
{
|
||||
if (ViewModel.Track is not null)
|
||||
@@ -63,33 +42,5 @@ public partial class TrackDetail : ComponentBase, IDisposable
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task PlayTrack()
|
||||
{
|
||||
if (ViewModel.Track is null) return;
|
||||
|
||||
var isThisTrack = PlayerService.CurrentTrack?.Id == ViewModel.Track.Id;
|
||||
|
||||
// Toggle play/pause if this track is already the active one (playing or paused);
|
||||
// otherwise start a fresh stream. SelectTrackStreaming is the live entry point —
|
||||
// the buffered SelectTrack path is dead.
|
||||
if (isThisTrack && (PlayerService.IsPlaying || PlayerService.IsPaused))
|
||||
{
|
||||
await PlayerService.TogglePlayPause();
|
||||
}
|
||||
else
|
||||
{
|
||||
await PlayerService.SelectTrackStreaming(ViewModel.Track);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_persistingSubscription.Dispose();
|
||||
|
||||
if (_subscribedService != null)
|
||||
{
|
||||
_subscribedService.StateChanged -= OnPlayerStateChanged;
|
||||
_subscribedService = null;
|
||||
}
|
||||
}
|
||||
public void Dispose() => _persistingSubscription.Dispose();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user