Files
deepdrft/DeepDrftPublic.Client/Pages/CutDetail.razor
T
daniel-c-harvey 6ac943ca09 feat(cuts): wire PlayAlbum/PlayTrack to IQueueService.PlayRelease (§3.4 seam, P11 W1)
Header Play loads full album at index 0; row play loads at that row's index with same-track
toggle preserved; null-safe cascade fallback to direct SelectTrackStreaming when queue absent.
2026-06-16 10:22:59 -04:00

174 lines
7.1 KiB
Plaintext

@page "/cuts/{Id:long}"
@using DeepDrftModels.DTOs
@using DeepDrftPublic.Client.Controls
@using DeepDrftPublic.Client.Services
@inherits CutDetailBase
<PageTitle>@(ViewModel.Release?.Title ?? "Cut") - 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">Cut not found.</MudText>
<div class="d-flex justify-center mt-4">
<MudButton Href="/cuts"
Variant="Variant.Text"
StartIcon="@Icons.Material.Filled.ArrowBack">
All cuts
</MudButton>
</div>
</div>
</div>
}
else
{
var release = ViewModel.Release;
var hasGenre = release.Genre is not null;
var hasYear = release.ReleaseDate is not null;
var firstTrack = ViewModel.Tracks.Count > 0 ? ViewModel.Tracks[0] : null;
<ReleaseDetailScaffold Title="@release.Title"
Artist="@release.Artist"
Track="@firstTrack"
BackHref="/cuts"
BackLabel="All cuts"
ShowShareRow="false">
<Header>
@* Header split: meta + Play/Share on the LEFT, bordered cover on the RIGHT (spec §3.1). *@
<div class="cut-detail-header">
<div class="cut-detail-meta">
<MudText Typo="Typo.h3">@release.Title</MudText>
<MudText Typo="Typo.h6" Color="Color.Primary">@release.Artist</MudText>
@if (hasGenre || hasYear)
{
<div class="cut-detail-subline">
@if (hasGenre)
{
<span class="cut-detail-genre">@release.Genre</span>
}
@if (hasGenre && hasYear)
{
<span class="cut-detail-sep">&middot;</span>
}
@if (hasYear)
{
<span class="cut-detail-year">@release.ReleaseDate!.Value.Year</span>
}
</div>
}
<div class="cut-detail-actions">
@* Header Play loads the full album into the queue at index 0 (§3.4 seam,
closed P11 W1). Disabled until at least one streamable track is resolved. *@
<MudButton Variant="Variant.Filled"
Color="Color.Secondary"
StartIcon="@Icons.Material.Filled.PlayArrow"
Disabled="@(firstTrack is null || !RendererInfo.IsInteractive)"
OnClick="@PlayAlbum">
Play
</MudButton>
@if (firstTrack is not null)
{
<SharePopover EntryKey="@firstTrack.EntryKey" />
}
</div>
</div>
<div class="cut-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>
</div>
</Header>
<BodyContent>
<MudDivider Class="cut-detail-divider" />
@if (ViewModel.Tracks.Count == 0)
{
<MudText Typo="Typo.body2" Class="cut-detail-empty">No tracks in this cut yet.</MudText>
}
else
{
<div class="cut-detail-tracklist">
@for (var i = 0; i < ViewModel.Tracks.Count; i++)
{
var track = ViewModel.Tracks[i];
var index = i;
<div class="cut-detail-track-row">
<span class="cut-detail-track-number">@track.TrackNumber</span>
<div class="cut-detail-track-play">
<PlayStateIcon Track="@track"
Size="Size.Medium"
Color="Color.Secondary"
OnToggle="@(() => PlayTrack(track, index))" />
</div>
<span class="cut-detail-track-name text-truncate">@track.TrackName</span>
</div>
}
</div>
}
</BodyContent>
</ReleaseDetailScaffold>
}
@code {
[CascadingParameter] public IStreamingPlayerService? PlayerService { get; set; }
[CascadingParameter] public IQueueService? Queue { get; set; }
// Header Play: load the full album into the queue starting at track 0.
private Task PlayAlbum()
{
if (ViewModel.Tracks.Count == 0) return Task.CompletedTask;
if (Queue is not null) return Queue.PlayRelease(ViewModel.Tracks, 0);
// Queue cascade absent (prerender or non-interactive): fall back to direct single-track play.
return PlayerService is not null
? PlayerService.SelectTrackStreaming(ViewModel.Tracks[0])
: Task.CompletedTask;
}
// Row play: toggle if this track is already playing/paused, otherwise load the album at this
// row's index so playback continues to the end from the chosen track.
private async Task PlayTrack(TrackDto track, int index)
{
if (PlayerService is null) return;
var isThisTrack = PlayerService.CurrentTrack?.Id == track.Id;
if (isThisTrack && (PlayerService.IsPlaying || PlayerService.IsPaused))
{
await PlayerService.TogglePlayPause();
return;
}
if (Queue is not null)
{
await Queue.PlayRelease(ViewModel.Tracks, index);
}
else
{
// Queue cascade absent: fall back to direct single-track play.
await PlayerService.SelectTrackStreaming(track);
}
}
}