redesign(public): session detail as hero-overlay composition, NowPlaying-themed

This commit is contained in:
daniel-c-harvey
2026-06-15 19:27:12 -04:00
parent 61d53dacff
commit 3c7f28b2eb
2 changed files with 259 additions and 58 deletions
+94 -43
View File
@@ -1,5 +1,7 @@
@page "/sessions/{Id:long}"
@using DeepDrftModels.DTOs
@using DeepDrftPublic.Client.Controls
@using DeepDrftPublic.Client.Services
@inherits ReleaseDetailBase
<PageTitle>@(ViewModel.Release?.Title ?? "Session") - DeepDrft</PageTitle>
@@ -44,54 +46,103 @@ else
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))
<MudContainer MaxWidth="MaxWidth.Large" Class="session-detail-page">
<MudLink Href="/sessions" Typo="Typo.body2" Class="deepdrft-track-detail-back">
&larr; All sessions
</MudLink>
@* The hero is the positioning context for every overlay row; the gradient shim and the
top/bottom overlays are absolutely positioned children of this wrapper. *@
<div class="session-hero">
@if (!string.IsNullOrEmpty(heroImage))
{
<MudPaper Elevation="0" Square="true" Class="session-hero-img"
Style="@($"background-image: url('api/image/{Uri.EscapeDataString(heroImage)}');")" />
}
else
{
<MudPaper Elevation="0" Square="true" Class="session-hero-placeholder deepdrft-gradient-soft-secondary">
<MudIcon Icon="@Icons.Material.Filled.Piano" Color="Color.Primary" />
</MudPaper>
}
@* Darkening shim so overlaid text/controls stay legible over any image. *@
<div class="session-hero-shim"></div>
@* Top overlay: secondary details (genre, release date) and the share affordance. *@
<div class="session-hero-top">
<MudStack Row AlignItems="AlignItems.Center" Spacing="3" Class="session-hero-meta">
@if (hasGenre)
{
<MudChip T="string" Variant="Variant.Outlined" Class="session-overlay-chip">
@release.Genre
</MudChip>
}
@if (hasDate)
{
<div class="session-overlay-date">
<span class="session-overlay-label">Released</span>
<span class="session-overlay-value">@release.ReleaseDate!.Value.ToString("MMMM yyyy")</span>
</div>
}
</MudStack>
@if (ViewModel.Track is not null)
{
<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 class="session-hero-share">
<SharePopover EntryKey="@ViewModel.Track.EntryKey" />
</div>
}
</div>
@if (showCover)
{
<div class="session-detail-cover">
<MudPaper Elevation="2" Class="deepdrft-track-detail-cover-art"
Style="@($"background-image: url('api/image/{Uri.EscapeDataString(release.ImagePath!)}');")" />
</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>
@* Bottom overlay: cover thumbnail, title/artist, and the play affordance in one row. *@
<div class="session-hero-bottom">
<MudStack Row AlignItems="AlignItems.Center" Spacing="4" Class="session-hero-bottom-row">
@if (showCover)
{
<div class="session-cover-thumb">
<MudPaper Elevation="0" Square="true" Class="deepdrft-track-detail-cover-art"
Style="@($"background-image: url('api/image/{Uri.EscapeDataString(release.ImagePath!)}');")" />
</div>
}
<div class="session-hero-titles">
<div class="session-overlay-title">@release.Title</div>
<div class="session-overlay-artist">@release.Artist</div>
</div>
@if (ViewModel.Track is not null)
{
<div class="session-hero-play">
<PlayStateIcon Track="@ViewModel.Track" Size="Size.Large" Color="Color.Secondary" OnToggle="@PlayTrack" />
</div>
}
</MudStack>
</div>
</div>
</MudContainer>
}
@code {
protected override string PersistKey => "session-detail";
[CascadingParameter] public IStreamingPlayerService? PlayerService { get; set; }
// Mirrors the play-toggle wiring the shared scaffold owns. Session detail composes the player
// affordance directly (it diverges from ReleaseDetailScaffold for the overlay layout), so the
// toggle logic lives here: toggle if this track is already active, otherwise start a fresh stream.
private async Task PlayTrack()
{
var track = ViewModel.Track;
if (track is null || PlayerService is null) return;
var isThisTrack = PlayerService.CurrentTrack?.Id == track.Id;
if (isThisTrack && (PlayerService.IsPlaying || PlayerService.IsPaused))
{
await PlayerService.TogglePlayPause();
}
else
{
await PlayerService.SelectTrackStreaming(track);
}
}
}