Add whole-release embeds to FramePlayer and un-gate the release embed share affordance

The queue gains an armed-but-idle state (Arm/Start) so a release embed stages track 0 prerender-safe, then queues the full release on first play and auto-advances.
This commit is contained in:
daniel-c-harvey
2026-06-19 12:05:35 -04:00
parent 1931574ad4
commit 912256d99a
12 changed files with 560 additions and 47 deletions
@@ -186,6 +186,16 @@ public partial class AudioPlayerBar : ComponentBase, IAsyncDisposable
// the first play click — the user gesture the browser requires before audio can start.
if (IsStaged)
{
// Release embed: the queue is armed with the whole release. Route the first gesture through
// the queue so it takes over (streams track 0 and auto-advances) rather than streaming the
// staged track in isolation. Single-track embeds leave the queue disarmed and fall through
// to the direct stream below — unchanged.
if (QueueService is { IsArmed: true })
{
await QueueService.Start();
return;
}
await PlayerService.SelectTrackStreaming(PlayerService.CurrentTrack!);
return;
}
@@ -42,35 +42,33 @@
}
</MudStack>
@* Embed is a single-track affordance only; a release page is not a single-track embed (§3b.3). *@
@if (!IsReleaseMode)
@* Embed is offered in both modes: a track snippet (TrackEntryKey) or a whole-release
snippet (ReleaseEntryKey) targeting FramePlayer's matching query param. *@
<MudDivider />
<MudCheckBox @bind-Value="Embed" Color="Color.Primary" Label="Embed player" Dense="true" />
@if (_embed)
{
<MudDivider />
<MudCheckBox @bind-Value="Embed" Color="Color.Primary" Label="Embed player" Dense="true" />
@if (_embed)
{
<MudStack Row AlignItems="AlignItems.Center" Spacing="1">
<MudTextField Value="@EmbedSnippet"
T="string"
ReadOnly="true"
Variant="Variant.Outlined"
Lines="3"
Margin="Margin.Dense"
Class="deepdrft-share-embed-field" />
<MudStack AlignItems="AlignItems.Center" Spacing="0">
<MudIconButton Icon="@Icons.Material.Filled.ContentCopy"
Color="Color.Primary"
OnClick="@CopyEmbed"
aria-label="Copy embed snippet" />
@if (_embedCopied)
{
<MudText Typo="Typo.caption" Color="Color.Success">Copied!</MudText>
}
</MudStack>
<MudStack Row AlignItems="AlignItems.Center" Spacing="1">
<MudTextField Value="@EmbedSnippet"
T="string"
ReadOnly="true"
Variant="Variant.Outlined"
Lines="3"
Margin="Margin.Dense"
Class="deepdrft-share-embed-field" />
<MudStack AlignItems="AlignItems.Center" Spacing="0">
<MudIconButton Icon="@Icons.Material.Filled.ContentCopy"
Color="Color.Primary"
OnClick="@CopyEmbed"
aria-label="Copy embed snippet" />
@if (_embedCopied)
{
<MudText Typo="Typo.caption" Color="Color.Success">Copied!</MudText>
}
</MudStack>
}
</MudStack>
}
</MudStack>
@@ -1,5 +1,6 @@
using DeepDrftModels.Enums;
using DeepDrftPublic.Client.Common;
using DeepDrftPublic.Client.Helpers;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
@@ -7,19 +8,19 @@ namespace DeepDrftPublic.Client.Controls;
/// <summary>
/// Share affordance with two modes from one source of clipboard/popover-chrome logic
/// (Phase 11 §3b). Track mode (<see cref="EntryKey"/> set) offers a canonical-link copy plus an
/// optional iframe embed snippet. Release mode (<see cref="ReleaseEntryKey"/> set) is copy-link-only —
/// it copies the absolute form of the release's canonical detail URL and hides the embed
/// affordance, since a release page is not a single-track embed. Clipboard writes go through
/// navigator.clipboard; each copy shows a transient "Copied!" confirmation that resets after a
/// short delay.
/// (Phase 11 §3b). Both modes offer a canonical-link copy plus an optional iframe embed snippet.
/// Track mode (<see cref="EntryKey"/> set) embeds a single track (FramePlayer?TrackEntryKey=...);
/// release mode (<see cref="ReleaseEntryKey"/> set) copies the release's canonical detail URL and
/// embeds the whole release (FramePlayer?ReleaseEntryKey=...), which queues and advances through its
/// tracks on first play. Clipboard writes go through navigator.clipboard; each copy shows a transient
/// "Copied!" confirmation that resets after a short delay.
/// </summary>
public partial class SharePopover : ComponentBase, IDisposable
{
/// <summary>Track mode: the vault entry key of the track to share. Mutually exclusive with the release target.</summary>
[Parameter] public string? EntryKey { get; set; }
/// <summary>Release mode: the release's opaque public EntryKey to share. When set (with <see cref="ReleaseMedium"/>), the popover shares the release detail URL and omits the embed option.</summary>
/// <summary>Release mode: the release's opaque public EntryKey to share. When set (with <see cref="ReleaseMedium"/>), the popover shares the release detail URL and embeds the whole release.</summary>
[Parameter] public string? ReleaseEntryKey { get; set; }
/// <summary>Release mode: the medium of the release, used to resolve its canonical detail route.</summary>
@@ -56,8 +57,11 @@ public partial class SharePopover : ComponentBase, IDisposable
private string TrackUrl => $"{Navigation.BaseUri}tracks/{EntryKey}";
private string EmbedSnippet =>
$"""<iframe src="{Navigation.BaseUri}FramePlayer?TrackEntryKey={EntryKey}" width="656" height="196" frameborder="0" style="border-radius:8px;" allow="autoplay"></iframe>""";
// FramePlayer's query param selects the embed mode: ReleaseEntryKey queues the whole release,
// TrackEntryKey stages a single track. The iframe chrome is identical in both modes.
private string EmbedSnippet => IsReleaseMode
? EmbedSnippetBuilder.ForRelease(Navigation.BaseUri, ReleaseEntryKey!)
: EmbedSnippetBuilder.ForTrack(Navigation.BaseUri, EntryKey!);
private void Toggle() => _open = !_open;