using DeepDrftModels.Enums; using DeepDrftPublic.Client.Common; 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). Track mode ( set) offers a canonical-link copy plus an /// optional iframe embed snippet. Release mode ( 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. /// 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 omits the embed option. [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; } 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}"; private string EmbedSnippet => $""""""; private void Toggle() => _open = !_open; 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(); }