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}"
@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);
}
}
}
@@ -1,24 +1,174 @@
/* 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 is hero-dominant: a large background image with the detail components overlaid
directly on top, themed to match the NowPlaying glassmorphic family. The page widens to the
Large container (set in markup) rather than the shared 760px detail column. */
::deep .session-detail-page {
padding-top: 2rem;
padding-bottom: 4rem;
}
/* session-detail-hero-img rides on MudPaper (child Razor component); ::deep pierces its output. */
::deep .session-detail-hero-img {
/* Positioning context for every overlay. A tall, dominant frame rather than the 16:9 strip. */
.session-hero {
position: relative;
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-position: center;
background-repeat: no-repeat;
}
/* Secondary medium square cover sitting below the hero — same 220px frame as mix detail,
smaller than the cut cover so the hero stays dominant. The cover-art MudPaper fills it. */
.session-detail-cover {
aspect-ratio: 1 / 1;
max-width: 220px;
margin: 0 auto 2rem;
overflow: hidden;
box-shadow: 0 8px 28px color-mix(in srgb, var(--mud-palette-text-secondary) 18%, transparent);
::deep .session-hero-placeholder {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--mud-palette-surface);
}
::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;
}
}