Merge branch 'share-button' into dev

This commit is contained in:
daniel-c-harvey
2026-06-07 16:41:35 -04:00
4 changed files with 172 additions and 4 deletions
@@ -0,0 +1,61 @@
@namespace DeepDrftPublic.Client.Controls
<MudTooltip Text="Share">
<MudIconButton Icon="@Icons.Material.Filled.Share"
Size="Size.Large"
Color="Color.Secondary"
OnClick="@Toggle"
aria-label="Share this track" />
</MudTooltip>
<MudOverlay Visible="@_open" OnClick="@Close" AutoClose="true" />
<MudPopover Open="@_open"
Fixed="false"
AnchorOrigin="Origin.BottomRight"
TransformOrigin="Origin.TopRight"
Class="deepdrft-share-popover">
<MudStack Spacing="2" Class="deepdrft-share-popover-body">
<MudStack Row AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween">
<MudButton StartIcon="@Icons.Material.Filled.Link"
Variant="Variant.Text"
Color="Color.Primary"
OnClick="@CopyLink">
Copy link
</MudButton>
@if (_linkCopied)
{
<MudText Typo="Typo.caption" Color="Color.Success">Copied!</MudText>
}
</MudStack>
<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>
}
</MudStack>
</MudPopover>
@@ -0,0 +1,91 @@
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
namespace DeepDrftPublic.Client.Controls;
/// <summary>
/// Share affordance for the track detail page: a popover offering a canonical-link copy
/// and an optional iframe embed snippet. 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
{
[Parameter] public string? EntryKey { get; set; }
[Inject] public required NavigationManager Navigation { get; set; }
[Inject] public required IJSRuntime JS { get; set; }
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;
}
}
private string TrackUrl => $"{Navigation.BaseUri}track/{EntryKey}";
private string EmbedSnippet =>
$"""<iframe src="{Navigation.BaseUri}FramePlayer?TrackEntryKey={EntryKey}" width="640" height="96" frameborder="0" style="border-radius:8px;" allow="autoplay"></iframe>""";
private void Toggle() => _open = !_open;
private void Close() => _open = false;
private async Task CopyLink()
{
if (await CopyToClipboard(TrackUrl))
{
_linkCopied = true;
await ResetAfterDelay(() => _linkCopied = false);
}
}
private async Task CopyEmbed()
{
if (await CopyToClipboard(EmbedSnippet))
{
_embedCopied = true;
await ResetAfterDelay(() => _embedCopied = false);
}
}
private async Task<bool> 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();
}
@@ -57,9 +57,10 @@ else if (ViewModel.Track is not null)
<MudText Typo="Typo.h6" Color="Color.Primary">@track.Artist</MudText>
</div>
<div>
<MudStack Row AlignItems="AlignItems.Center" Spacing="1">
<SharePopover EntryKey="@track.EntryKey" />
<PlayStateIcon Size="Size.Large" Color="Color.Secondary" OnToggle="@PlayTrack"/>
</div>
</MudStack>
</MudStack>
<div class="deepdrft-track-detail-cover">
@@ -67,8 +68,6 @@ else if (ViewModel.Track is not null)
<MudIcon Icon="@Icons.Material.Filled.Album" Color="Color.Primary" />
</MudPaper>
</div>
@if (hasMeta)
{
@@ -339,6 +339,23 @@ h2, h3, h4, h5, h6,
font-family: var(--deepdrft-font-mono) !important;
}
.deepdrft-share-popover-body {
padding: 0.75rem 1rem;
min-width: 280px;
max-width: 360px;
}
/* Monospace snippet so the iframe markup stays legible inside the readonly field. */
.deepdrft-share-embed-field {
flex: 1 1 auto;
}
.deepdrft-share-embed-field .mud-input-slot {
font-family: var(--deepdrft-font-mono) !important;
font-size: 0.75rem;
word-break: break-all;
}
/* display:contents so the anchor wraps the card's cover and title without
introducing its own box — the container's positioning context and the
content column's flex layout are both preserved. */