508a522a8d
- Extend ICmsTrackService.GetPagedAsync with album/genre filter params - Add CmsTrackBrowserViewModel (DI-scoped) with lazy album/genre load - Extract CmsTrackGrid: 9-column layout, waveform status, per-row generate, info tooltip, album/genre filter params, OnStatusLoaded callback - Restructure TrackList: remove MudTabs, add three @page routes, mode toggle, Generate All Missing button; album/genre stubs for next wave
266 lines
11 KiB
Plaintext
266 lines
11 KiB
Plaintext
@using System.Net
|
|
@using DeepDrftManager.Services
|
|
@using DeepDrftModels.DTOs
|
|
@inject ICmsTrackService CmsTrackService
|
|
@inject IHttpClientFactory HttpClientFactory
|
|
@inject IDialogService DialogService
|
|
@inject ISnackbar Snackbar
|
|
@inject ILogger<CmsTrackGrid> Logger
|
|
@inject NavigationManager NavigationManager
|
|
|
|
@if (ShowAddButton)
|
|
{
|
|
<MudStack Row="true" Justify="Justify.FlexEnd" Class="mb-2">
|
|
<MudButton Variant="Variant.Filled"
|
|
Color="Color.Primary"
|
|
StartIcon="@Icons.Material.Filled.Add"
|
|
Href="/tracks/upload">
|
|
Add Track
|
|
</MudButton>
|
|
</MudStack>
|
|
}
|
|
|
|
<MudTable T="TrackDto"
|
|
@ref="_table"
|
|
ServerData="LoadServerData"
|
|
Hover="true"
|
|
Striped="true"
|
|
Dense="true"
|
|
Bordered="false"
|
|
FixedHeader="true"
|
|
RowsPerPage="@PageSize"
|
|
AllowUnsorted="false">
|
|
<NoRecordsContent>
|
|
<MudText Typo="Typo.body1">No tracks found.</MudText>
|
|
</NoRecordsContent>
|
|
<LoadingContent>
|
|
<MudText Typo="Typo.body1">Loading tracks…</MudText>
|
|
</LoadingContent>
|
|
<HeaderContent>
|
|
<MudTh Style="width: 1%; white-space: nowrap;">Track #</MudTh>
|
|
<MudTh Style="width: 1%; white-space: nowrap;">Art</MudTh>
|
|
<MudTh><MudTableSortLabel SortLabel="TrackName" T="TrackDto" InitialDirection="SortDirection.Ascending">Track Name</MudTableSortLabel></MudTh>
|
|
<MudTh><MudTableSortLabel SortLabel="Artist" T="TrackDto">Artist</MudTableSortLabel></MudTh>
|
|
<MudTh><MudTableSortLabel SortLabel="Album" T="TrackDto">Album</MudTableSortLabel></MudTh>
|
|
<MudTh><MudTableSortLabel SortLabel="Genre" T="TrackDto">Genre</MudTableSortLabel></MudTh>
|
|
<MudTh><MudTableSortLabel SortLabel="ReleaseDate" T="TrackDto">Release Date</MudTableSortLabel></MudTh>
|
|
<MudTh Style="width: 1%; white-space: nowrap;">Waveform</MudTh>
|
|
<MudTh Style="width: 1%; white-space: nowrap;">Actions</MudTh>
|
|
</HeaderContent>
|
|
<RowTemplate>
|
|
<MudTd DataLabel="Track #">@context.TrackNumber</MudTd>
|
|
<MudTd DataLabel="Art">
|
|
@if (!string.IsNullOrEmpty(context.Release?.ImagePath))
|
|
{
|
|
<div class="cms-track-thumb"
|
|
style="background-image: url('@ThumbUrl(context.Release.ImagePath)');"></div>
|
|
}
|
|
else
|
|
{
|
|
<div class="cms-track-thumb cms-track-thumb--fallback"></div>
|
|
}
|
|
</MudTd>
|
|
<MudTd DataLabel="Track Name">@context.TrackName</MudTd>
|
|
<MudTd DataLabel="Artist">@(context.Release?.Artist ?? "—")</MudTd>
|
|
<MudTd DataLabel="Album">@(context.Release?.Title ?? "—")</MudTd>
|
|
<MudTd DataLabel="Genre">@(context.Release?.Genre ?? "—")</MudTd>
|
|
<MudTd DataLabel="Release Date">@(context.Release?.ReleaseDate?.ToString("d MMMM, yyyy") ?? "—")</MudTd>
|
|
<MudTd DataLabel="Waveform">
|
|
@if (HasProfile(context.EntryKey))
|
|
{
|
|
<MudIcon Icon="@Icons.Material.Filled.CheckCircle" Color="Color.Success" Size="Size.Small" />
|
|
}
|
|
else
|
|
{
|
|
<MudIcon Icon="@Icons.Material.Filled.Cancel" Color="Color.Warning" Size="Size.Small" />
|
|
}
|
|
</MudTd>
|
|
<MudTd DataLabel="Actions">
|
|
<MudTooltip Text="Edit">
|
|
<MudIconButton Icon="@Icons.Material.Filled.Edit"
|
|
Size="Size.Small"
|
|
Color="Color.Primary"
|
|
Href="@($"/tracks/{context.Id}")" />
|
|
</MudTooltip>
|
|
<MudTooltip Text="Delete">
|
|
<MudIconButton Icon="@Icons.Material.Filled.Delete"
|
|
Size="Size.Small"
|
|
Color="Color.Error"
|
|
OnClick="@(() => ConfirmAndDelete(context))" />
|
|
</MudTooltip>
|
|
<MudTooltip>
|
|
<TooltipContent>
|
|
<div class="cms-track-info">
|
|
<div>Entry: @context.EntryKey</div>
|
|
<div>File: @(context.OriginalFileName ?? "—")</div>
|
|
</div>
|
|
</TooltipContent>
|
|
<ChildContent>
|
|
<MudIconButton Icon="@Icons.Material.Outlined.Info" Size="Size.Small" />
|
|
</ChildContent>
|
|
</MudTooltip>
|
|
@if (!HasProfile(context.EntryKey))
|
|
{
|
|
<MudTooltip Text="Generate Waveform">
|
|
<MudIconButton Icon="@Icons.Material.Filled.GraphicEq"
|
|
Size="Size.Small"
|
|
Color="Color.Secondary"
|
|
Disabled="@(_bulkRunning || _generating.Contains(context.EntryKey))"
|
|
OnClick="@(() => GenerateOneAsync(context))" />
|
|
</MudTooltip>
|
|
}
|
|
</MudTd>
|
|
</RowTemplate>
|
|
<PagerContent>
|
|
<MudTablePager PageSizeOptions="new[] { 10, 20, 50 }" />
|
|
</PagerContent>
|
|
</MudTable>
|
|
|
|
@code {
|
|
[Parameter] public string? AlbumFilter { get; set; }
|
|
[Parameter] public string? GenreFilter { get; set; }
|
|
[Parameter] public bool ShowAddButton { get; set; } = true;
|
|
[Parameter] public int PageSize { get; set; } = 20;
|
|
[Parameter] public EventCallback OnTracksChanged { get; set; }
|
|
[Parameter] public EventCallback OnStatusLoaded { get; set; }
|
|
|
|
private MudTable<TrackDto>? _table;
|
|
|
|
// EntryKey → HasProfile. Loaded once on init; per-row generate flips a single entry to true.
|
|
private Dictionary<string, bool> _waveformStatus = new();
|
|
private readonly HashSet<string> _generating = new();
|
|
|
|
// The parent owns "Generate All Missing"; while it runs it disables this grid's per-row buttons.
|
|
private bool _bulkRunning;
|
|
|
|
// The image endpoint (GET api/image/{entryKey}) lives on DeepDrftAPI and is unauthenticated, so
|
|
// the browser hits it directly. Base address comes from the same named client the CMS uses.
|
|
private Uri? _contentApiBase;
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
_contentApiBase = HttpClientFactory.CreateClient("DeepDrft.Content.Cms").BaseAddress;
|
|
await RefreshWaveformStatusAsync();
|
|
}
|
|
|
|
private bool HasProfile(string entryKey) =>
|
|
_waveformStatus.TryGetValue(entryKey, out var hasProfile) && hasProfile;
|
|
|
|
private string? ThumbUrl(string imagePath) =>
|
|
_contentApiBase is null
|
|
? null
|
|
: new Uri(_contentApiBase, $"api/image/{Uri.EscapeDataString(imagePath)}").ToString();
|
|
|
|
/// <summary>Number of tracks with a missing waveform profile — drives the parent's bulk button label.</summary>
|
|
public int GetMissingCount() => _waveformStatus.Count(kv => !kv.Value);
|
|
|
|
/// <summary>
|
|
/// Reload the full waveform-status map. Called on init and by the parent after a bulk generate so
|
|
/// the per-row icons reflect the new state.
|
|
/// </summary>
|
|
public async Task RefreshWaveformStatusAsync()
|
|
{
|
|
var result = await CmsTrackService.GetWaveformStatusAsync();
|
|
_waveformStatus = result.Success && result.Value is not null
|
|
? result.Value.ToDictionary(s => s.EntryKey, s => s.HasProfile)
|
|
: new Dictionary<string, bool>();
|
|
|
|
StateHasChanged();
|
|
await OnStatusLoaded.InvokeAsync();
|
|
}
|
|
|
|
/// <summary>Set by the parent while its bulk generate runs so per-row buttons disable.</summary>
|
|
public void SetBulkRunning(bool running)
|
|
{
|
|
_bulkRunning = running;
|
|
StateHasChanged();
|
|
}
|
|
|
|
private async Task<TableData<TrackDto>> LoadServerData(TableState state, CancellationToken cancellationToken)
|
|
{
|
|
var pageNumber = state.Page + 1; // MudTable is 0-based, service is 1-based.
|
|
var sortColumn = string.IsNullOrEmpty(state.SortLabel) ? "TrackName" : state.SortLabel;
|
|
var sortDescending = state.SortDirection == SortDirection.Descending;
|
|
|
|
var result = await CmsTrackService.GetPagedAsync(
|
|
pageNumber, state.PageSize, sortColumn, sortDescending,
|
|
AlbumFilter, GenreFilter, cancellationToken);
|
|
|
|
if (!result.Success || result.Value is null)
|
|
{
|
|
var errorText = result.Messages.FirstOrDefault()?.Message ?? "Unknown error";
|
|
Snackbar.Add($"Failed to load tracks: {errorText}", Severity.Error);
|
|
return new TableData<TrackDto> { Items = Array.Empty<TrackDto>(), TotalItems = 0 };
|
|
}
|
|
|
|
var page = result.Value;
|
|
return new TableData<TrackDto>
|
|
{
|
|
Items = page.Items,
|
|
TotalItems = page.TotalCount
|
|
};
|
|
}
|
|
|
|
private async Task ConfirmAndDelete(TrackDto track)
|
|
{
|
|
var confirmed = await DialogService.ShowMessageBox(
|
|
title: "Delete track",
|
|
markupMessage: new MarkupString($"Delete <strong>{WebUtility.HtmlEncode(track.TrackName)}</strong> by {WebUtility.HtmlEncode(track.Release?.Artist ?? "Unknown")}? This removes both the metadata row and the underlying audio entry."),
|
|
yesText: "Delete",
|
|
cancelText: "Cancel");
|
|
|
|
if (confirmed != true) return;
|
|
|
|
try
|
|
{
|
|
var result = await CmsTrackService.DeleteTrackAsync(track.Id);
|
|
if (result.Success)
|
|
{
|
|
Snackbar.Add($"Deleted '{track.TrackName}'.", Severity.Success);
|
|
if (_table is not null) await _table.ReloadServerData();
|
|
await OnTracksChanged.InvokeAsync();
|
|
}
|
|
else
|
|
{
|
|
var error = result.Messages.FirstOrDefault()?.Message ?? "Unknown error";
|
|
Snackbar.Add($"Delete failed: {error}", Severity.Error);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError(ex, "Delete failed for track {TrackId}", track.Id);
|
|
Snackbar.Add("Delete failed — please try again.", Severity.Error);
|
|
}
|
|
}
|
|
|
|
private async Task GenerateOneAsync(TrackDto track)
|
|
{
|
|
_generating.Add(track.EntryKey);
|
|
StateHasChanged();
|
|
try
|
|
{
|
|
var result = await CmsTrackService.GenerateWaveformProfileAsync(track.EntryKey);
|
|
if (result.Success)
|
|
{
|
|
_waveformStatus[track.EntryKey] = true;
|
|
Snackbar.Add($"Generated profile for '{track.TrackName}'.", Severity.Success);
|
|
}
|
|
else
|
|
{
|
|
var error = result.Messages.FirstOrDefault()?.Message ?? "Unknown error";
|
|
Snackbar.Add($"Generate failed for '{track.TrackName}': {error}", Severity.Error);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError(ex, "Waveform generation failed for {EntryKey}", track.EntryKey);
|
|
Snackbar.Add($"Generate failed for '{track.TrackName}' — please try again.", Severity.Error);
|
|
}
|
|
finally
|
|
{
|
|
_generating.Remove(track.EntryKey);
|
|
StateHasChanged();
|
|
}
|
|
}
|
|
}
|