Merge Theater Mode refinements (Phase 20 Wave 2) into dev
This commit is contained in:
@@ -10,13 +10,20 @@ else
|
|||||||
<MudContainer MaxWidth="MaxWidth.Large" Class="player-inner-container">
|
<MudContainer MaxWidth="MaxWidth.Large" Class="player-inner-container">
|
||||||
<MudPaper Elevation="8" Class="player-surface pa-3">
|
<MudPaper Elevation="8" Class="player-surface pa-3">
|
||||||
|
|
||||||
@* Theater Mode "now showing" band (Phase 20 §5/§7). Shown only when Theater is ON and a
|
@* Theater Mode "now showing" band (Phase 20 §5/§7, Wave 2 §2). Keyed off the playing
|
||||||
release is playing — keyed off the playing track's Release, not off any detail page
|
track's Release, not off any detail page (the bar reaches into no page; §6). The release
|
||||||
(the bar reaches into no page; §6). The release page is hidden in Theater Mode, so the
|
page is hidden in Theater Mode, so the bar carries its identity: cover, linked title,
|
||||||
bar carries its identity: cover, linked title, release share. *@
|
release share. The band stays mounted whenever a release is playing and eases in/out via
|
||||||
@if (VisualizerControlState.TheaterMode && CurrentTrack?.Release is not null)
|
the shared .dd-theater-collapsible wrapper — collapsed (zero height, faded) unless
|
||||||
|
Theater is ON — so the bar grows/shrinks smoothly instead of popping. *@
|
||||||
|
@if (CurrentTrack?.Release is not null)
|
||||||
{
|
{
|
||||||
<NowShowingPanel Release="CurrentTrack.Release" />
|
var nowShowing = VisualizerControlState.TheaterMode;
|
||||||
|
<div class="dd-theater-collapsible @(nowShowing ? null : "dd-theater-collapsed")">
|
||||||
|
<div class="dd-theater-collapsible-inner">
|
||||||
|
<NowShowingPanel Release="CurrentTrack.Release" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="player-layout">
|
<div class="player-layout">
|
||||||
|
|||||||
@@ -10,11 +10,13 @@
|
|||||||
reaches into no page and no bar — single source, multiple observers (§6).
|
reaches into no page and no bar — single source, multiple observers (§6).
|
||||||
|
|
||||||
Visible only when the lava OR waveform subsystem is on — there is nothing to go to theater FOR if both
|
Visible only when the lava OR waveform subsystem is on — there is nothing to go to theater FOR if both
|
||||||
are off (§3.2). Disabled until interactive (§3.4), the same prerender guard the lava/Play buttons use.
|
are off (§3.2) — AND when <see cref="Available"/> is true. The page supplies Available so the toggle
|
||||||
Active visual state when Theater is ON. .dd-accent-icon gives the green-accent glyph in both themes
|
only appears when this page's release is the one playing (Phase 20 Wave 2 §3): the toggle owns the
|
||||||
with zero new CSS (§8) — same treatment as the lava-lamp trigger it sits beside. *@
|
subsystem gate; the page owns the release-playing predicate. Disabled until interactive (§3.4), the
|
||||||
|
same prerender guard the lava/Play buttons use. Active visual state when Theater is ON. .dd-accent-icon
|
||||||
|
gives the green-accent glyph in both themes with zero new CSS (§8) — same as the lava-lamp trigger. *@
|
||||||
|
|
||||||
@if (State.LavaEnabled || State.WaveformEnabled)
|
@if (Available && (State.LavaEnabled || State.WaveformEnabled))
|
||||||
{
|
{
|
||||||
<div class="dd-accent-icon">
|
<div class="dd-accent-icon">
|
||||||
<MudTooltip Text="@(State.TheaterMode ? "Exit theater mode" : "Theater mode")">
|
<MudTooltip Text="@(State.TheaterMode ? "Exit theater mode" : "Theater mode")">
|
||||||
@@ -33,6 +35,14 @@
|
|||||||
/// <summary>Trigger-icon size. Defaults Large to match the lava-lamp popover trigger it sits beside.</summary>
|
/// <summary>Trigger-icon size. Defaults Large to match the lava-lamp popover trigger it sits beside.</summary>
|
||||||
[Parameter] public Size IconSize { get; set; } = Size.Large;
|
[Parameter] public Size IconSize { get; set; } = Size.Large;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the toggle is available on this surface (Phase 20 Wave 2 §3). The page passes the
|
||||||
|
/// "this release is the one playing" predicate here; Theater Mode only applies to the playing
|
||||||
|
/// release, so a detail page whose release is not playing passes <c>false</c> and shows no toggle.
|
||||||
|
/// Defaults <c>true</c> so surfaces with no release-scoping (none today) keep the subsystem-only gate.
|
||||||
|
/// </summary>
|
||||||
|
[Parameter] public bool Available { get; set; } = true;
|
||||||
|
|
||||||
protected override void OnInitialized() => State.Changed += OnStateChanged;
|
protected override void OnInitialized() => State.Changed += OnStateChanged;
|
||||||
|
|
||||||
// The toggle's own visibility and active state both key off State, which another observer (or this
|
// The toggle's own visibility and active state both key off State, which another observer (or this
|
||||||
|
|||||||
@@ -37,6 +37,10 @@ else
|
|||||||
var hasYear = release.ReleaseDate is not null;
|
var hasYear = release.ReleaseDate is not null;
|
||||||
var firstTrack = ViewModel.Tracks.Count > 0 ? ViewModel.Tracks[0] : null;
|
var firstTrack = ViewModel.Tracks.Count > 0 ? ViewModel.Tracks[0] : null;
|
||||||
|
|
||||||
|
@* Full-screen content body (Phase 20 Wave 2 §1): the scaffold has no Class param, so a thin wrapper
|
||||||
|
carries the min-height. dd-detail-fill keeps the body >= viewport height (below the nav) so the
|
||||||
|
ambient visualizer reads full-screen and the site footer is pushed below the fold. *@
|
||||||
|
<div class="dd-detail-fill">
|
||||||
<ReleaseDetailScaffold Title="@release.Title"
|
<ReleaseDetailScaffold Title="@release.Title"
|
||||||
Artist="@release.Artist"
|
Artist="@release.Artist"
|
||||||
Track="@firstTrack"
|
Track="@firstTrack"
|
||||||
@@ -58,17 +62,20 @@ else
|
|||||||
controls over the experience, not release content, so both stay in Theater Mode (§4/OQ4).
|
controls over the experience, not release content, so both stay in Theater Mode (§4/OQ4).
|
||||||
Wrapped so they cluster on the right rather than spreading across the SpaceBetween row. *@
|
Wrapped so they cluster on the right rather than spreading across the SpaceBetween row. *@
|
||||||
<div class="dd-detail-top-actions">
|
<div class="dd-detail-top-actions">
|
||||||
<TheaterModeToggle />
|
@* Theater toggle only appears when this Cut is the currently-playing release (Phase 20
|
||||||
|
Wave 2 §3). ShowTheaterToggle folds in the subsystem gate + the release-playing check. *@
|
||||||
|
<TheaterModeToggle Available="ShowTheaterToggle" />
|
||||||
@* Lava-lamp icon → popover panel (full parity, §3d-revised). Sits top-right across from the
|
@* Lava-lamp icon → popover panel (full parity, §3d-revised). Sits top-right across from the
|
||||||
back link, clear of the header's own Play/Share affordances below. *@
|
back link, clear of the header's own Play/Share affordances below. *@
|
||||||
<WaveformVisualizerControlPopover />
|
<WaveformVisualizerControlPopover />
|
||||||
</div>
|
</div>
|
||||||
</TopRightAction>
|
</TopRightAction>
|
||||||
<Header>
|
<Header>
|
||||||
@* Theater Mode (Phase 20 §4): the release content is removed from the render — not
|
@* Theater Mode (Phase 20 §4, Wave 2 §2): the release content stays mounted and eases out via
|
||||||
CSS-hidden — so the visualizer fills the surface. OFF restores it byte-for-byte. *@
|
a collapsing wrapper so it does not pop — IsContentHidden collapses it to zero height when
|
||||||
@if (!VisualizerControlState.TheaterMode)
|
Theater is on AND this Cut is the playing release. OFF eases it back to its normal layout. *@
|
||||||
{
|
<div class="dd-theater-collapsible @(IsContentHidden ? "dd-theater-collapsed" : null)">
|
||||||
|
<div class="dd-theater-collapsible-inner">
|
||||||
@* Header split: meta + Play/Share on the LEFT, bordered cover on the RIGHT (spec §3.1). *@
|
@* 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-header">
|
||||||
<div class="cut-detail-meta">
|
<div class="cut-detail-meta">
|
||||||
@@ -127,11 +134,13 @@ else
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
</div>
|
||||||
|
</div>
|
||||||
</Header>
|
</Header>
|
||||||
<BodyContent>
|
<BodyContent>
|
||||||
@if (!VisualizerControlState.TheaterMode)
|
@* Theater Mode (Wave 2 §2): eased collapse, mirroring the Header region. *@
|
||||||
{
|
<div class="dd-theater-collapsible @(IsContentHidden ? "dd-theater-collapsed" : null)">
|
||||||
|
<div class="dd-theater-collapsible-inner">
|
||||||
@* Blurb sits between the header and the track-list divider. *@
|
@* Blurb sits between the header and the track-list divider. *@
|
||||||
<ReleaseDescription Description="@release.Description" />
|
<ReleaseDescription Description="@release.Description" />
|
||||||
<MudDivider Class="cut-detail-divider" />
|
<MudDivider Class="cut-detail-divider" />
|
||||||
@@ -162,13 +171,15 @@ else
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
</div>
|
||||||
|
</div>
|
||||||
</BodyContent>
|
</BodyContent>
|
||||||
</ReleaseDetailScaffold>
|
</ReleaseDetailScaffold>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
[CascadingParameter] public IStreamingPlayerService? PlayerService { get; set; }
|
// PlayerService is cascaded by CutDetailBase (used there for the Theater release-playing predicate).
|
||||||
[CascadingParameter] public IQueueService? Queue { get; set; }
|
[CascadingParameter] public IQueueService? Queue { get; set; }
|
||||||
|
|
||||||
// Header Play: load the full album into the queue starting at track 0.
|
// Header Play: load the full album into the queue starting at track 0.
|
||||||
|
|||||||
@@ -20,11 +20,41 @@ public abstract class CutDetailBase : ComponentBase, IDisposable
|
|||||||
[Inject] public required CutDetailViewModel ViewModel { get; set; }
|
[Inject] public required CutDetailViewModel ViewModel { get; set; }
|
||||||
[Inject] public required PersistentComponentState PersistentState { get; set; }
|
[Inject] public required PersistentComponentState PersistentState { get; set; }
|
||||||
|
|
||||||
// Theater Mode (Phase 20). The page owns the @if (!VisualizerControlState.TheaterMode) content gate,
|
// Theater Mode (Phase 20). The page owns the content gate, so it must re-render when the flag flips
|
||||||
// so it must re-render when the flag flips on the toggle. Property-injected; no constructor growth.
|
// on the toggle. Property-injected; no constructor growth.
|
||||||
[Inject] public required WaveformVisualizerControlState VisualizerControlState { get; set; }
|
[Inject] public required WaveformVisualizerControlState VisualizerControlState { get; set; }
|
||||||
|
|
||||||
|
// Theater Mode is scoped to the currently-playing release (Phase 20 Wave 2 §3). The page observes
|
||||||
|
// player state so the toggle availability and content gate re-evaluate live when playback starts,
|
||||||
|
// stops, or moves to a different release. Cascaded by AudioPlayerProvider; no constructor growth.
|
||||||
|
// The cascade is IsFixed, so the provider's own re-render does not reach this page — the page must
|
||||||
|
// subscribe to StateChanged to re-render itself (same posture as AudioPlayerBar).
|
||||||
|
[CascadingParameter] public IStreamingPlayerService? PlayerService { get; set; }
|
||||||
|
|
||||||
private PersistingComponentStateSubscription _persistingSubscription;
|
private PersistingComponentStateSubscription _persistingSubscription;
|
||||||
|
private IStreamingPlayerService? _subscribedPlayer;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True when the currently-playing track belongs to this page's release. Theater Mode only applies
|
||||||
|
/// to the playing release: a detail page whose release is not playing ignores the global flag and
|
||||||
|
/// shows no toggle. Identity is the release <c>EntryKey</c> — the canonical public key the routes
|
||||||
|
/// and <see cref="DeepDrftPublic.Client.Common.ReleaseRoutes"/> use.
|
||||||
|
/// </summary>
|
||||||
|
protected bool IsThisReleasePlaying =>
|
||||||
|
PlayerService?.CurrentTrack?.Release?.EntryKey == EntryKey;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True when this page's release content should be hidden for Theater Mode — only when Theater is on
|
||||||
|
/// AND this release is the one playing. Drives the eased collapse of the header/track-list regions.
|
||||||
|
/// </summary>
|
||||||
|
protected bool IsContentHidden => VisualizerControlState.TheaterMode && IsThisReleasePlaying;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True when the Theater toggle should be offered on this page: a visualizer subsystem is on AND
|
||||||
|
/// this page's release is the one playing.
|
||||||
|
/// </summary>
|
||||||
|
protected bool ShowTheaterToggle =>
|
||||||
|
(VisualizerControlState.LavaEnabled || VisualizerControlState.WaveformEnabled) && IsThisReleasePlaying;
|
||||||
|
|
||||||
// The release EntryKey the ViewModel currently holds — tracks param-only navigations (e.g.
|
// The release EntryKey the ViewModel currently holds — tracks param-only navigations (e.g.
|
||||||
// /cuts/{a} -> /cuts/{b}) which reuse this component instance and fire OnParametersSet without
|
// /cuts/{a} -> /cuts/{b}) which reuse this component instance and fire OnParametersSet without
|
||||||
@@ -40,8 +70,22 @@ public abstract class CutDetailBase : ComponentBase, IDisposable
|
|||||||
|
|
||||||
private void OnVisualizerStateChanged() => InvokeAsync(StateHasChanged);
|
private void OnVisualizerStateChanged() => InvokeAsync(StateHasChanged);
|
||||||
|
|
||||||
|
private void OnPlayerStateChanged() => InvokeAsync(StateHasChanged);
|
||||||
|
|
||||||
protected override async Task OnParametersSetAsync()
|
protected override async Task OnParametersSetAsync()
|
||||||
{
|
{
|
||||||
|
// The player cascade is IsFixed, so the provider's re-render does not reach this page; subscribe
|
||||||
|
// to the StateChanged side-channel to re-render when playback moves between releases. Idempotent
|
||||||
|
// (reference-guarded) and unsubscribed on dispose — same posture as AudioPlayerBar.
|
||||||
|
if (PlayerService is not null && !ReferenceEquals(PlayerService, _subscribedPlayer))
|
||||||
|
{
|
||||||
|
if (_subscribedPlayer is not null)
|
||||||
|
_subscribedPlayer.StateChanged -= OnPlayerStateChanged;
|
||||||
|
|
||||||
|
PlayerService.StateChanged += OnPlayerStateChanged;
|
||||||
|
_subscribedPlayer = PlayerService;
|
||||||
|
}
|
||||||
|
|
||||||
if (_loaded && _loadedKey == EntryKey) return;
|
if (_loaded && _loadedKey == EntryKey) return;
|
||||||
|
|
||||||
// Capture the key synchronously before any await so a re-entrant call (rapid navigation or a
|
// Capture the key synchronously before any await so a re-entrant call (rapid navigation or a
|
||||||
@@ -75,6 +119,11 @@ public abstract class CutDetailBase : ComponentBase, IDisposable
|
|||||||
{
|
{
|
||||||
_persistingSubscription.Dispose();
|
_persistingSubscription.Dispose();
|
||||||
VisualizerControlState.Changed -= OnVisualizerStateChanged;
|
VisualizerControlState.Changed -= OnVisualizerStateChanged;
|
||||||
|
if (_subscribedPlayer is not null)
|
||||||
|
{
|
||||||
|
_subscribedPlayer.StateChanged -= OnPlayerStateChanged;
|
||||||
|
_subscribedPlayer = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// JSON-serializable bridge payload. Round-trips through PersistentComponentState's serializer.
|
// JSON-serializable bridge payload. Round-trips through PersistentComponentState's serializer.
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ else
|
|||||||
TrackId="@ViewModel.Track?.Id"
|
TrackId="@ViewModel.Track?.Id"
|
||||||
TrackEntryKey="@ViewModel.Track?.EntryKey" />
|
TrackEntryKey="@ViewModel.Track?.EntryKey" />
|
||||||
|
|
||||||
<div class="mix-detail-foreground">
|
<div class="mix-detail-foreground dd-detail-fill">
|
||||||
<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).
|
@* 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
|
Title/artist/genre/date/share/play all move into the overlaid hero, so the scaffold's
|
||||||
@@ -60,7 +60,9 @@ else
|
|||||||
visible in Theater Mode — controls over the experience, not release content (§4/OQ4).
|
visible in Theater Mode — controls over the experience, not release content (§4/OQ4).
|
||||||
Wrapped so they cluster on the right rather than spreading across the SpaceBetween row. *@
|
Wrapped so they cluster on the right rather than spreading across the SpaceBetween row. *@
|
||||||
<div class="dd-detail-top-actions">
|
<div class="dd-detail-top-actions">
|
||||||
<TheaterModeToggle />
|
@* Theater toggle only appears when this Mix is the currently-playing release
|
||||||
|
(Phase 20 Wave 2 §3). ShowTheaterToggle folds in the subsystem + release-playing gate. *@
|
||||||
|
<TheaterModeToggle Available="ShowTheaterToggle" />
|
||||||
@* Lava-lamp icon → popover panel, top-right across from the back link (Phase 12
|
@* Lava-lamp icon → popover panel, top-right across from the back link (Phase 12
|
||||||
§3d-revised). Replaces the former inline TopContent knob-bar: the icon IS the toggle
|
§3d-revised). Replaces the former inline TopContent knob-bar: the icon IS the toggle
|
||||||
and the popover IS the panel. Mix takes the cleanest anchor case (§8e) — the popover's
|
and the popover IS the panel. Mix takes the cleanest anchor case (§8e) — the popover's
|
||||||
@@ -69,10 +71,11 @@ else
|
|||||||
</div>
|
</div>
|
||||||
</TopRightAction>
|
</TopRightAction>
|
||||||
<Hero>
|
<Hero>
|
||||||
@* Theater Mode (Phase 20 §4): the hero overlay (Session/Mix release content) is removed
|
@* Theater Mode (Phase 20 §4, Wave 2 §2): the hero overlay stays mounted and eases out via
|
||||||
from the render so the full-bleed visualizer fills the surface. OFF restores it. *@
|
a collapsing wrapper so it does not pop — collapsed to zero height when Theater is on AND
|
||||||
@if (!VisualizerControlState.TheaterMode)
|
this Mix is the playing release. OFF eases the full-bleed visualizer back behind the hero. *@
|
||||||
{
|
<div class="dd-theater-collapsible @(IsContentHidden ? "dd-theater-collapsed" : null)">
|
||||||
|
<div class="dd-theater-collapsible-inner">
|
||||||
@* Cover-as-background hero with all metadata overlaid, square `mix-hero` sizing. The
|
@* Cover-as-background hero with all metadata overlaid, square `mix-hero` sizing. The
|
||||||
cover art IS the background, so no separate cover thumbnail (CoverThumbKey defaults
|
cover art IS the background, so no separate cover thumbnail (CoverThumbKey defaults
|
||||||
to null). Share and play ride in as slots, matching Sessions. *@
|
to null). Share and play ride in as slots, matching Sessions. *@
|
||||||
@@ -96,14 +99,17 @@ else
|
|||||||
}
|
}
|
||||||
</PlayContent>
|
</PlayContent>
|
||||||
</ReleaseHeroOverlay>
|
</ReleaseHeroOverlay>
|
||||||
}
|
</div>
|
||||||
|
</div>
|
||||||
</Hero>
|
</Hero>
|
||||||
<BodyContent>
|
<BodyContent>
|
||||||
@if (!VisualizerControlState.TheaterMode)
|
@* Theater Mode (Wave 2 §2): eased collapse, mirroring the Hero region. *@
|
||||||
{
|
<div class="dd-theater-collapsible @(IsContentHidden ? "dd-theater-collapsed" : null)">
|
||||||
|
<div class="dd-theater-collapsible-inner">
|
||||||
@* Blurb sits below the hero, inside the scaffold's foreground stacking context. *@
|
@* Blurb sits below the hero, inside the scaffold's foreground stacking context. *@
|
||||||
<ReleaseDescription Description="@release.Description" />
|
<ReleaseDescription Description="@release.Description" />
|
||||||
}
|
</div>
|
||||||
|
</div>
|
||||||
</BodyContent>
|
</BodyContent>
|
||||||
</ReleaseDetailScaffold>
|
</ReleaseDetailScaffold>
|
||||||
</MudContainer>
|
</MudContainer>
|
||||||
@@ -113,7 +119,7 @@ else
|
|||||||
@code {
|
@code {
|
||||||
protected override string PersistKey => "mix-detail";
|
protected override string PersistKey => "mix-detail";
|
||||||
|
|
||||||
[CascadingParameter] public IStreamingPlayerService? PlayerService { get; set; }
|
// PlayerService is cascaded by ReleaseDetailBase (used there for the Theater release-playing predicate).
|
||||||
[CascadingParameter] public IQueueService? Queue { get; set; }
|
[CascadingParameter] public IQueueService? Queue { get; set; }
|
||||||
|
|
||||||
// The hero now carries the play affordance (the scaffold's header is suppressed), so the
|
// The hero now carries the play affordance (the scaffold's header is suppressed), so the
|
||||||
|
|||||||
@@ -18,11 +18,41 @@ public abstract class ReleaseDetailBase : ComponentBase, IDisposable
|
|||||||
[Inject] public required ReleaseDetailViewModel ViewModel { get; set; }
|
[Inject] public required ReleaseDetailViewModel ViewModel { get; set; }
|
||||||
[Inject] public required PersistentComponentState PersistentState { get; set; }
|
[Inject] public required PersistentComponentState PersistentState { get; set; }
|
||||||
|
|
||||||
// Theater Mode (Phase 20). The page owns the @if (!VisualizerControlState.TheaterMode) content gate,
|
// Theater Mode (Phase 20). The page owns the content gate, so it must re-render when the flag flips
|
||||||
// so it must re-render when the flag flips on the toggle. Property-injected; no constructor growth.
|
// on the toggle. Property-injected; no constructor growth.
|
||||||
[Inject] public required WaveformVisualizerControlState VisualizerControlState { get; set; }
|
[Inject] public required WaveformVisualizerControlState VisualizerControlState { get; set; }
|
||||||
|
|
||||||
|
// Theater Mode is scoped to the currently-playing release (Phase 20 Wave 2 §3). The page observes
|
||||||
|
// player state so the toggle availability and content gate re-evaluate live when playback starts,
|
||||||
|
// stops, or moves to a different release. Cascaded by AudioPlayerProvider; no constructor growth.
|
||||||
|
// The cascade is IsFixed, so the provider's own re-render does not reach this page — the page must
|
||||||
|
// subscribe to StateChanged to re-render itself (same posture as AudioPlayerBar).
|
||||||
|
[CascadingParameter] public IStreamingPlayerService? PlayerService { get; set; }
|
||||||
|
|
||||||
private PersistingComponentStateSubscription _persistingSubscription;
|
private PersistingComponentStateSubscription _persistingSubscription;
|
||||||
|
private IStreamingPlayerService? _subscribedPlayer;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True when the currently-playing track belongs to this page's release. Theater Mode only applies
|
||||||
|
/// to the playing release: a detail page whose release is not playing ignores the global flag and
|
||||||
|
/// shows no toggle. Identity is the release <c>EntryKey</c> — the canonical public key the routes
|
||||||
|
/// and <see cref="DeepDrftPublic.Client.Common.ReleaseRoutes"/> use.
|
||||||
|
/// </summary>
|
||||||
|
protected bool IsThisReleasePlaying =>
|
||||||
|
PlayerService?.CurrentTrack?.Release?.EntryKey == EntryKey;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True when this page's release content should be hidden for Theater Mode — only when Theater is on
|
||||||
|
/// AND this release is the one playing. Drives the eased collapse of the hero/blurb regions.
|
||||||
|
/// </summary>
|
||||||
|
protected bool IsContentHidden => VisualizerControlState.TheaterMode && IsThisReleasePlaying;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True when the Theater toggle should be offered on this page: a visualizer subsystem is on AND
|
||||||
|
/// this page's release is the one playing.
|
||||||
|
/// </summary>
|
||||||
|
protected bool ShowTheaterToggle =>
|
||||||
|
(VisualizerControlState.LavaEnabled || VisualizerControlState.WaveformEnabled) && IsThisReleasePlaying;
|
||||||
|
|
||||||
// The release EntryKey the ViewModel currently holds. Tracks param-only navigations (e.g.
|
// The release EntryKey the ViewModel currently holds. Tracks param-only navigations (e.g.
|
||||||
// /mixes/{a} -> /mixes/{b}) which reuse this component instance and fire OnParametersSet
|
// /mixes/{a} -> /mixes/{b}) which reuse this component instance and fire OnParametersSet
|
||||||
@@ -42,8 +72,22 @@ public abstract class ReleaseDetailBase : ComponentBase, IDisposable
|
|||||||
|
|
||||||
private void OnVisualizerStateChanged() => InvokeAsync(StateHasChanged);
|
private void OnVisualizerStateChanged() => InvokeAsync(StateHasChanged);
|
||||||
|
|
||||||
|
private void OnPlayerStateChanged() => InvokeAsync(StateHasChanged);
|
||||||
|
|
||||||
protected override async Task OnParametersSetAsync()
|
protected override async Task OnParametersSetAsync()
|
||||||
{
|
{
|
||||||
|
// The player cascade is IsFixed, so the provider's re-render does not reach this page; subscribe
|
||||||
|
// to the StateChanged side-channel to re-render when playback moves between releases. Idempotent
|
||||||
|
// (reference-guarded) and unsubscribed on dispose — same posture as AudioPlayerBar.
|
||||||
|
if (PlayerService is not null && !ReferenceEquals(PlayerService, _subscribedPlayer))
|
||||||
|
{
|
||||||
|
if (_subscribedPlayer is not null)
|
||||||
|
_subscribedPlayer.StateChanged -= OnPlayerStateChanged;
|
||||||
|
|
||||||
|
PlayerService.StateChanged += OnPlayerStateChanged;
|
||||||
|
_subscribedPlayer = PlayerService;
|
||||||
|
}
|
||||||
|
|
||||||
// Re-run whenever the route key changes. Component instances are reused across
|
// Re-run whenever the route key changes. Component instances are reused across
|
||||||
// same-template navigations, so the load decision must live here, not in
|
// same-template navigations, so the load decision must live here, not in
|
||||||
// OnInitialized (which fires once per instance).
|
// OnInitialized (which fires once per instance).
|
||||||
@@ -83,6 +127,11 @@ public abstract class ReleaseDetailBase : ComponentBase, IDisposable
|
|||||||
{
|
{
|
||||||
_persistingSubscription.Dispose();
|
_persistingSubscription.Dispose();
|
||||||
VisualizerControlState.Changed -= OnVisualizerStateChanged;
|
VisualizerControlState.Changed -= OnVisualizerStateChanged;
|
||||||
|
if (_subscribedPlayer is not null)
|
||||||
|
{
|
||||||
|
_subscribedPlayer.StateChanged -= OnPlayerStateChanged;
|
||||||
|
_subscribedPlayer = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// JSON-serializable bridge payload. Round-trips through PersistentComponentState's serializer.
|
// JSON-serializable bridge payload. Round-trips through PersistentComponentState's serializer.
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ else
|
|||||||
TrackId="@ViewModel.Track?.Id"
|
TrackId="@ViewModel.Track?.Id"
|
||||||
TrackEntryKey="@ViewModel.Track?.EntryKey" />
|
TrackEntryKey="@ViewModel.Track?.EntryKey" />
|
||||||
|
|
||||||
<MudContainer MaxWidth="MaxWidth.Large" Class="session-detail-page session-detail-foreground">
|
<MudContainer MaxWidth="MaxWidth.Large" Class="session-detail-page session-detail-foreground dd-detail-fill">
|
||||||
|
|
||||||
<div class="session-detail-top-row">
|
<div class="session-detail-top-row">
|
||||||
<MudLink Href="/sessions" Typo="Typo.body2" Class="deepdrft-track-detail-back">
|
<MudLink Href="/sessions" Typo="Typo.body2" Class="deepdrft-track-detail-back">
|
||||||
@@ -59,18 +59,20 @@ else
|
|||||||
@* Theater toggle sits immediately LEFT of the lava-lamp popover (Phase 20 §3). The whole top
|
@* Theater toggle sits immediately LEFT of the lava-lamp popover (Phase 20 §3). The whole top
|
||||||
row (back + theater + lava) stays in Theater Mode — controls, not release content (§4/OQ4). *@
|
row (back + theater + lava) stays in Theater Mode — controls, not release content (§4/OQ4). *@
|
||||||
<div class="dd-detail-top-actions">
|
<div class="dd-detail-top-actions">
|
||||||
<TheaterModeToggle />
|
@* Theater toggle only appears when this Session is the currently-playing release
|
||||||
|
(Phase 20 Wave 2 §3). ShowTheaterToggle folds in the subsystem + release-playing gate. *@
|
||||||
|
<TheaterModeToggle Available="ShowTheaterToggle" />
|
||||||
@* Lava-lamp icon → popover panel (full parity, §3e/§3d-revised). Anchored top-right, clear of
|
@* Lava-lamp icon → popover panel (full parity, §3e/§3d-revised). Anchored top-right, clear of
|
||||||
the hero overlay and the share/play affordances overlaid on the hero below. *@
|
the hero overlay and the share/play affordances overlaid on the hero below. *@
|
||||||
<WaveformVisualizerControlPopover />
|
<WaveformVisualizerControlPopover />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@* Theater Mode (Phase 20 §4): the hero overlay + blurb (the session's release content) are removed
|
@* Theater Mode (Phase 20 §4, Wave 2 §2): the hero overlay + blurb stay mounted and ease out via a
|
||||||
from the render so the ambient visualizer fills the surface. The top row above stays. OFF
|
collapsing wrapper so they do not pop — collapsed to zero height when Theater is on AND this
|
||||||
restores this region byte-for-byte. *@
|
Session is the playing release. The top row above stays. OFF eases this region back in. *@
|
||||||
@if (!VisualizerControlState.TheaterMode)
|
<div class="dd-theater-collapsible @(IsContentHidden ? "dd-theater-collapsed" : null)">
|
||||||
{
|
<div class="dd-theater-collapsible-inner">
|
||||||
@* The overlay shows the cover thumbnail only when it differs from the resolved hero image —
|
@* The overlay shows the cover thumbnail only when it differs from the resolved hero image —
|
||||||
when there is no dedicated hero, heroImage already falls back to release.ImagePath, so the
|
when there is no dedicated hero, heroImage already falls back to release.ImagePath, so the
|
||||||
thumb would duplicate the background. That logic lives in ReleaseHeroOverlay. *@
|
thumb would duplicate the background. That logic lives in ReleaseHeroOverlay. *@
|
||||||
@@ -96,7 +98,8 @@ else
|
|||||||
</ReleaseHeroOverlay>
|
</ReleaseHeroOverlay>
|
||||||
|
|
||||||
<ReleaseDescription Description="@release.Description" />
|
<ReleaseDescription Description="@release.Description" />
|
||||||
}
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</MudContainer>
|
</MudContainer>
|
||||||
}
|
}
|
||||||
@@ -104,7 +107,7 @@ else
|
|||||||
@code {
|
@code {
|
||||||
protected override string PersistKey => "session-detail";
|
protected override string PersistKey => "session-detail";
|
||||||
|
|
||||||
[CascadingParameter] public IStreamingPlayerService? PlayerService { get; set; }
|
// PlayerService is cascaded by ReleaseDetailBase (used there for the Theater release-playing predicate).
|
||||||
[CascadingParameter] public IQueueService? Queue { get; set; }
|
[CascadingParameter] public IQueueService? Queue { get; set; }
|
||||||
|
|
||||||
// Mirrors the play-toggle wiring the shared scaffold owns. Session detail composes the player
|
// Mirrors the play-toggle wiring the shared scaffold owns. Session detail composes the player
|
||||||
|
|||||||
@@ -360,6 +360,66 @@ h2, h3, h4, h5, h6,
|
|||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Full-screen detail body (Phase 20 Wave 2 §1). The content body always fills the viewport below the
|
||||||
|
fixed nav so the ambient/full-bleed visualizer reads as genuinely full-screen and the footer is pushed
|
||||||
|
below the fold (scroll to reach it) — independent of Theater Mode. Reuses the shared
|
||||||
|
--deepdrft-nav-height token (88px desktop / 72px mobile) so the clearance tracks the bar across
|
||||||
|
breakpoints; no new layout token. Applied to each detail page's foreground content container. */
|
||||||
|
.dd-detail-fill {
|
||||||
|
min-height: calc(100vh - var(--deepdrft-nav-height, 88px));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Eased content collapse for Theater Mode (Phase 20 Wave 2 §2). The detail content stays mounted and
|
||||||
|
collapses smoothly when .dd-theater-collapsed is applied, so toggling Theater eases both directions
|
||||||
|
instead of popping — when collapsed the content is fully out of the way and the visualizer is
|
||||||
|
unobstructed. The same pattern drives the player-bar "now showing" band so the bar grows/shrinks
|
||||||
|
smoothly too.
|
||||||
|
|
||||||
|
Technique: grid-template-rows 1fr → 0fr interpolates the REAL content height (no 400vh ceiling
|
||||||
|
artifact / delayed-start that the old max-height approach had). The direct child receives
|
||||||
|
overflow:hidden + min-height:0 so it actually clips during the transition (the grid child is the
|
||||||
|
collapsing unit). visibility:hidden removes all descendants from the tab order and from pointer/
|
||||||
|
keyboard interaction once collapsed — this fixes the Major accessibility defect where Tab could
|
||||||
|
reach hidden controls. transition-behavior:allow-discrete makes visibility flip discretely: it
|
||||||
|
flips to hidden AFTER the ease-out finishes (so the animation plays fully), and flips back to
|
||||||
|
visible BEFORE the ease-in starts (so content is immediately interactive on the way back in).
|
||||||
|
The visibility transition duration matches the height ease (0.45s) so allow-discrete has a real
|
||||||
|
interval to defer against: on collapse the flip to hidden is held until t=0.45s; on reopen it
|
||||||
|
fires at t=0 (immediately interactive). A 0s duration would fire the flip at t≈0 on collapse,
|
||||||
|
defeating the deferral and hiding content before the ease-out finishes. */
|
||||||
|
.dd-theater-collapsible {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: 1fr;
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
transition: grid-template-rows 0.45s ease, opacity 0.3s ease, visibility 0.45s;
|
||||||
|
transition-behavior: allow-discrete;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The single direct child clips itself during the grid-row collapse. min-height:0 overrides the
|
||||||
|
implicit min-height:auto that would prevent the row from shrinking past the content's intrinsic
|
||||||
|
height. overflow:hidden clips painted content when the row is partially collapsed. */
|
||||||
|
.dd-theater-collapsible > * {
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dd-theater-collapsed {
|
||||||
|
grid-template-rows: 0fr;
|
||||||
|
opacity: 0;
|
||||||
|
/* visibility flips to hidden at the END of the 0.45s ease-out (deferred by allow-discrete);
|
||||||
|
on reopen it flips back to visible at t=0 so content is immediately interactive. */
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Honor reduced-motion: collapse still happens (it is layout, not decoration) but instantly, matching
|
||||||
|
the parallax precedent (transition-duration: 0). */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.dd-theater-collapsible {
|
||||||
|
transition-duration: 0ms;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.deepdrft-track-detail-meta {
|
.deepdrft-track-detail-meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|||||||
Reference in New Issue
Block a user