feat(mix-detail): extract shared ReleaseHeroOverlay; Mix cover becomes overlaid 600px square hero (Direction B)
This commit is contained in:
@@ -25,27 +25,32 @@
|
|||||||
|
|
||||||
@* The header region. A composer that wants the default masthead+play row supplies nothing; one
|
@* The header region. A composer that wants the default masthead+play row supplies nothing; one
|
||||||
that needs a different arrangement (e.g. the Cut album's left-meta / right-cover split) supplies
|
that needs a different arrangement (e.g. the Cut album's left-meta / right-cover split) supplies
|
||||||
its own Header fragment. Layout variance rides this slot, never a boolean flag (Phase 9 §5.3). *@
|
its own Header fragment. Layout variance rides this slot, never a boolean flag (Phase 9 §5.3).
|
||||||
@if (Header is not null)
|
ShowHeader (a gate, not a layout flag) suppresses the region entirely for composers that carry
|
||||||
|
title/artist/play elsewhere — Mix overlays them on its hero. *@
|
||||||
|
@if (ShowHeader)
|
||||||
{
|
{
|
||||||
@Header
|
@if (Header is not null)
|
||||||
}
|
{
|
||||||
else
|
@Header
|
||||||
{
|
}
|
||||||
<MudStack Row AlignItems="AlignItems.Start" Justify="Justify.SpaceBetween" Style="margin: 2rem 0 1.5rem;">
|
else
|
||||||
<div class="deepdrft-track-detail-masthead">
|
{
|
||||||
<MudText Typo="Typo.h3">@Title</MudText>
|
<MudStack Row AlignItems="AlignItems.Start" Justify="Justify.SpaceBetween" Style="margin: 2rem 0 1.5rem;">
|
||||||
<MudText Typo="Typo.h6" Color="Color.Primary">@Artist</MudText>
|
<div class="deepdrft-track-detail-masthead">
|
||||||
</div>
|
<MudText Typo="Typo.h3">@Title</MudText>
|
||||||
|
<MudText Typo="Typo.h6" Color="Color.Primary">@Artist</MudText>
|
||||||
|
</div>
|
||||||
|
|
||||||
@* Play only makes sense once a playable track is resolved. *@
|
@* Play only makes sense once a playable track is resolved. *@
|
||||||
@if (Track is not null)
|
@if (Track is not null)
|
||||||
{
|
{
|
||||||
<MudStack Row AlignItems="AlignItems.Center" Spacing="1">
|
<MudStack Row AlignItems="AlignItems.Center" Spacing="1">
|
||||||
<PlayStateIcon Track="@Track" Size="Size.Large" Color="Color.Secondary" OnToggle="@PlayTrack" />
|
<PlayStateIcon Track="@Track" Size="Size.Large" Color="Color.Secondary" OnToggle="@PlayTrack" />
|
||||||
</MudStack>
|
</MudStack>
|
||||||
}
|
}
|
||||||
</MudStack>
|
</MudStack>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Hero
|
@Hero
|
||||||
|
|||||||
@@ -60,6 +60,14 @@ public partial class ReleaseDetailScaffold : ComponentBase
|
|||||||
/// <summary>Optional medium-specific metadata block, rendered under a divider when present.</summary>
|
/// <summary>Optional medium-specific metadata block, rendered under a divider when present.</summary>
|
||||||
[Parameter] public RenderFragment? MetaContent { get; set; }
|
[Parameter] public RenderFragment? MetaContent { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gate for the header region (masthead + play, or a custom <see cref="Header"/>). A composer that
|
||||||
|
/// carries title/artist/play elsewhere — e.g. Mix overlays them on its hero — sets this false to
|
||||||
|
/// suppress the duplicate. A gate, not a layout flag, so it is slot-consistent with
|
||||||
|
/// <see cref="ShowMeta"/> / <see cref="ShowShareRow"/> (Phase 9 §5.3). Defaults to shown.
|
||||||
|
/// </summary>
|
||||||
|
[Parameter] public bool ShowHeader { get; set; } = true;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gate for the metadata block. Lets a consumer supply a <see cref="MetaContent"/> fragment but
|
/// Gate for the metadata block. Lets a consumer supply a <see cref="MetaContent"/> fragment but
|
||||||
/// suppress the divider + block when its data is empty (slot fragments cannot be conditionally
|
/// suppress the divider + block when its data is empty (slot fragments cannot be conditionally
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
@namespace DeepDrftPublic.Client.Controls
|
||||||
|
|
||||||
|
@* Shared background-image hero with all release metadata overlaid: top row (genre/date + share),
|
||||||
|
bottom row (optional cover thumb + title/artist + play). Single source of truth for the overlay
|
||||||
|
composition consumed by both Session and Mix detail. Purely presentational — owns no data fetch
|
||||||
|
and no player wiring; play/share ride in as slots so each page keeps its own toggle. Per-page
|
||||||
|
aspect/sizing variance rides the Class parameter (e.g. Mix's square `mix-hero`), never a fork. *@
|
||||||
|
|
||||||
|
@{
|
||||||
|
var hasGenre = !string.IsNullOrEmpty(Genre);
|
||||||
|
var hasDate = ReleaseDate is not null;
|
||||||
|
// Show the cover thumbnail only when it differs from the hero background — otherwise it would
|
||||||
|
// duplicate the same image. Mix passes CoverThumbKey=null, so this is false there for free.
|
||||||
|
var showCover = !string.IsNullOrEmpty(CoverThumbKey) && CoverThumbKey != HeroImageKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
@* 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="release-hero @Class">
|
||||||
|
@if (!string.IsNullOrEmpty(HeroImageKey))
|
||||||
|
{
|
||||||
|
<div class="release-hero-img"
|
||||||
|
style="@($"background-image: url('api/image/{Uri.EscapeDataString(HeroImageKey)}');")"></div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="release-hero-placeholder deepdrft-gradient-soft-secondary">
|
||||||
|
<MudIcon Icon="@PlaceholderIcon" Color="Color.Primary" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@* Darkening shim so overlaid text/controls stay legible over any image. *@
|
||||||
|
<div class="release-hero-shim"></div>
|
||||||
|
|
||||||
|
@* Top overlay: secondary details (genre, release date) and the share affordance. *@
|
||||||
|
<div class="release-hero-top">
|
||||||
|
<MudStack Row AlignItems="AlignItems.Center" Spacing="3" Class="release-hero-meta">
|
||||||
|
@if (hasGenre)
|
||||||
|
{
|
||||||
|
<MudChip T="string" Variant="Variant.Outlined" Class="release-overlay-chip">
|
||||||
|
@Genre
|
||||||
|
</MudChip>
|
||||||
|
}
|
||||||
|
@if (hasDate)
|
||||||
|
{
|
||||||
|
<div class="release-overlay-date">
|
||||||
|
<span class="release-overlay-label">Released</span>
|
||||||
|
<span class="release-overlay-value">@ReleaseDate!.Value.ToString("MMMM yyyy")</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</MudStack>
|
||||||
|
@if (ShareContent is not null)
|
||||||
|
{
|
||||||
|
<div class="release-hero-share">
|
||||||
|
@ShareContent
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@* Bottom overlay: cover thumbnail, title/artist, and the play affordance in one row. *@
|
||||||
|
<div class="release-hero-bottom">
|
||||||
|
<MudStack Row AlignItems="AlignItems.Center" Spacing="4" Class="release-hero-bottom-row">
|
||||||
|
@if (showCover)
|
||||||
|
{
|
||||||
|
<div class="release-cover-thumb">
|
||||||
|
<div class="deepdrft-track-detail-cover-art"
|
||||||
|
style="@($"background-image: url('api/image/{Uri.EscapeDataString(CoverThumbKey!)}');")"></div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div class="release-hero-titles">
|
||||||
|
<div class="release-overlay-title">@Title</div>
|
||||||
|
<div class="release-overlay-artist">@Artist</div>
|
||||||
|
</div>
|
||||||
|
@if (PlayContent is not null)
|
||||||
|
{
|
||||||
|
<div class="release-hero-play">
|
||||||
|
@PlayContent
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</MudStack>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
/// <summary>Background image entry key. Null renders the placeholder treatment.</summary>
|
||||||
|
[Parameter] public string? HeroImageKey { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Material icon for the no-image placeholder (Session: Piano; Mix: Album).</summary>
|
||||||
|
[Parameter] public required string PlaceholderIcon { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Optional small cover thumbnail in the bottom row. Shown only when it differs from
|
||||||
|
/// <see cref="HeroImageKey"/> (otherwise it would duplicate the background). Mix passes null.
|
||||||
|
/// </summary>
|
||||||
|
[Parameter] public string? CoverThumbKey { get; set; }
|
||||||
|
|
||||||
|
[Parameter] public required string Title { get; set; }
|
||||||
|
[Parameter] public string? Artist { get; set; }
|
||||||
|
[Parameter] public string? Genre { get; set; }
|
||||||
|
[Parameter] public DateOnly? ReleaseDate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Share affordance slot — each page passes its own SharePopover with the right params.</summary>
|
||||||
|
[Parameter] public RenderFragment? ShareContent { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Play affordance slot — each page passes its PlayStateIcon wired to its own toggle.</summary>
|
||||||
|
[Parameter] public RenderFragment? PlayContent { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Extra class for per-page aspect/sizing variance (e.g. Mix's square `mix-hero`).</summary>
|
||||||
|
[Parameter] public string? Class { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
/* Background-image hero with the release detail overlaid directly on top, themed to match the
|
||||||
|
NowPlaying glassmorphic family. The overlay shell (.release-hero, shim, top/bottom rows) is
|
||||||
|
plain <div>s; per-page aspect/sizing variance rides an extra class (e.g. .mix-hero) layered on
|
||||||
|
.release-hero. The default aspect here is Sessions' wide hero. */
|
||||||
|
|
||||||
|
/* Positioning context for every overlay. A tall, dominant frame rather than the 16:9 strip. */
|
||||||
|
.release-hero {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The background-image surface and placeholder are plain <div>s, so no ::deep is needed here. */
|
||||||
|
.release-hero-img {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.release-hero-placeholder {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: var(--mud-palette-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
::deep .release-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. */
|
||||||
|
.release-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 --- */
|
||||||
|
.release-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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.release-overlay-date {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.release-overlay-label {
|
||||||
|
font-family: var(--deepdrft-font-mono);
|
||||||
|
font-size: 0.6rem;
|
||||||
|
letter-spacing: 0.25em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--deepdrft-green-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.release-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 .release-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 --- */
|
||||||
|
.release-hero-bottom {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 2;
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.release-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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The cover-thumb art surface is a plain <div>, so no ::deep is needed. */
|
||||||
|
.release-cover-thumb .deepdrft-track-detail-cover-art {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.release-hero-titles {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.release-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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.release-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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.release-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 .release-hero-play .mud-icon-button,
|
||||||
|
::deep .release-hero-play .mud-progress-circular,
|
||||||
|
::deep .release-hero-share .mud-icon-button {
|
||||||
|
color: var(--deepdrft-white);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 599.98px) {
|
||||||
|
.release-hero {
|
||||||
|
aspect-ratio: 3 / 4;
|
||||||
|
min-height: 380px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* release-hero-bottom-row rides on MudStack's native output div, so ::deep is required. */
|
||||||
|
::deep .release-hero-bottom-row {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.release-cover-thumb {
|
||||||
|
width: 72px;
|
||||||
|
height: 72px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
@page "/mixes/{EntryKey}"
|
@page "/mixes/{EntryKey}"
|
||||||
@using DeepDrftPublic.Client.Controls
|
@using DeepDrftPublic.Client.Controls
|
||||||
|
@using DeepDrftPublic.Client.Services
|
||||||
@inherits ReleaseDetailBase
|
@inherits ReleaseDetailBase
|
||||||
|
|
||||||
<PageTitle>@(ViewModel.Release?.Title ?? "Mix") - DeepDrft</PageTitle>
|
<PageTitle>@(ViewModel.Release?.Title ?? "Mix") - DeepDrft</PageTitle>
|
||||||
@@ -31,8 +32,6 @@ else if (ViewModel.NotFound || ViewModel.Release is null)
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
var release = ViewModel.Release;
|
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
|
@* Full-page waveform sits behind the scaffold content. The scaffold's container is positioned
|
||||||
above it via the mix-detail-foreground stacking context. TrackId lets the visualizer couple to
|
above it via the mix-detail-foreground stacking context. TrackId lets the visualizer couple to
|
||||||
@@ -41,12 +40,17 @@ else
|
|||||||
|
|
||||||
<div class="mix-detail-foreground">
|
<div class="mix-detail-foreground">
|
||||||
<MudContainer MaxWidth="MaxWidth.Large" Class="mix-detail-container">
|
<MudContainer MaxWidth="MaxWidth.Large" Class="mix-detail-container">
|
||||||
|
@* Mix keeps the scaffold solely for the Phase 10 top row (back link | controls | lava-lamp).
|
||||||
|
Title/artist/genre/date/share/play all move into the overlaid hero, so the scaffold's
|
||||||
|
default header and meta regions are suppressed (ShowHeader/ShowMeta=false) and the share
|
||||||
|
row stays off (ShowShareRow=false). *@
|
||||||
<ReleaseDetailScaffold Title="@release.Title"
|
<ReleaseDetailScaffold Title="@release.Title"
|
||||||
Artist="@release.Artist"
|
Artist="@release.Artist"
|
||||||
Track="@ViewModel.Track"
|
Track="@ViewModel.Track"
|
||||||
BackHref="/mixes"
|
BackHref="/mixes"
|
||||||
BackLabel="All mixes"
|
BackLabel="All mixes"
|
||||||
ShowMeta="@(hasGenre || hasDate)"
|
ShowHeader="false"
|
||||||
|
ShowMeta="false"
|
||||||
ShowShareRow="false">
|
ShowShareRow="false">
|
||||||
<TopContent>
|
<TopContent>
|
||||||
@* The seven-knob band lives in its own full-width area below the back/lamp top row.
|
@* The seven-knob band lives in its own full-width area below the back/lamp top row.
|
||||||
@@ -73,43 +77,28 @@ else
|
|||||||
</MudTooltip>
|
</MudTooltip>
|
||||||
</TopRightAction>
|
</TopRightAction>
|
||||||
<Hero>
|
<Hero>
|
||||||
<div class="mix-detail-cover">
|
@* Cover-as-background hero with all metadata overlaid, square `mix-hero` sizing. The
|
||||||
@if (!string.IsNullOrEmpty(release.ImagePath))
|
cover art IS the background, so no separate cover thumbnail (CoverThumbKey defaults
|
||||||
{
|
to null). Share and play ride in as slots, matching Sessions. *@
|
||||||
<MudPaper Elevation="2" Class="deepdrft-track-detail-cover-art"
|
<ReleaseHeroOverlay Class="mix-hero"
|
||||||
Style="@($"background-image: url('api/image/{Uri.EscapeDataString(release.ImagePath)}');")" />
|
HeroImageKey="@release.ImagePath"
|
||||||
}
|
PlaceholderIcon="@Icons.Material.Filled.Album"
|
||||||
else
|
Title="@release.Title"
|
||||||
{
|
Artist="@release.Artist"
|
||||||
<MudPaper Elevation="2" Class="deepdrft-track-detail-cover-placeholder deepdrft-gradient-soft-secondary">
|
Genre="@release.Genre"
|
||||||
<MudIcon Icon="@Icons.Material.Filled.Album" Color="Color.Primary" />
|
ReleaseDate="@release.ReleaseDate">
|
||||||
</MudPaper>
|
<ShareContent>
|
||||||
}
|
@* Release-mode share: copies the canonical /mixes/{entryKey} URL, not a single track (§3b). *@
|
||||||
</div>
|
<SharePopover ReleaseEntryKey="@release.EntryKey" ReleaseMedium="@release.Medium" />
|
||||||
|
</ShareContent>
|
||||||
|
<PlayContent>
|
||||||
|
@if (ViewModel.Track is not null)
|
||||||
|
{
|
||||||
|
<PlayStateIcon Track="@ViewModel.Track" Size="Size.Large" Color="Color.Secondary" OnToggle="@PlayTrack" />
|
||||||
|
}
|
||||||
|
</PlayContent>
|
||||||
|
</ReleaseHeroOverlay>
|
||||||
</Hero>
|
</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>
|
|
||||||
<BodyContent>
|
|
||||||
@* Release-mode share: copies the canonical /mixes/{entryKey} URL, not a single track (§3b). *@
|
|
||||||
<div class="deepdrft-share-row">
|
|
||||||
<SharePopover ReleaseEntryKey="@release.EntryKey" ReleaseMedium="@release.Medium" />
|
|
||||||
</div>
|
|
||||||
</BodyContent>
|
|
||||||
</ReleaseDetailScaffold>
|
</ReleaseDetailScaffold>
|
||||||
</MudContainer>
|
</MudContainer>
|
||||||
</div>
|
</div>
|
||||||
@@ -118,6 +107,27 @@ else
|
|||||||
@code {
|
@code {
|
||||||
protected override string PersistKey => "mix-detail";
|
protected override string PersistKey => "mix-detail";
|
||||||
|
|
||||||
|
[CascadingParameter] public IStreamingPlayerService? PlayerService { get; set; }
|
||||||
|
|
||||||
|
// The hero now carries the play affordance (the scaffold's header is suppressed), so the
|
||||||
|
// play-toggle is wired here directly — mirroring SessionDetail. 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Lava-lamp knob-band visibility. Pure presentation over MixVisualizerControlState — gates whether
|
// Lava-lamp knob-band visibility. Pure presentation over MixVisualizerControlState — gates whether
|
||||||
// the seven-knob MixVisualizerControls is rendered into the TopContent band; toggling it touches no
|
// the seven-knob MixVisualizerControls is rendered into the TopContent band; toggling it touches no
|
||||||
// control value or bridge push. The lava-lamp button's filled/outline glyph is driven off this flag.
|
// control value or bridge push. The lava-lamp button's filled/outline glyph is driven off this flag.
|
||||||
|
|||||||
@@ -4,12 +4,14 @@
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Medium square cover — deliberately smaller than the 360px cut cover so the
|
/* Mix's per-page hero variance: a centered medium square, overriding ReleaseHeroOverlay's default
|
||||||
waveform backdrop keeps room. The placeholder/art MudPaper fills this frame. */
|
wide (16/10) aspect. The cover art is square album art, and a smaller square frees the surrounding
|
||||||
.mix-detail-cover {
|
canvas for the lava-lamp visualizer. The mix-hero class lands on the overlay component's root
|
||||||
|
.release-hero <div> (a child Razor component's native output), so ::deep is required to reach it. */
|
||||||
|
::deep .release-hero.mix-hero {
|
||||||
aspect-ratio: 1 / 1;
|
aspect-ratio: 1 / 1;
|
||||||
max-width: 220px;
|
max-width: 600px;
|
||||||
margin: 0 auto 2rem;
|
min-height: 0;
|
||||||
overflow: hidden;
|
max-height: none;
|
||||||
box-shadow: 0 8px 28px color-mix(in srgb, var(--mud-palette-text-secondary) 18%, transparent);
|
margin-inline: auto;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,12 +39,6 @@ else
|
|||||||
var heroKey = release.SessionMetadata?.HeroImageEntryKey;
|
var heroKey = release.SessionMetadata?.HeroImageEntryKey;
|
||||||
// Hero image precedence: the session's dedicated hero, then the release cover, then a placeholder.
|
// Hero image precedence: the session's dedicated hero, then the release cover, then a placeholder.
|
||||||
var heroImage = !string.IsNullOrEmpty(heroKey) ? heroKey : release.ImagePath;
|
var heroImage = !string.IsNullOrEmpty(heroKey) ? heroKey : release.ImagePath;
|
||||||
// Show the cover thumbnail only when it differs from what the hero displays. When there is no
|
|
||||||
// dedicated hero, the hero already falls back to release.ImagePath — rendering the cover too
|
|
||||||
// would duplicate the same image.
|
|
||||||
var showCover = !string.IsNullOrEmpty(release.ImagePath) && release.ImagePath != heroImage;
|
|
||||||
var hasGenre = release.Genre is not null;
|
|
||||||
var hasDate = release.ReleaseDate is not null;
|
|
||||||
|
|
||||||
<MudContainer MaxWidth="MaxWidth.Large" Class="session-detail-page">
|
<MudContainer MaxWidth="MaxWidth.Large" Class="session-detail-page">
|
||||||
|
|
||||||
@@ -52,70 +46,27 @@ else
|
|||||||
← All sessions
|
← All sessions
|
||||||
</MudLink>
|
</MudLink>
|
||||||
|
|
||||||
@* The hero is the positioning context for every overlay row; the gradient shim and the
|
@* The overlay shows the cover thumbnail only when it differs from the resolved hero image —
|
||||||
top/bottom overlays are absolutely positioned children of this wrapper. *@
|
when there is no dedicated hero, heroImage already falls back to release.ImagePath, so the
|
||||||
<div class="session-hero">
|
thumb would duplicate the background. That logic lives in ReleaseHeroOverlay. *@
|
||||||
@if (!string.IsNullOrEmpty(heroImage))
|
<ReleaseHeroOverlay HeroImageKey="@heroImage"
|
||||||
{
|
CoverThumbKey="@release.ImagePath"
|
||||||
<MudPaper Elevation="0" Square="true" Class="session-hero-img"
|
PlaceholderIcon="@Icons.Material.Filled.Piano"
|
||||||
Style="@($"background-image: url('api/image/{Uri.EscapeDataString(heroImage)}');")" />
|
Title="@release.Title"
|
||||||
}
|
Artist="@release.Artist"
|
||||||
else
|
Genre="@release.Genre"
|
||||||
{
|
ReleaseDate="@release.ReleaseDate">
|
||||||
<MudPaper Elevation="0" Square="true" Class="session-hero-placeholder deepdrft-gradient-soft-secondary">
|
<ShareContent>
|
||||||
<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>
|
|
||||||
@* Release-mode share: copies the canonical /sessions/{entryKey} URL, not a single track (§3b). *@
|
@* Release-mode share: copies the canonical /sessions/{entryKey} URL, not a single track (§3b). *@
|
||||||
<div class="session-hero-share">
|
<SharePopover ReleaseEntryKey="@release.EntryKey" ReleaseMedium="@release.Medium" />
|
||||||
<SharePopover ReleaseEntryKey="@release.EntryKey" ReleaseMedium="@release.Medium" />
|
</ShareContent>
|
||||||
</div>
|
<PlayContent>
|
||||||
</div>
|
@if (ViewModel.Track is not null)
|
||||||
|
{
|
||||||
@* Bottom overlay: cover thumbnail, title/artist, and the play affordance in one row. *@
|
<PlayStateIcon Track="@ViewModel.Track" Size="Size.Large" Color="Color.Secondary" OnToggle="@PlayTrack" />
|
||||||
<div class="session-hero-bottom">
|
}
|
||||||
<MudStack Row AlignItems="AlignItems.Center" Spacing="4" Class="session-hero-bottom-row">
|
</PlayContent>
|
||||||
@if (showCover)
|
</ReleaseHeroOverlay>
|
||||||
{
|
|
||||||
<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>
|
</MudContainer>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,174 +1,8 @@
|
|||||||
/* Session detail is hero-dominant: a large background image with the detail components overlaid
|
/* 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
|
directly on top (the overlay composition lives in ReleaseHeroOverlay). The page widens to the
|
||||||
Large container (set in markup) rather than the shared 760px detail column. */
|
Large container (set in markup) rather than the shared 760px detail column. */
|
||||||
|
|
||||||
::deep .session-detail-page {
|
::deep .session-detail-page {
|
||||||
padding-top: 2rem;
|
padding-top: 2rem;
|
||||||
padding-bottom: 4rem;
|
padding-bottom: 4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Positioning context for every overlay. A tall, dominant frame rather than the 16:9 strip. */
|
|
||||||
.session-hero {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
::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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user