fc32791cea
MixBrowser WaveformCell: wrap icon+button in MudStack Row. SessionBrowser HeroCell: split into two SpecialActionColumns (thumb + button). AlbumBrowser track table: always show regenerate button for Profile and High-res.
189 lines
7.8 KiB
Plaintext
189 lines
7.8 KiB
Plaintext
@page "/tracks/sessions"
|
|
@inherits CmsMediumBrowserBase<CmsSessionBrowser.SessionRow>
|
|
@using DeepDrftModels.DTOs
|
|
@using DeepDrftModels.Enums
|
|
@using Microsoft.AspNetCore.Components.Forms
|
|
@attribute [Authorize]
|
|
@inject ILogger<CmsSessionBrowser> Logger
|
|
|
|
@* Embedded as the SESSIONS tab content of the Release Archive (Phase 9 §8.A), and still routable at
|
|
/tracks/sessions for direct-URL access. The grid is the rich CmsAlbumBrowser filtered to Sessions
|
|
(§8.C parity: expand-tracks, delete, Type chip, per-row edit), with the Session hero upload supplied
|
|
as its medium-specific special-action column so that affordance survives the move off the thin table.
|
|
When embedded, the page chrome (title, container, the now-meaningless "Back to Release Archive"
|
|
button) is suppressed; the standalone route keeps it. The hero affordance (9.5.E) is preserved in
|
|
both contexts. *@
|
|
@if (Embedded)
|
|
{
|
|
@GridContent
|
|
}
|
|
else
|
|
{
|
|
<PageTitle>Sessions — DeepDrft CMS</PageTitle>
|
|
|
|
<MudContainer MaxWidth="MaxWidth.Large" Class="mt-8">
|
|
<MudButton Variant="Variant.Text"
|
|
StartIcon="@Icons.Material.Filled.ArrowBack"
|
|
Href="/releases"
|
|
Class="mb-4">
|
|
Back to Releases
|
|
</MudButton>
|
|
|
|
<MudText Typo="Typo.h4" GutterBottom="true">Sessions</MudText>
|
|
|
|
@GridContent
|
|
</MudContainer>
|
|
}
|
|
|
|
@code {
|
|
/// <summary>
|
|
/// True when rendered as tab content inside the Release Archive; suppresses the standalone page
|
|
/// chrome (title, container, back button). False (default) renders the full routable page.
|
|
/// </summary>
|
|
[Parameter] public bool Embedded { get; set; }
|
|
|
|
/// <summary>
|
|
/// Forwarded from the inner <see cref="CmsAlbumBrowser"/>: fires after any per-row waveform
|
|
/// generate succeeds so the parent page can refresh its catalogue-wide missing-count badges.
|
|
/// </summary>
|
|
[Parameter] public EventCallback OnWaveformGenerated { get; set; }
|
|
|
|
private CmsAlbumBrowser? _albumBrowser;
|
|
|
|
protected override ReleaseMedium Medium => ReleaseMedium.Session;
|
|
protected override string MediumNoun => "sessions";
|
|
|
|
/// <summary>
|
|
/// Clears the inner grid's cached per-track waveform status so the next row expand re-fetches.
|
|
/// Called by the parent page after a catalogue-wide bulk run.
|
|
/// </summary>
|
|
public Task InvalidateWaveformStatusAsync() =>
|
|
_albumBrowser?.InvalidateWaveformStatusAsync() ?? Task.CompletedTask;
|
|
|
|
protected override SessionRow ToRow(ReleaseDto release) => new()
|
|
{
|
|
Release = release,
|
|
HeroImageEntryKey = release.SessionMetadata?.HeroImageEntryKey
|
|
};
|
|
|
|
protected override ReleaseDto ReleaseOf(SessionRow row) => row.Release;
|
|
|
|
// The grid itself — identical in the embedded and standalone contexts. Defined once as a fragment so
|
|
// both branches above render the same markup without duplication. The Session declares one dedicated
|
|
// "Hero" special-action column; the grid renders it between Tracks and Actions, handing the cell each
|
|
// release, and RowFor recovers the matching SessionRow's upload state.
|
|
private RenderFragment GridContent => @<CmsAlbumBrowser @ref="_albumBrowser"
|
|
Releases="Releases"
|
|
IsLoading="Loading"
|
|
OnReleasesChanged="ReloadAsync"
|
|
OnWaveformGenerated="OnWaveformGenerated"
|
|
SpecialColumns="_specialColumns" />;
|
|
|
|
// Allocated once per component instance in OnInitialized (field initializers cannot reference
|
|
// instance members, so initialization is deferred to the first lifecycle hook).
|
|
private IReadOnlyList<SpecialActionColumn> _specialColumns = Array.Empty<SpecialActionColumn>();
|
|
|
|
protected override void OnInitialized()
|
|
{
|
|
_specialColumns = new[]
|
|
{
|
|
new SpecialActionColumn("Hero", HeroThumbCell),
|
|
new SpecialActionColumn("", HeroButtonCell),
|
|
};
|
|
base.OnInitialized();
|
|
}
|
|
|
|
// Per-row cell for the "Hero" thumbnail column: just the image preview div.
|
|
private RenderFragment<ReleaseDto> HeroThumbCell => release =>@<text>
|
|
@{ var row = RowFor(release); }
|
|
@if (row is not null)
|
|
{
|
|
@if (row.HeroImageEntryKey is { Length: > 0 } heroKey)
|
|
{
|
|
<div class="cms-album-thumb" style="background-image: url('@ThumbUrl(heroKey)');"></div>
|
|
}
|
|
else
|
|
{
|
|
<div class="cms-album-thumb cms-album-thumb--fallback"></div>
|
|
}
|
|
}
|
|
</text>;
|
|
|
|
// Per-row cell for the "Hero Image" upload button column: set/replace upload button with progress.
|
|
private RenderFragment<ReleaseDto> HeroButtonCell => release =>@<text>
|
|
@{ var row = RowFor(release); }
|
|
@if (row is not null)
|
|
{
|
|
<MudFileUpload T="IBrowserFile"
|
|
Accept="image/*"
|
|
FilesChanged="@(file => UploadHeroAsync(row, file))"
|
|
Disabled="@row.IsUploading">
|
|
<ActivatorContent>
|
|
<MudButton Variant="Variant.Outlined"
|
|
Size="Size.Small"
|
|
StartIcon="@Icons.Material.Filled.Image"
|
|
Disabled="@row.IsUploading">
|
|
@if (row.IsUploading)
|
|
{
|
|
<MudProgressCircular Class="mr-2" Size="Size.Small" Indeterminate="true" />
|
|
<span>Uploading…</span>
|
|
}
|
|
else
|
|
{
|
|
<span>@(row.HeroImageEntryKey is { Length: > 0 } ? "Replace hero" : "Set hero")</span>
|
|
}
|
|
</MudButton>
|
|
</ActivatorContent>
|
|
</MudFileUpload>
|
|
}
|
|
</text>;
|
|
|
|
private async Task UploadHeroAsync(SessionRow row, IBrowserFile? file)
|
|
{
|
|
if (file is null) return;
|
|
row.IsUploading = true;
|
|
StateHasChanged();
|
|
try
|
|
{
|
|
await using var stream = file.OpenReadStream(maxAllowedSize: 50_000_000);
|
|
var result = await CmsReleaseService.UploadSessionHeroImageAsync(
|
|
row.Release.Id, stream, file.Name, file.ContentType);
|
|
|
|
if (result.Success)
|
|
{
|
|
// The endpoint returns no payload; the entry key is server-generated. Re-fetch the
|
|
// release so the hero thumbnail reflects the new key without guessing it.
|
|
var refreshed = await CmsReleaseService.GetByIdAsync(row.Release.Id);
|
|
if (refreshed.Success && refreshed.Value is { } release)
|
|
{
|
|
row.Release = release;
|
|
row.HeroImageEntryKey = release.SessionMetadata?.HeroImageEntryKey;
|
|
}
|
|
Snackbar.Add($"Hero image set for '{row.Release.Title}'.", Severity.Success);
|
|
}
|
|
else
|
|
{
|
|
var error = result.Messages.FirstOrDefault()?.Message ?? "Unknown error";
|
|
Snackbar.Add($"Hero image upload failed: {error}", Severity.Error);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError(ex, "Hero image upload failed for release {ReleaseId}", row.Release.Id);
|
|
Snackbar.Add("Hero image upload failed — please try again.", Severity.Error);
|
|
}
|
|
finally
|
|
{
|
|
row.IsUploading = false;
|
|
StateHasChanged();
|
|
}
|
|
}
|
|
|
|
public sealed class SessionRow
|
|
{
|
|
public required ReleaseDto Release { get; set; }
|
|
public string? HeroImageEntryKey { get; set; }
|
|
public bool IsUploading { get; set; }
|
|
}
|
|
}
|