feat(cms): add Track Browser foundation with mode toggle and CmsTrackGrid
- 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
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
using DeepDrftModels.DTOs;
|
||||
|
||||
namespace DeepDrftManager.Services;
|
||||
|
||||
/// <summary>The three browse dimensions for the /tracks page.</summary>
|
||||
public enum BrowseMode
|
||||
{
|
||||
Tracks,
|
||||
Albums,
|
||||
Genres,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Holds the /tracks browser's current mode plus the album- and genre-mode datasets. Scoped per
|
||||
/// circuit. Album and genre lists are fetched lazily on first switch into their mode and cached for
|
||||
/// the circuit's lifetime; Track mode owns its own paging inside <c>CmsTrackGrid</c> and needs no
|
||||
/// state here.
|
||||
/// </summary>
|
||||
public class CmsTrackBrowserViewModel
|
||||
{
|
||||
private readonly ICmsTrackService _trackService;
|
||||
|
||||
public CmsTrackBrowserViewModel(ICmsTrackService trackService)
|
||||
{
|
||||
_trackService = trackService;
|
||||
}
|
||||
|
||||
public BrowseMode Mode { get; private set; } = BrowseMode.Tracks;
|
||||
|
||||
// Album mode.
|
||||
public IReadOnlyList<ReleaseDto> Albums { get; private set; } = Array.Empty<ReleaseDto>();
|
||||
public bool AlbumsLoading { get; private set; }
|
||||
|
||||
// Genre mode.
|
||||
public IReadOnlyList<GenreSummaryDto> Genres { get; private set; } = Array.Empty<GenreSummaryDto>();
|
||||
public bool GenresLoading { get; private set; }
|
||||
public string? ExpandedGenre { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Switch the active mode, lazily loading the album or genre dataset on first entry. Collapses
|
||||
/// any expanded genre row. The grid in Track mode owns its own data, so no fetch happens there.
|
||||
/// </summary>
|
||||
public async Task SwitchModeAsync(BrowseMode mode)
|
||||
{
|
||||
Mode = mode;
|
||||
ExpandedGenre = null; // collapse on mode switch
|
||||
|
||||
if (mode == BrowseMode.Albums && Albums.Count == 0 && !AlbumsLoading)
|
||||
{
|
||||
AlbumsLoading = true;
|
||||
var result = await _trackService.GetReleasesAsync();
|
||||
Albums = result.Success && result.Value is not null
|
||||
? result.Value
|
||||
: Array.Empty<ReleaseDto>();
|
||||
AlbumsLoading = false;
|
||||
}
|
||||
else if (mode == BrowseMode.Genres && Genres.Count == 0 && !GenresLoading)
|
||||
{
|
||||
GenresLoading = true;
|
||||
var result = await _trackService.GetGenreSummariesAsync();
|
||||
Genres = result.Success && result.Value is not null
|
||||
? result.Value
|
||||
: Array.Empty<GenreSummaryDto>();
|
||||
GenresLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Toggle the expanded genre row. Selecting the already-expanded genre collapses it.</summary>
|
||||
public void SetExpandedGenre(string? genre)
|
||||
{
|
||||
ExpandedGenre = ExpandedGenre == genre ? null : genre;
|
||||
}
|
||||
}
|
||||
@@ -154,6 +154,7 @@ public class CmsTrackService : ICmsTrackService
|
||||
|
||||
public async Task<ResultContainer<PagedResult<TrackDto>>> GetPagedAsync(
|
||||
int page, int pageSize, string? sortColumn, bool sortDescending,
|
||||
string? album = null, string? genre = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var client = _httpClientFactory.CreateClient(ContentCmsClientName);
|
||||
@@ -162,6 +163,14 @@ public class CmsTrackService : ICmsTrackService
|
||||
{
|
||||
query += $"&sortColumn={Uri.EscapeDataString(sortColumn)}";
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(album))
|
||||
{
|
||||
query += $"&album={Uri.EscapeDataString(album)}";
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(genre))
|
||||
{
|
||||
query += $"&genre={Uri.EscapeDataString(genre)}";
|
||||
}
|
||||
|
||||
HttpResponseMessage response;
|
||||
try
|
||||
@@ -542,7 +551,7 @@ public class CmsTrackService : ICmsTrackService
|
||||
{
|
||||
// Re-use the paged endpoint: a single-item page carries the full TotalCount, so no
|
||||
// dedicated count endpoint is needed.
|
||||
var paged = await GetPagedAsync(page: 1, pageSize: 1, sortColumn: null, sortDescending: false, ct);
|
||||
var paged = await GetPagedAsync(page: 1, pageSize: 1, sortColumn: null, sortDescending: false, ct: ct);
|
||||
if (!paged.Success || paged.Value is null)
|
||||
{
|
||||
var error = paged.Messages.FirstOrDefault()?.Message ?? "Failed to load track count.";
|
||||
|
||||
@@ -41,10 +41,13 @@ public interface ICmsTrackService
|
||||
Task<Result> DeleteTrackAsync(long id, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Fetch a page of track metadata from the Content API's <c>GET api/track/page</c>.
|
||||
/// Fetch a page of track metadata from the Content API's <c>GET api/track/page</c>. Optional
|
||||
/// <paramref name="album"/> and <paramref name="genre"/> filters narrow the result to a single
|
||||
/// release title or genre; null leaves the dimension unfiltered.
|
||||
/// </summary>
|
||||
Task<ResultContainer<PagedResult<TrackDto>>> GetPagedAsync(
|
||||
int page, int pageSize, string? sortColumn, bool sortDescending,
|
||||
string? album = null, string? genre = null,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user