Merge session-detail-hero-overlay into dev (Session detail hero-overlay redesign, NowPlaying-themed)

This commit is contained in:
daniel-c-harvey
2026-06-15 20:38:09 -04:00
2 changed files with 259 additions and 58 deletions
+94 -43
View File
@@ -1,5 +1,7 @@
@page "/sessions/{Id:long}" @page "/sessions/{Id:long}"
@using DeepDrftModels.DTOs
@using DeepDrftPublic.Client.Controls @using DeepDrftPublic.Client.Controls
@using DeepDrftPublic.Client.Services
@inherits ReleaseDetailBase @inherits ReleaseDetailBase
<PageTitle>@(ViewModel.Release?.Title ?? "Session") - DeepDrft</PageTitle> <PageTitle>@(ViewModel.Release?.Title ?? "Session") - DeepDrft</PageTitle>
@@ -44,54 +46,103 @@ else
var hasGenre = release.Genre is not null; var hasGenre = release.Genre is not null;
var hasDate = release.ReleaseDate is not null; var hasDate = release.ReleaseDate is not null;
<ReleaseDetailScaffold Title="@release.Title" <MudContainer MaxWidth="MaxWidth.Large" Class="session-detail-page">
Artist="@release.Artist"
Track="@ViewModel.Track" <MudLink Href="/sessions" Typo="Typo.body2" Class="deepdrft-track-detail-back">
BackHref="/sessions" &larr; All sessions
BackLabel="All sessions" </MudLink>
ShowMeta="@(hasGenre || hasDate)">
<Hero> @* The hero is the positioning context for every overlay row; the gradient shim and the
<div class="session-detail-hero"> top/bottom overlays are absolutely positioned children of this wrapper. *@
@if (!string.IsNullOrEmpty(heroImage)) <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" <div class="session-hero-share">
Style="@($"background-image: url('api/image/{Uri.EscapeDataString(heroImage)}');")" /> <SharePopover EntryKey="@ViewModel.Track.EntryKey" />
} </div>
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> </div>
@if (showCover)
{ @* Bottom overlay: cover thumbnail, title/artist, and the play affordance in one row. *@
<div class="session-detail-cover"> <div class="session-hero-bottom">
<MudPaper Elevation="2" Class="deepdrft-track-detail-cover-art" <MudStack Row AlignItems="AlignItems.Center" Spacing="4" Class="session-hero-bottom-row">
Style="@($"background-image: url('api/image/{Uri.EscapeDataString(release.ImagePath!)}');")" /> @if (showCover)
</div> {
} <div class="session-cover-thumb">
</Hero> <MudPaper Elevation="0" Square="true" Class="deepdrft-track-detail-cover-art"
<MetaContent> Style="@($"background-image: url('api/image/{Uri.EscapeDataString(release.ImagePath!)}');")" />
@if (hasGenre) </div>
{ }
<div> <div class="session-hero-titles">
<MudChip T="string" Variant="Variant.Outlined" Color="Color.Tertiary" Class="deepdrft-genre-chip"> <div class="session-overlay-title">@release.Title</div>
@release.Genre <div class="session-overlay-artist">@release.Artist</div>
</MudChip> </div>
</div> @if (ViewModel.Track is not null)
} {
@if (hasDate) <div class="session-hero-play">
{ <PlayStateIcon Track="@ViewModel.Track" Size="Size.Large" Color="Color.Secondary" OnToggle="@PlayTrack" />
<div> </div>
<MudText Typo="Typo.overline">Released</MudText> }
<MudText Typo="Typo.body1">@release.ReleaseDate!.Value.ToString("MMMM yyyy")</MudText> </MudStack>
</div> </div>
} </div>
</MetaContent>
</ReleaseDetailScaffold> </MudContainer>
} }
@code { @code {
protected override string PersistKey => "session-detail"; 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);
}
}
} }
@@ -1,24 +1,174 @@
/* Hero-dominant: a wide 16:9 image rather than the square cover used on track detail. */ /* Session detail is hero-dominant: a large background image with the detail components overlaid
.session-detail-hero { directly on top, themed to match the NowPlaying glassmorphic family. The page widens to the
margin: 0 auto 2rem; Large container (set in markup) rather than the shared 760px detail column. */
overflow: hidden;
::deep .session-detail-page {
padding-top: 2rem;
padding-bottom: 4rem;
} }
/* session-detail-hero-img rides on MudPaper (child Razor component); ::deep pierces its output. */ /* Positioning context for every overlay. A tall, dominant frame rather than the 16:9 strip. */
::deep .session-detail-hero-img { .session-hero {
position: relative;
width: 100%; width: 100%;
aspect-ratio: 16 / 9; aspect-ratio: 16 / 10;
max-height: 70vh;
min-height: 420px;
margin-top: 1rem;
overflow: hidden;
border-radius: 8px;
box-shadow: 0 12px 40px color-mix(in srgb, var(--mud-palette-text-secondary) 22%, transparent);
}
/* session-hero-img / session-hero-placeholder ride on MudPaper (child Razor component);
the class lands on MudPaper's native .mud-paper output, so ::deep pierces the component boundary. */
::deep .session-hero-img {
position: absolute;
inset: 0;
background-size: cover; background-size: cover;
background-position: center; background-position: center;
background-repeat: no-repeat; background-repeat: no-repeat;
} }
/* Secondary medium square cover sitting below the hero — same 220px frame as mix detail, ::deep .session-hero-placeholder {
smaller than the cut cover so the hero stays dominant. The cover-art MudPaper fills it. */ position: absolute;
.session-detail-cover { inset: 0;
aspect-ratio: 1 / 1; display: flex;
max-width: 220px; align-items: center;
margin: 0 auto 2rem; justify-content: center;
overflow: hidden; background-color: var(--mud-palette-surface);
box-shadow: 0 8px 28px color-mix(in srgb, var(--mud-palette-text-secondary) 18%, transparent); }
::deep .session-hero-placeholder .mud-icon-root {
font-size: 120px;
}
/* Darkening gradient shim: stronger at the bottom (under the title/play row) and lighter toward
the middle, with a top darken so the genre/share overlay stays legible too. */
.session-hero-shim {
position: absolute;
inset: 0;
pointer-events: none;
background:
linear-gradient(to bottom,
rgba(0, 0, 0, 0.55) 0%,
rgba(0, 0, 0, 0.15) 28%,
rgba(0, 0, 0, 0.15) 55%,
rgba(0, 0, 0, 0.75) 100%);
}
/* --- Top overlay: genre / date / share --- */
.session-hero-top {
position: absolute;
top: 0;
left: 0;
right: 0;
z-index: 2;
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 1rem;
padding: 1.25rem 1.5rem;
}
.session-overlay-date {
display: flex;
flex-direction: column;
gap: 0.1rem;
}
.session-overlay-label {
font-family: var(--deepdrft-font-mono);
font-size: 0.6rem;
letter-spacing: 0.25em;
text-transform: uppercase;
color: var(--deepdrft-green-accent);
}
.session-overlay-value {
font-family: var(--deepdrft-font-body);
font-size: 0.8rem;
color: var(--deepdrft-white);
}
/* Genre chip themed to the glassmorphic NowPlaying surface. The class lands on MudChip's native
.mud-chip output, so ::deep is required to reach it. */
::deep .session-overlay-chip.mud-chip {
background: rgba(250, 250, 248, 0.06);
border: 1px solid rgba(250, 250, 248, 0.12);
backdrop-filter: blur(8px);
color: var(--deepdrft-white);
font-family: var(--deepdrft-font-mono);
letter-spacing: 0.12em;
}
/* --- Bottom overlay: cover thumb / title / play --- */
.session-hero-bottom {
position: absolute;
bottom: 0;
left: 0;
right: 0;
z-index: 2;
padding: 1.5rem;
}
.session-cover-thumb {
flex: 0 0 auto;
width: 96px;
height: 96px;
overflow: hidden;
border: 1px solid rgba(250, 250, 248, 0.12);
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.45);
}
.session-hero-titles {
flex: 1 1 auto;
min-width: 0;
}
.session-overlay-title {
font-family: var(--deepdrft-font-display);
font-size: clamp(1.75rem, 4vw, 2.75rem);
font-weight: 400;
line-height: 1.1;
color: var(--deepdrft-white);
text-shadow: 0 2px 12px rgba(0, 0, 0, 0.6);
}
.session-overlay-artist {
font-family: var(--deepdrft-font-body);
font-size: 0.85rem;
letter-spacing: 0.08em;
color: rgba(250, 250, 248, 0.7);
margin-top: 0.35rem;
}
.session-hero-play {
flex: 0 0 auto;
}
/* The play affordance and share button sit over a dark image — force their icon glyphs to the
light theme color regardless of MudBlazor's Secondary palette. Both PlayStateIcon and
SharePopover render MudIconButton / MudProgressCircular internals, so ::deep is required. */
::deep .session-hero-play .mud-icon-button,
::deep .session-hero-play .mud-progress-circular,
::deep .session-hero-share .mud-icon-button {
color: var(--deepdrft-white);
}
@media (max-width: 599.98px) {
.session-hero {
aspect-ratio: 3 / 4;
min-height: 380px;
}
/* session-hero-bottom-row rides on MudStack's native output div, so ::deep is required. */
::deep .session-hero-bottom-row {
flex-wrap: wrap;
}
.session-cover-thumb {
width: 72px;
height: 72px;
}
} }