feat(cms): compose Session hero image into the upload form (8.F)
Session upload now carries a deferred hero-image input; the submit handler creates the release then POSTs the held hero to the existing resource-addressed endpoint. Hero is optional with a non-blocking warn-if-missing gate. The per-row hero upload in CmsSessionBrowser remains the replace/correct path.
This commit is contained in:
@@ -66,6 +66,8 @@
|
|||||||
|
|
||||||
<MediumFields @bind-Medium="MediumBinding"
|
<MediumFields @bind-Medium="MediumBinding"
|
||||||
@bind-ReleaseType="ReleaseTypeBinding"
|
@bind-ReleaseType="ReleaseTypeBinding"
|
||||||
|
HeroImageFile="HeroImageFile"
|
||||||
|
HeroImageFileChanged="HeroImageFileChanged"
|
||||||
Disabled="Disabled" />
|
Disabled="Disabled" />
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
|
|
||||||
@@ -85,6 +87,11 @@
|
|||||||
[Parameter] public IBrowserFile? SelectedImageFile { get; set; }
|
[Parameter] public IBrowserFile? SelectedImageFile { get; set; }
|
||||||
[Parameter] public EventCallback<IBrowserFile?> SelectedImageFileChanged { get; set; }
|
[Parameter] public EventCallback<IBrowserFile?> SelectedImageFileChanged { get; set; }
|
||||||
|
|
||||||
|
// Session-only — the held hero-image file, threaded through MediumFields to SessionFields.
|
||||||
|
// Ignored for Cut/Mix media. The parent (BatchUpload) owns it and uploads it after create.
|
||||||
|
[Parameter] public IBrowserFile? HeroImageFile { get; set; }
|
||||||
|
[Parameter] public EventCallback<IBrowserFile?> HeroImageFileChanged { get; set; }
|
||||||
|
|
||||||
// BatchEdit only: when set (and no new file picked), preview the release's current cover.
|
// BatchEdit only: when set (and no new file picked), preview the release's current cover.
|
||||||
// The parent nulls this to drop the preview when the admin clears the existing cover.
|
// The parent nulls this to drop the preview when the admin clears the existing cover.
|
||||||
[Parameter] public string? ExistingImagePath { get; set; }
|
[Parameter] public string? ExistingImagePath { get; set; }
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
Medium="_medium"
|
Medium="_medium"
|
||||||
MediumChanged="OnMediumChanged"
|
MediumChanged="OnMediumChanged"
|
||||||
@bind-SelectedImageFile="_selectedImageFile"
|
@bind-SelectedImageFile="_selectedImageFile"
|
||||||
|
@bind-HeroImageFile="_heroImageFile"
|
||||||
Disabled="_uploading" />
|
Disabled="_uploading" />
|
||||||
|
|
||||||
@if (_medium == ReleaseMedium.Cut)
|
@if (_medium == ReleaseMedium.Cut)
|
||||||
@@ -108,6 +109,12 @@
|
|||||||
private IBrowserFile? _selectedImageFile;
|
private IBrowserFile? _selectedImageFile;
|
||||||
private string? _imagePath;
|
private string? _imagePath;
|
||||||
|
|
||||||
|
// Session-only: the hero image is resource-addressed and cannot be uploaded until the release
|
||||||
|
// exists, so it is held here and POSTed to api/release/{id}/session/hero-image after create.
|
||||||
|
private IBrowserFile? _heroImageFile;
|
||||||
|
// Set true once the admin has acknowledged the missing-hero warning, so a second submit proceeds.
|
||||||
|
private bool _heroWarningAcknowledged;
|
||||||
|
|
||||||
private string _albumName = string.Empty;
|
private string _albumName = string.Empty;
|
||||||
private string _artist = string.Empty;
|
private string _artist = string.Empty;
|
||||||
private string _genre = string.Empty;
|
private string _genre = string.Empty;
|
||||||
@@ -244,6 +251,18 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A Session's hero is its primary visual identity on the public detail page. It is optional —
|
||||||
|
// a Session can be authored without one and set later from the Sessions browser — but a missing
|
||||||
|
// hero is usually an oversight, so warn (do not block). The first submit without a hero shows the
|
||||||
|
// warning and primes acknowledgment; a second submit proceeds.
|
||||||
|
if (_medium == ReleaseMedium.Session && _heroImageFile is null && !_heroWarningAcknowledged)
|
||||||
|
{
|
||||||
|
_heroWarningAcknowledged = true;
|
||||||
|
_errorMessage = "No hero image selected. A Session usually needs one — you can add it now, "
|
||||||
|
+ "or submit again to create the Session without it (set the hero later from the Sessions browser).";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// For single-track media (Session/Mix) the track name is derived from the Release Name —
|
// For single-track media (Session/Mix) the track name is derived from the Release Name —
|
||||||
// no separate Track Name input is shown. Sync here so the stored name always matches.
|
// no separate Track Name input is shown. Sync here so the stored name always matches.
|
||||||
if (MediumRules.CardinalityOf(_medium).IsSingleTrack && _tracks.Count > 0)
|
if (MediumRules.CardinalityOf(_medium).IsSingleTrack && _tracks.Count > 0)
|
||||||
@@ -336,6 +355,34 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Session hero image is resource-addressed, so it is uploaded here — after the
|
||||||
|
// release exists and we have its id — within the same submit gesture. Non-blocking:
|
||||||
|
// the Session is persisted; a failed hero upload is recoverable from the Sessions
|
||||||
|
// browser's per-row Set/Replace hero action.
|
||||||
|
if (_medium == ReleaseMedium.Session
|
||||||
|
&& _heroImageFile is { } heroFile
|
||||||
|
&& result.Value.ReleaseId is { } sessionReleaseId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using var heroStream = heroFile.OpenReadStream(maxAllowedSize: 50_000_000);
|
||||||
|
var heroResult = await CmsReleaseService.UploadSessionHeroImageAsync(
|
||||||
|
sessionReleaseId, heroStream, heroFile.Name, heroFile.ContentType);
|
||||||
|
if (!heroResult.Success)
|
||||||
|
{
|
||||||
|
var heroError = heroResult.Messages.FirstOrDefault()?.Message ?? "Unknown error";
|
||||||
|
Logger.LogWarning("Batch upload: hero image upload failed for release {ReleaseId} ('{TrackName}'): {Error}",
|
||||||
|
sessionReleaseId, row.TrackName, heroError);
|
||||||
|
Snackbar.Add("Session uploaded, but the hero image failed. Set it from the Sessions browser.", Severity.Warning);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception heroEx)
|
||||||
|
{
|
||||||
|
Logger.LogError(heroEx, "Batch upload: exception uploading hero image for release {ReleaseId}", sessionReleaseId);
|
||||||
|
Snackbar.Add("Session uploaded, but the hero image failed. Set it from the Sessions browser.", Severity.Warning);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Mix uploads fire the server-side high-res waveform trigger (§3.4). The CMS
|
// Mix uploads fire the server-side high-res waveform trigger (§3.4). The CMS
|
||||||
// computes nothing — the API derives the datum from the audio it just stored.
|
// computes nothing — the API derives the datum from the audio it just stored.
|
||||||
// Non-blocking: the track is persisted; a failed trigger is recoverable from
|
// Non-blocking: the track is persisted; a failed trigger is recoverable from
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
@using DeepDrftModels.Enums
|
@using DeepDrftModels.Enums
|
||||||
|
@using Microsoft.AspNetCore.Components.Forms
|
||||||
|
|
||||||
@* The single dispatch point for medium-conditional form fields. All five upload/edit forms embed this
|
@* The single dispatch point for medium-conditional form fields. All five upload/edit forms embed this
|
||||||
one component; the @switch below is the ONLY place medium-specific form shape is decided. Adding a
|
one component; the @switch below is the ONLY place medium-specific form shape is decided. Adding a
|
||||||
@@ -26,7 +27,9 @@
|
|||||||
Disabled="Disabled" />
|
Disabled="Disabled" />
|
||||||
break;
|
break;
|
||||||
case ReleaseMedium.Session:
|
case ReleaseMedium.Session:
|
||||||
<SessionFields />
|
<SessionFields HeroImageFile="HeroImageFile"
|
||||||
|
HeroImageFileChanged="HeroImageFileChanged"
|
||||||
|
Disabled="Disabled" />
|
||||||
break;
|
break;
|
||||||
case ReleaseMedium.Mix:
|
case ReleaseMedium.Mix:
|
||||||
<MixFields />
|
<MixFields />
|
||||||
@@ -42,5 +45,9 @@
|
|||||||
[Parameter] public ReleaseType ReleaseType { get; set; } = ReleaseType.Single;
|
[Parameter] public ReleaseType ReleaseType { get; set; } = ReleaseType.Single;
|
||||||
[Parameter] public EventCallback<ReleaseType> ReleaseTypeChanged { get; set; }
|
[Parameter] public EventCallback<ReleaseType> ReleaseTypeChanged { get; set; }
|
||||||
|
|
||||||
|
// Session-only — the held hero-image file, uploaded after create. Ignored for Cut/Mix.
|
||||||
|
[Parameter] public IBrowserFile? HeroImageFile { get; set; }
|
||||||
|
[Parameter] public EventCallback<IBrowserFile?> HeroImageFileChanged { get; set; }
|
||||||
|
|
||||||
[Parameter] public bool Disabled { get; set; }
|
[Parameter] public bool Disabled { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,51 @@
|
|||||||
|
@using Microsoft.AspNetCore.Components.Forms
|
||||||
|
|
||||||
@* Session-medium fields. The hero image is resource-addressed (POST api/release/{id}/session/hero-image)
|
@* Session-medium fields. The hero image is resource-addressed (POST api/release/{id}/session/hero-image)
|
||||||
and therefore set after the release exists — managed per-row in the Sessions browser, not at create
|
and therefore cannot be uploaded until the release exists. The file is selected here, held by the
|
||||||
time when no release id is yet assigned. This section states that contract so the admin knows where
|
parent form, and uploaded after the create call returns a release id — the same deferred-upload
|
||||||
the hero image is managed; it carries no input of its own. *@
|
pattern AlbumHeaderFields uses for cover art ("Will upload on submit"). The hero is optional; the
|
||||||
|
parent warns (does not block) when a Session is submitted without one. *@
|
||||||
<MudItem xs="12">
|
<MudItem xs="12">
|
||||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Outlined">
|
<MudField Label="Hero Image" Variant="Variant.Outlined" InnerPadding="false">
|
||||||
Sessions are single-track live releases. After upload, set the hero image from the
|
<MudStack Spacing="3">
|
||||||
<strong>Release Archive → Sessions</strong> browser.
|
<MudText Typo="Typo.body2" Color="Color.Default">
|
||||||
</MudAlert>
|
Sessions are single-track live releases. The hero image is the session's primary visual identity.
|
||||||
|
</MudText>
|
||||||
|
|
||||||
|
@if (HeroImageFile is { } selectedHero)
|
||||||
|
{
|
||||||
|
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||||
|
<MudText Typo="Typo.body2" Color="Color.Default">Selected: @selectedHero.Name</MudText>
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.Clear"
|
||||||
|
Color="Color.Error"
|
||||||
|
Size="Size.Small"
|
||||||
|
Disabled="Disabled"
|
||||||
|
OnClick="ClearHeroFile"
|
||||||
|
aria-label="Cancel hero image selection" />
|
||||||
|
</MudStack>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudText Typo="Typo.body2" Color="Color.Default">No hero image — optional, but recommended.</MudText>
|
||||||
|
}
|
||||||
|
|
||||||
|
<InputFile OnChange="HandleHeroFileSelected" accept="image/*" disabled="@Disabled" />
|
||||||
|
@if (HeroImageFile is not null)
|
||||||
|
{
|
||||||
|
<MudText Typo="Typo.caption">Will upload on submit.</MudText>
|
||||||
|
}
|
||||||
|
</MudStack>
|
||||||
|
</MudField>
|
||||||
</MudItem>
|
</MudItem>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter] public IBrowserFile? HeroImageFile { get; set; }
|
||||||
|
[Parameter] public EventCallback<IBrowserFile?> HeroImageFileChanged { get; set; }
|
||||||
|
[Parameter] public bool Disabled { get; set; }
|
||||||
|
|
||||||
|
private Task HandleHeroFileSelected(InputFileChangeEventArgs e) =>
|
||||||
|
HeroImageFileChanged.InvokeAsync(e.File);
|
||||||
|
|
||||||
|
private Task ClearHeroFile() =>
|
||||||
|
HeroImageFileChanged.InvokeAsync(null);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user