diff --git a/DeepDrftData/TrackManager.cs b/DeepDrftData/TrackManager.cs index 043d3c8..54ea6ae 100644 --- a/DeepDrftData/TrackManager.cs +++ b/DeepDrftData/TrackManager.cs @@ -117,8 +117,9 @@ public class TrackManager "Artist" => e => (object)(e.Release == null ? string.Empty : e.Release.Artist), "Album" => e => (object)(e.Release == null ? string.Empty : e.Release.Title), "Genre" => e => (object)(e.Release == null ? string.Empty : (e.Release.Genre ?? string.Empty)), - "ReleaseDate" => e => (object)(e.Release == null ? DateOnly.MaxValue : (e.Release.ReleaseDate ?? DateOnly.MaxValue)), - _ => e => e.Id + "ReleaseDate" => e => (object)(e.Release == null ? DateOnly.MaxValue : (e.Release.ReleaseDate ?? DateOnly.MaxValue)), + "TrackNumber" => e => e.TrackNumber, + _ => e => e.Id } }; diff --git a/DeepDrftManager/Components/Pages/Tracks/AlbumHeaderFields.razor b/DeepDrftManager/Components/Pages/Tracks/AlbumHeaderFields.razor new file mode 100644 index 0000000..4989bd0 --- /dev/null +++ b/DeepDrftManager/Components/Pages/Tracks/AlbumHeaderFields.razor @@ -0,0 +1,115 @@ +@using DeepDrftModels.Enums +@using Microsoft.AspNetCore.Components.Forms +@inject IHttpClientFactory HttpClientFactory + + + + + + + + + + + + + + + + + + @foreach (var rt in Enum.GetValues()) + { + @rt + } + + + + + + @if (SelectedImageFile is { } selectedImage) + { + + Selected: @selectedImage.Name + + + } + else if (ExistingImagePreviewUrl is { } previewUrl) + { + + + Current cover art. + + } + else + { + No cover art — optional. + } + + + @if (SelectedImageFile is not null) + { + Will upload on submit. + } + + + + + + +@code { + [Parameter] public string AlbumName { get; set; } = string.Empty; + [Parameter] public EventCallback AlbumNameChanged { get; set; } + [Parameter] public string Artist { get; set; } = string.Empty; + [Parameter] public EventCallback ArtistChanged { get; set; } + [Parameter] public string Genre { get; set; } = string.Empty; + [Parameter] public EventCallback GenreChanged { get; set; } + [Parameter] public string ReleaseDate { get; set; } = string.Empty; + [Parameter] public EventCallback ReleaseDateChanged { get; set; } + [Parameter] public ReleaseType ReleaseType { get; set; } = ReleaseType.Single; + [Parameter] public EventCallback ReleaseTypeChanged { get; set; } + [Parameter] public IBrowserFile? SelectedImageFile { get; set; } + [Parameter] public EventCallback SelectedImageFileChanged { 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; } + + [Parameter] public bool Disabled { get; set; } + + // The image endpoint (GET api/image/{entryKey}) is unauthenticated, so the browser hits + // DeepDrftAPI directly. Base address comes from the same named client the CMS uses. + private string? ExistingImagePreviewUrl + { + get + { + if (string.IsNullOrEmpty(ExistingImagePath)) return null; + var baseAddress = HttpClientFactory.CreateClient("DeepDrft.Content.Cms").BaseAddress; + return baseAddress is null + ? null + : new Uri(baseAddress, $"api/image/{Uri.EscapeDataString(ExistingImagePath)}").ToString(); + } + } + + private Task HandleImageFileSelected(InputFileChangeEventArgs e) => + SelectedImageFileChanged.InvokeAsync(e.File); + + private Task ClearSelectedFile() => + SelectedImageFileChanged.InvokeAsync(null); +} diff --git a/DeepDrftManager/Components/Pages/Tracks/BatchEdit.razor b/DeepDrftManager/Components/Pages/Tracks/BatchEdit.razor new file mode 100644 index 0000000..b993a56 --- /dev/null +++ b/DeepDrftManager/Components/Pages/Tracks/BatchEdit.razor @@ -0,0 +1,467 @@ +@page "/tracks/album/{AlbumName}/edit" +@using System.Security.Claims +@using DeepDrftManager.Services +@using DeepDrftModels.DTOs +@using DeepDrftModels.Enums +@using Microsoft.AspNetCore.Components.Forms +@attribute [Authorize] + +@inject ICmsTrackService CmsTrackService +@inject AuthenticationStateProvider AuthStateProvider +@inject NavigationManager Navigation +@inject ISnackbar Snackbar +@inject IDialogService DialogService +@inject ILogger Logger + +Edit Release — DeepDrft CMS + + + Edit Release + + @if (_loading) + { + + } + else if (_loadError is { } loadError) + { + @loadError + } + else + { + + + @if (_existingImagePath is not null && _selectedImageFile is null) + { + + + Remove cover + + + } + + + + + + + + + + + + + + @if (!string.IsNullOrEmpty(_errorMessage)) + { + @_errorMessage + } + + + + Cancel + + + @if (_saving) + { + + Saving @_processedCount / @_tracks.Count… + } + else + { + Save Changes + } + + + } + + +@code { + // 1 GB ceiling matches DeepDrftAPI's per-request limit on api/track/upload. + private const long MaxUploadBytes = 1_073_741_824L; + + [Parameter] public string AlbumName { get; set; } = string.Empty; + + private List _tracks = new(); + private int _selectedIndex = -1; + private bool _loading = true; + private string? _loadError; + private bool _saving; + private int _processedCount; + private string? _errorMessage; + + private IBrowserFile? _selectedImageFile; + private string? _imagePath; + private string? _existingImagePath; + private bool _clearExistingImage; + + private string _albumName = string.Empty; + private string _artist = string.Empty; + private string _genre = string.Empty; + private string _releaseDate = string.Empty; + private ReleaseType _releaseType = ReleaseType.Single; + + protected override async Task OnInitializedAsync() + { + // A single page of 100 covers the full release (albums are small — same assumption as + // CmsAlbumBrowser). Sorted by track number so list order matches the saved ordinals. + var result = await CmsTrackService.GetPagedAsync( + page: 1, pageSize: 100, + sortColumn: "TrackNumber", sortDescending: false, + album: AlbumName); + + if (!result.Success || result.Value is null) + { + _loadError = result.Messages.FirstOrDefault()?.Message ?? "Failed to load release."; + _loading = false; + return; + } + + var tracks = result.Value.Items.ToList(); + if (tracks.Count == 0) + { + _loadError = $"No tracks found for release '{AlbumName}'."; + _loading = false; + return; + } + + var release = tracks[0].Release; + _albumName = AlbumName; + _artist = release?.Artist ?? string.Empty; + _genre = release?.Genre ?? string.Empty; + _releaseDate = release?.ReleaseDate?.ToString("yyyy-MM-dd") ?? string.Empty; + _releaseType = release?.ReleaseType ?? ReleaseType.Single; + _existingImagePath = release?.ImagePath; + + _tracks = tracks.Select(t => new BatchRowModel + { + Id = t.Id, + EntryKey = t.EntryKey, + OriginalFileName = t.OriginalFileName, + TrackName = t.TrackName, + TrackNumber = t.TrackNumber, + WavFile = null, + Status = BatchRowStatus.Queued + }).ToList(); + + _selectedIndex = _tracks.Count > 0 ? 0 : -1; + _loading = false; + } + + private void HandleWavFilesSelected(IReadOnlyList files) + { + _errorMessage = null; + foreach (var file in files) + { + if (!file.Name.EndsWith(".wav", StringComparison.OrdinalIgnoreCase)) + { + Snackbar.Add($"Skipped '{file.Name}' — not a .wav file.", Severity.Warning); + continue; + } + + // New rows carry no Id — they take the upload path on save. + _tracks.Add(new BatchRowModel + { + WavFile = file, + TrackName = Path.GetFileNameWithoutExtension(file.Name) + }); + } + + if (_selectedIndex < 0 && _tracks.Count > 0) + { + _selectedIndex = 0; + } + } + + private void MoveUp(int i) + { + if (i == 0) return; + (_tracks[i], _tracks[i - 1]) = (_tracks[i - 1], _tracks[i]); + if (_selectedIndex == i) _selectedIndex = i - 1; + else if (_selectedIndex == i - 1) _selectedIndex = i; + } + + private void MoveDown(int i) + { + if (i == _tracks.Count - 1) return; + (_tracks[i], _tracks[i + 1]) = (_tracks[i + 1], _tracks[i]); + if (_selectedIndex == i) _selectedIndex = i + 1; + else if (_selectedIndex == i + 1) _selectedIndex = i; + } + + private async Task RemoveRow(int index) + { + var row = _tracks[index]; + if (row.Id.HasValue) + { + // Existing track — confirm before deleting. + var confirmed = await DialogService.ShowMessageBox( + "Remove track", + $"Remove '{row.TrackName}' from this release? This deletes the track permanently.", + yesText: "Remove", cancelText: "Cancel"); + if (confirmed != true) return; + + var result = await CmsTrackService.DeleteTrackAsync(row.Id.Value); + if (!result.Success) + { + var error = result.Messages.FirstOrDefault()?.Message ?? "Unknown error"; + Snackbar.Add($"Delete failed: {error}", Severity.Error); + return; + } + } + + // New track (not yet uploaded) or confirmed existing delete — remove from list. + _tracks.RemoveAt(index); + if (index < _selectedIndex) _selectedIndex--; + if (_selectedIndex >= _tracks.Count) _selectedIndex = _tracks.Count - 1; + } + + private void RemoveCover() + { + // Defer the actual clear to save: pass "" to UpdateAsync's tri-state imagePath. Nulling + // the existing path here drops the preview in AlbumHeaderFields. + _clearExistingImage = true; + _existingImagePath = null; + } + + private async Task SaveAsync() + { + _errorMessage = null; + + if (string.IsNullOrWhiteSpace(_albumName)) + { + _errorMessage = "Album Name is required."; + return; + } + + if (string.IsNullOrWhiteSpace(_artist)) + { + _errorMessage = "Artist is required."; + return; + } + + if (_tracks.Count == 0) + { + _errorMessage = "A release must have at least one track."; + return; + } + + if (!string.IsNullOrWhiteSpace(_releaseDate) + && !DateOnly.TryParseExact(_releaseDate, "yyyy-MM-dd", out _)) + { + _errorMessage = "Release Date must be in YYYY-MM-DD format."; + return; + } + + // New rows (no Id) need a WAV; existing rows keep their vault audio. + foreach (var t in _tracks) + { + if (!t.Id.HasValue && t.WavFile is null) + { + _errorMessage = $"'{t.TrackName}' has no WAV file selected."; + return; + } + } + + var authState = await AuthStateProvider.GetAuthenticationStateAsync(); + var userIdValue = authState.User.FindFirstValue(ClaimTypes.NameIdentifier); + if (!long.TryParse(userIdValue, out var createdByUserId)) + { + // [Authorize]/Admin-gated page — an unparseable id here is a configuration bug. + Logger.LogError("Authenticated user has no parseable NameIdentifier claim: {Value}", userIdValue); + _errorMessage = "Your session is missing a valid identifier. Please sign in again."; + return; + } + + DateOnly? releaseDate = string.IsNullOrWhiteSpace(_releaseDate) + ? null + : DateOnly.ParseExact(_releaseDate, "yyyy-MM-dd"); + var album = string.IsNullOrWhiteSpace(_albumName) ? null : _albumName; + var genre = string.IsNullOrWhiteSpace(_genre) ? null : _genre; + + _imagePath = null; // Clear any stale uploaded path from a prior partial attempt. + _saving = true; + _processedCount = 0; + + try + { + // Upload any newly picked cover art once; abort if it fails so we never point metadata + // at an image that was never stored. + if (_selectedImageFile is { } imgFile) + { + await using var imgStream = imgFile.OpenReadStream(maxAllowedSize: 50_000_000); + var imgResult = await CmsTrackService.UploadImageAsync(imgStream, imgFile.Name, imgFile.ContentType); + if (!imgResult.Success) + { + var imgError = imgResult.Messages.FirstOrDefault()?.Message ?? "Unknown error"; + _errorMessage = $"Image upload failed: {imgError}"; + return; + } + _imagePath = imgResult.Value; + } + + // Tri-state cover for UpdateAsync: a freshly uploaded path sets it; an explicit clear + // sends ""; otherwise null leaves the existing cover untouched. + string? imagePathForUpdate = + _imagePath is { } newPath ? newPath + : _clearExistingImage ? "" + : null; + + int succeeded = 0, failed = 0; + for (int i = 0; i < _tracks.Count; i++) + { + var row = _tracks[i]; + + if (row.Status == BatchRowStatus.Done) + { + _processedCount++; + continue; + } + + var trackNumber = i + 1; // 1-based ordinal from list position + + row.Status = BatchRowStatus.Uploading; + StateHasChanged(); + + try + { + if (row.Id.HasValue) + { + // Existing track — metadata-only update; audio stays in the vault. + var updateResult = await CmsTrackService.UpdateAsync( + row.Id.Value, + row.TrackName, + _artist, + album, + genre, + releaseDate, + imagePathForUpdate, + _releaseType, + trackNumber); + + if (!updateResult.Success) + { + var error = updateResult.Messages.FirstOrDefault()?.Message ?? "Unknown error"; + row.Status = BatchRowStatus.Failed; + row.ErrorMessage = error; + failed++; + Logger.LogWarning("Batch edit: update for '{TrackName}' (id={Id}) failed: {Error}", + row.TrackName, row.Id.Value, error); + } + else + { + row.Status = BatchRowStatus.Done; + succeeded++; + } + } + else + { + // New track — upload, then link cover art with a follow-up update (same + // two-step pattern as BatchUpload; the upload endpoint takes no imagePath). + await using var wavStream = row.WavFile!.OpenReadStream(MaxUploadBytes); + var uploadResult = await CmsTrackService.UploadTrackAsync( + wavStream, + row.WavFile.Name, + row.WavFile.ContentType, + row.TrackName, + _artist, + album, + genre, + string.IsNullOrWhiteSpace(_releaseDate) ? null : _releaseDate, + row.WavFile.Name, + createdByUserId, + _releaseType, + trackNumber); + + if (!uploadResult.Success || uploadResult.Value is null) + { + var error = uploadResult.Messages.FirstOrDefault()?.Message ?? "Unknown error"; + row.Status = BatchRowStatus.Failed; + row.ErrorMessage = error; + failed++; + Logger.LogWarning("Batch edit: upload for new track '{TrackName}' failed: {Error}", + row.TrackName, error); + } + else + { + // Link a cover only when one is actively set ("" clear doesn't apply to + // a brand-new track that has no cover yet). + if (imagePathForUpdate is { Length: > 0 } linkPath) + { + var linkResult = await CmsTrackService.UpdateAsync( + uploadResult.Value.Id, + row.TrackName, + _artist, + album, + genre, + releaseDate, + linkPath, + _releaseType, + trackNumber); + + if (!linkResult.Success) + { + // Non-blocking: track persisted; cover can be linked via TrackEdit. + Logger.LogWarning("Batch edit: cover link failed for new track '{TrackName}' (id={Id})", + row.TrackName, uploadResult.Value.Id); + } + } + + row.Status = BatchRowStatus.Done; + succeeded++; + } + } + } + catch (Exception ex) + { + Logger.LogError(ex, "Batch edit: exception processing '{TrackName}'", row.TrackName); + row.Status = BatchRowStatus.Failed; + row.ErrorMessage = "Save failed — please try again."; + failed++; + } + + _processedCount++; + StateHasChanged(); + } + + if (failed == 0) + { + Snackbar.Add($"Saved {succeeded} track(s).", Severity.Success); + Navigation.NavigateTo("/tracks/albums"); + } + else + { + Snackbar.Add($"Saved {succeeded} track(s); {failed} failed — review errors below.", Severity.Warning); + // Stay on page so the admin can see the failed rows. + } + } + finally + { + _saving = false; + StateHasChanged(); + } + } +} diff --git a/DeepDrftManager/Components/Pages/Tracks/BatchRowModel.cs b/DeepDrftManager/Components/Pages/Tracks/BatchRowModel.cs new file mode 100644 index 0000000..7f057ee --- /dev/null +++ b/DeepDrftManager/Components/Pages/Tracks/BatchRowModel.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNetCore.Components.Forms; + +namespace DeepDrftManager.Components.Pages.Tracks; + +/// +/// A single track row shared by BatchUpload (all rows are new uploads) and +/// BatchEdit (existing rows carry ; admins may also add new upload rows). +/// +public class BatchRowModel +{ + /// SQL id of an existing track. null means a new row to upload. + public long? Id { get; set; } + + /// Vault entry key — existing rows only. + public string? EntryKey { get; set; } + + /// Original upload filename — existing rows only, read-only display. + public string? OriginalFileName { get; set; } + + /// Selected WAV — new rows only. + public IBrowserFile? WavFile { get; set; } + + public string TrackName { get; set; } = string.Empty; + + public int TrackNumber { get; set; } + + public BatchRowStatus Status { get; set; } = BatchRowStatus.Queued; + + public string? ErrorMessage { get; set; } +} + +public enum BatchRowStatus { Queued, Uploading, Done, Failed } diff --git a/DeepDrftManager/Components/Pages/Tracks/BatchTrackDetail.razor b/DeepDrftManager/Components/Pages/Tracks/BatchTrackDetail.razor new file mode 100644 index 0000000..da6c77f --- /dev/null +++ b/DeepDrftManager/Components/Pages/Tracks/BatchTrackDetail.razor @@ -0,0 +1,60 @@ +@if (SelectedTrack is null) +{ + Select a track from the list to edit its details. +} +else +{ + + + + @if (SelectedTrack.Id.HasValue) + { + + @(string.IsNullOrEmpty(SelectedTrack.OriginalFileName) ? "—" : SelectedTrack.OriginalFileName) + Existing track — audio is not editable. + + } + else + { + + @if (SelectedTrack.WavFile is { } wav) + { + @wav.Name (@FormatBytes(wav.Size)) + } + else + { + No WAV file selected. + } + + } + + @if (SelectedTrack.Status == BatchRowStatus.Failed) + { + @SelectedTrack.ErrorMessage + } + +} + +@code { + [Parameter] public BatchRowModel? SelectedTrack { get; set; } + [Parameter] public bool Disabled { get; set; } + [Parameter] public EventCallback TrackNameChanged { get; set; } + + private static string FormatBytes(long bytes) + { + const long KB = 1024; + const long MB = KB * 1024; + const long GB = MB * 1024; + if (bytes >= GB) return $"{bytes / (double)GB:F2} GB"; + if (bytes >= MB) return $"{bytes / (double)MB:F2} MB"; + if (bytes >= KB) return $"{bytes / (double)KB:F2} KB"; + return $"{bytes} bytes"; + } +} diff --git a/DeepDrftManager/Components/Pages/Tracks/BatchTrackList.razor b/DeepDrftManager/Components/Pages/Tracks/BatchTrackList.razor new file mode 100644 index 0000000..e05fd12 --- /dev/null +++ b/DeepDrftManager/Components/Pages/Tracks/BatchTrackList.razor @@ -0,0 +1,84 @@ +@using Microsoft.AspNetCore.Components.Forms + + + Tracks + + @if (AllowNewTracks) + { + + } + + @if (Tracks.Count == 0) + { + No tracks added yet. + } + else + { + + @for (var i = 0; i < Tracks.Count; i++) + { + var index = i; + var row = Tracks[index]; +
+ + @(index + 1). + @row.TrackName + @StatusChip(row) + + + + +
+ } +
+ } +
+ +@code { + [Parameter] public List Tracks { get; set; } = new(); + [Parameter] public int SelectedIndex { get; set; } + [Parameter] public EventCallback SelectedIndexChanged { get; set; } + [Parameter] public bool Disabled { get; set; } + [Parameter] public bool AllowNewTracks { get; set; } = true; + [Parameter] public EventCallback> OnWavFilesSelected { get; set; } + [Parameter] public EventCallback OnMoveUp { get; set; } + [Parameter] public EventCallback OnMoveDown { get; set; } + [Parameter] public EventCallback OnRemove { get; set; } + + private const int MaxFilesPerPick = 50; + + private Task SelectRow(int index) => SelectedIndexChanged.InvokeAsync(index); + + private Task HandleWavFilesSelected(InputFileChangeEventArgs e) => + OnWavFilesSelected.InvokeAsync(e.GetMultipleFiles(MaxFilesPerPick)); + + private string RowStyle(int index) + { + const string baseStyle = "cursor: pointer; border-radius: 4px;"; + return index == SelectedIndex + ? $"{baseStyle} background-color: var(--mud-palette-action-default-hover);" + : baseStyle; + } + + private RenderFragment StatusChip(BatchRowModel row) => row.Status switch + { + BatchRowStatus.Uploading => @ + Uploading, + BatchRowStatus.Done => @Done, + BatchRowStatus.Failed => @Failed, + _ => @Queued + }; +} diff --git a/DeepDrftManager/Components/Pages/Tracks/BatchUpload.razor b/DeepDrftManager/Components/Pages/Tracks/BatchUpload.razor index a82370f..1ee6e7c 100644 --- a/DeepDrftManager/Components/Pages/Tracks/BatchUpload.razor +++ b/DeepDrftManager/Components/Pages/Tracks/BatchUpload.razor @@ -2,6 +2,7 @@ @using System.Security.Claims @using DeepDrftManager.Services @using DeepDrftModels.Enums +@using Microsoft.AspNetCore.Components.Forms @attribute [Authorize] @inject ICmsTrackService CmsTrackService @@ -15,140 +16,30 @@ Upload Release - - - - - - - - - - - - - - - - - @foreach (var rt in Enum.GetValues()) - { - @rt - } - - - - - - @if (_selectedImageFile is { } selectedImage) - { - - Selected: @selectedImage.Name - - - } - else - { - No cover art — optional. - } - - - @if (_selectedImageFile is not null) - { - Will upload on submit. - } - - - - - + - - Tracks - - - - @if (_tracks.Count == 0) - { - No tracks added yet. - } - else - { - - @for (var i = 0; i < _tracks.Count; i++) - { - var index = i; - var row = _tracks[index]; -
- - @(index + 1). - @row.TrackName - @StatusChip(row) - - - - -
- } -
- } -
+
- @if (_selectedIndex < 0 || _tracks.Count == 0) - { - Select a track from the list to edit its details. - } - else - { - var selected = _tracks[_selectedIndex]; - - - - - @if (selected.WavFile is { } wav) - { - @wav.Name (@FormatBytes(wav.Size)) - } - else - { - No WAV file selected. - } - - - @if (selected.Status == TrackUploadStatus.Failed) - { - @selected.ErrorMessage - } - - } +
@@ -186,9 +77,7 @@ // streaming path means the limit caps the request, not in-memory buffering. private const long MaxUploadBytes = 1_073_741_824L; - private const int MaxFilesPerPick = 50; - - private List _tracks = new(); + private List _tracks = new(); private int _selectedIndex = -1; private bool _uploading; private int _uploadedCount; @@ -203,20 +92,10 @@ private string _releaseDate = string.Empty; private ReleaseType _releaseType = ReleaseType.Single; - private class BatchTrackRow - { - public IBrowserFile? WavFile { get; set; } - public string TrackName { get; set; } = string.Empty; - public TrackUploadStatus Status { get; set; } = TrackUploadStatus.Queued; - public string? ErrorMessage { get; set; } - } - - private enum TrackUploadStatus { Queued, Uploading, Done, Failed } - - private void HandleWavFilesSelected(InputFileChangeEventArgs e) + private void HandleWavFilesSelected(IReadOnlyList files) { _errorMessage = null; - foreach (var file in e.GetMultipleFiles(MaxFilesPerPick)) + foreach (var file in files) { if (!file.Name.EndsWith(".wav", StringComparison.OrdinalIgnoreCase)) { @@ -224,7 +103,7 @@ continue; } - _tracks.Add(new BatchTrackRow + _tracks.Add(new BatchRowModel { WavFile = file, TrackName = Path.GetFileNameWithoutExtension(file.Name) @@ -237,8 +116,6 @@ } } - private void SelectRow(int index) => _selectedIndex = index; - private void MoveUp(int i) { if (i == 0) return; @@ -262,35 +139,6 @@ if (_selectedIndex >= _tracks.Count) _selectedIndex = _tracks.Count - 1; } - private string RowStyle(int index) - { - var baseStyle = "cursor: pointer; border-radius: 4px;"; - return index == _selectedIndex - ? $"{baseStyle} background-color: var(--mud-palette-action-default-hover);" - : baseStyle; - } - - private RenderFragment StatusChip(BatchTrackRow row) => row.Status switch - { - TrackUploadStatus.Uploading => @ - Uploading, - TrackUploadStatus.Done => @Done, - TrackUploadStatus.Failed => @Failed, - _ => @Queued - }; - - private void HandleImageFileSelected(InputFileChangeEventArgs e) - { - _selectedImageFile = e.File; - _imagePath = null; - } - - private void ClearImage() - { - _selectedImageFile = null; - _imagePath = null; - } - private async Task SubmitAsync() { _errorMessage = null; @@ -340,6 +188,7 @@ return; } + _imagePath = null; // Clear any stale uploaded path from a prior partial attempt. _uploading = true; _uploadedCount = 0; @@ -366,7 +215,7 @@ var row = _tracks[i]; var trackNumber = i + 1; // 1-based ordinal from list position - row.Status = TrackUploadStatus.Uploading; + row.Status = BatchRowStatus.Uploading; StateHasChanged(); try @@ -392,7 +241,7 @@ if (!result.Success || result.Value is null) { var error = result.Messages.FirstOrDefault()?.Message ?? "Unknown error"; - row.Status = TrackUploadStatus.Failed; + row.Status = BatchRowStatus.Failed; row.ErrorMessage = error; failed++; Logger.LogWarning("Batch upload: track '{TrackName}' failed: {Error}", row.TrackName, error); @@ -422,14 +271,14 @@ } } - row.Status = TrackUploadStatus.Done; + row.Status = BatchRowStatus.Done; succeeded++; } } catch (Exception ex) { Logger.LogError(ex, "Batch upload: exception uploading '{TrackName}'", row.TrackName); - row.Status = TrackUploadStatus.Failed; + row.Status = BatchRowStatus.Failed; row.ErrorMessage = "Upload failed — please try again."; failed++; } @@ -455,15 +304,4 @@ StateHasChanged(); } } - - private static string FormatBytes(long bytes) - { - const long KB = 1024; - const long MB = KB * 1024; - const long GB = MB * 1024; - if (bytes >= GB) return $"{bytes / (double)GB:F2} GB"; - if (bytes >= MB) return $"{bytes / (double)MB:F2} MB"; - if (bytes >= KB) return $"{bytes / (double)KB:F2} KB"; - return $"{bytes} bytes"; - } }