diff --git a/DeepDrftManager/Components/Pages/Tracks/AlbumHeaderFields.razor b/DeepDrftManager/Components/Pages/Tracks/AlbumHeaderFields.razor index 89c70e9..113c85d 100644 --- a/DeepDrftManager/Components/Pages/Tracks/AlbumHeaderFields.razor +++ b/DeepDrftManager/Components/Pages/Tracks/AlbumHeaderFields.razor @@ -66,6 +66,9 @@ @@ -85,6 +88,15 @@ [Parameter] public IBrowserFile? SelectedImageFile { get; set; } [Parameter] public EventCallback 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 HeroImageFileChanged { get; set; } + + // Gates the hero file picker in SessionFields (threaded to MediumFields → SessionFields). + // Set true only on the BatchUpload create path; leave false/absent on all edit paths. + [Parameter] public bool AllowHeroUpload { get; set; } + // 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. [Parameter] public string? ExistingImagePath { get; set; } diff --git a/DeepDrftManager/Components/Pages/Tracks/BatchUpload.razor b/DeepDrftManager/Components/Pages/Tracks/BatchUpload.razor index 76ce17d..66ee1ea 100644 --- a/DeepDrftManager/Components/Pages/Tracks/BatchUpload.razor +++ b/DeepDrftManager/Components/Pages/Tracks/BatchUpload.razor @@ -26,6 +26,8 @@ Medium="_medium" MediumChanged="OnMediumChanged" @bind-SelectedImageFile="_selectedImageFile" + @bind-HeroImageFile="_heroImageFile" + AllowHeroUpload="true" Disabled="_uploading" /> @if (_medium == ReleaseMedium.Cut) @@ -71,6 +73,11 @@ @_errorMessage } + @if (!string.IsNullOrEmpty(_warningMessage)) + { + @_warningMessage + } + 0) @@ -336,6 +364,43 @@ } } + // 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); + } + } + else if (_medium == ReleaseMedium.Session && _heroImageFile is not null) + { + // ReleaseId was null on a Session track result — internal inconsistency. + // Hero file is held but cannot be uploaded without a release id; log and + // surface so the admin can set it from the Sessions browser. + Logger.LogWarning("Batch upload: Session track '{TrackName}' (id={Id}) has no ReleaseId — hero image dropped", + row.TrackName, result.Value.Id); + Snackbar.Add("Session uploaded, but the hero image could not be linked (no release id). Set it from the Sessions browser.", Severity.Warning); + } + // 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. // Non-blocking: the track is persisted; a failed trigger is recoverable from diff --git a/DeepDrftManager/Components/Pages/Tracks/MediumFields.razor b/DeepDrftManager/Components/Pages/Tracks/MediumFields.razor index 45a5cde..6e24668 100644 --- a/DeepDrftManager/Components/Pages/Tracks/MediumFields.razor +++ b/DeepDrftManager/Components/Pages/Tracks/MediumFields.razor @@ -1,4 +1,5 @@ @using DeepDrftModels.Enums +@using Microsoft.AspNetCore.Components.Forms @* 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 @@ -26,7 +27,10 @@ Disabled="Disabled" /> break; case ReleaseMedium.Session: - + break; case ReleaseMedium.Mix: @@ -42,5 +46,13 @@ [Parameter] public ReleaseType ReleaseType { get; set; } = ReleaseType.Single; [Parameter] public EventCallback 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 HeroImageFileChanged { get; set; } + + // Gates the hero file picker in SessionFields. True on the BatchUpload create path; + // false/absent on all edit paths so SessionFields falls back to the guidance alert. + [Parameter] public bool AllowHeroUpload { get; set; } + [Parameter] public bool Disabled { get; set; } } diff --git a/DeepDrftManager/Components/Pages/Tracks/SessionFields.razor b/DeepDrftManager/Components/Pages/Tracks/SessionFields.razor index 227dc7c..cee4777 100644 --- a/DeepDrftManager/Components/Pages/Tracks/SessionFields.razor +++ b/DeepDrftManager/Components/Pages/Tracks/SessionFields.razor @@ -1,10 +1,68 @@ -@* 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 - time when no release id is yet assigned. This section states that contract so the admin knows where - the hero image is managed; it carries no input of its own. *@ - - - Sessions are single-track live releases. After upload, set the hero image from the - Release Archive → Sessions browser. - - +@using Microsoft.AspNetCore.Components.Forms + +@* Session-medium fields. When AllowHeroUpload is true (BatchUpload create path), the hero image + file picker is shown — the file is held by the parent and POSTed after the release is created. + When false (edit paths: BatchEdit, TrackEdit, TrackNew), the original guidance alert is rendered + instead, directing the admin to the Sessions browser per-row replace action. This gate prevents + a dead/inert control on edit forms that do not wire the hero callbacks. *@ +@if (AllowHeroUpload) +{ + + + + + Sessions are single-track live releases. The hero image is the session's primary visual identity. + + + @if (HeroImageFile is { } selectedHero) + { + + Selected: @selectedHero.Name + + + } + else + { + No hero image — optional, but recommended. + } + + + @if (HeroImageFile is not null) + { + Will upload on submit. + } + + + +} +else +{ + + + Sessions are single-track live releases. After upload, set the hero image from the + Release Archive → Sessions browser. + + +} + +@code { + [Parameter] public IBrowserFile? HeroImageFile { get; set; } + [Parameter] public EventCallback HeroImageFileChanged { get; set; } + [Parameter] public bool Disabled { get; set; } + + // When true (BatchUpload create path), render the hero file picker. + // When false/absent (edit paths), render the guidance alert directing the admin to the + // Sessions browser — no dead control where callbacks are unwired. + [Parameter] public bool AllowHeroUpload { get; set; } + + private Task HandleHeroFileSelected(InputFileChangeEventArgs e) => + HeroImageFileChanged.InvokeAsync(e.File); + + private Task ClearHeroFile() => + HeroImageFileChanged.InvokeAsync(null); +}