using DeepDrftModels.Enums;
using DeepDrftPublic.Client.Common;
using DeepDrftPublic.Client.Helpers;
using DeepDrftPublic.Client.Services;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
namespace DeepDrftPublic.Client.Controls;
///
/// Share affordance with two modes from one source of clipboard/popover-chrome logic
/// (Phase 11 §3b). Both modes offer a canonical-link copy plus an optional iframe embed snippet.
/// Track mode ( set) embeds a single track (FramePlayer?TrackEntryKey=...);
/// release mode ( 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.
///
public partial class SharePopover : ComponentBase, IDisposable
{
/// Track mode: the vault entry key of the track to share. Mutually exclusive with the release target.
[Parameter] public string? EntryKey { get; set; }
/// Release mode: the release's opaque public EntryKey to share. When set (with ), the popover shares the release detail URL and embeds the whole release.
[Parameter] public string? ReleaseEntryKey { get; set; }
/// Release mode: the medium of the release, used to resolve its canonical detail route.
[Parameter] public ReleaseMedium ReleaseMedium { get; set; }
[Inject] public required NavigationManager Navigation { get; set; }
[Inject] public required IJSRuntime JS { get; set; }
[Inject] public required ShareTracker ShareTracker { get; set; }
[Inject] public required IAnonIdProvider AnonId { get; set; }
private bool IsReleaseMode => ReleaseEntryKey is not null;
private bool _open;
private bool _embed;
private bool _linkCopied;
private bool _embedCopied;
private readonly CancellationTokenSource _cts = new();
private bool Embed
{
get => _embed;
set
{
_embed = value;
if (!value) _embedCopied = false;
}
}
// The URL "Copy link" places on the clipboard. Release mode resolves the canonical detail
// route (which carries a leading slash) and composes it against BaseUri (which carries a
// trailing slash) — trim one to avoid a doubled separator.
private string LinkUrl => IsReleaseMode
? $"{Navigation.BaseUri.TrimEnd('/')}{ReleaseRoutes.DetailHref(ReleaseEntryKey!, ReleaseMedium)}"
: TrackUrl;
private string TrackUrl => $"{Navigation.BaseUri}tracks/{EntryKey}";
// 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 async Task Toggle()
{
_open = !_open;
// Warm the anon-id cache when the popover opens (wave 16.3) so a copy-share fired moments later
// reads a populated AnonId.Current. Idempotent and best-effort — if it fails the share simply
// carries no anonId. Opening is interactive, so the localStorage interop is available here.
if (_open)
await AnonId.EnsureLoadedAsync();
}
private void Close() => _open = false;
private async Task CopyLink()
{
if (await CopyToClipboard(LinkUrl))
{
// Record a share only after the clipboard write succeeds (§1b). Release mode targets the
// release EntryKey; track mode targets the track EntryKey. The tracker debounces repeat
// copies of the same (target, channel) into one event.
if (IsReleaseMode)
ShareTracker.RecordShare(ShareTargetType.Release, ReleaseEntryKey!, ShareChannel.Link);
else if (!string.IsNullOrWhiteSpace(EntryKey))
ShareTracker.RecordShare(ShareTargetType.Track, EntryKey, ShareChannel.Link);
_linkCopied = true;
await ResetAfterDelay(() => _linkCopied = false);
}
}
private async Task CopyEmbed()
{
if (await CopyToClipboard(EmbedSnippet))
{
// Embed is a single-track affordance only (release mode hides it), so this always targets a
// track with channel = embed.
if (!string.IsNullOrWhiteSpace(EntryKey))
ShareTracker.RecordShare(ShareTargetType.Track, EntryKey, ShareChannel.Embed);
_embedCopied = true;
await ResetAfterDelay(() => _embedCopied = false);
}
}
private async Task CopyToClipboard(string text)
{
try
{
await JS.InvokeVoidAsync("navigator.clipboard.writeText", text);
return true;
}
catch (Exception)
{
return false;
}
}
private async Task ResetAfterDelay(Action reset)
{
try
{
await Task.Delay(1500, _cts.Token);
}
catch (TaskCanceledException)
{
return;
}
reset();
StateHasChanged();
}
public void Dispose() => _cts.Cancel();
}