feat(theater): full-screen detail body, eased content collapse, playing-release scoping
Detail bodies fill 100vh below the nav so the visualizer reads full-screen; Theater toggle eases page content and the player-bar now-showing panel in/out instead of popping (reduced-motion honored); Theater only applies to the currently-playing release.
This commit is contained in:
@@ -18,11 +18,41 @@ public abstract class ReleaseDetailBase : ComponentBase, IDisposable
|
||||
[Inject] public required ReleaseDetailViewModel ViewModel { get; set; }
|
||||
[Inject] public required PersistentComponentState PersistentState { get; set; }
|
||||
|
||||
// Theater Mode (Phase 20). The page owns the @if (!VisualizerControlState.TheaterMode) content gate,
|
||||
// so it must re-render when the flag flips on the toggle. Property-injected; no constructor growth.
|
||||
// Theater Mode (Phase 20). The page owns the content gate, so it must re-render when the flag flips
|
||||
// on the toggle. Property-injected; no constructor growth.
|
||||
[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 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.
|
||||
// /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 OnPlayerStateChanged() => InvokeAsync(StateHasChanged);
|
||||
|
||||
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
|
||||
// same-template navigations, so the load decision must live here, not in
|
||||
// OnInitialized (which fires once per instance).
|
||||
@@ -83,6 +127,11 @@ public abstract class ReleaseDetailBase : ComponentBase, IDisposable
|
||||
{
|
||||
_persistingSubscription.Dispose();
|
||||
VisualizerControlState.Changed -= OnVisualizerStateChanged;
|
||||
if (_subscribedPlayer is not null)
|
||||
{
|
||||
_subscribedPlayer.StateChanged -= OnPlayerStateChanged;
|
||||
_subscribedPlayer = null;
|
||||
}
|
||||
}
|
||||
|
||||
// JSON-serializable bridge payload. Round-trips through PersistentComponentState's serializer.
|
||||
|
||||
Reference in New Issue
Block a user