feat: add BatchEdit page and extract reusable batch sub-components from BatchUpload
fix: TrackNumber sort case, stale _imagePath reset, skip Done rows on retry in BatchEdit
This commit is contained in:
@@ -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 @@
|
||||
<MudContainer MaxWidth="MaxWidth.Large" Class="mt-8">
|
||||
<MudText Typo="Typo.h4" GutterBottom="true">Upload Release</MudText>
|
||||
|
||||
<MudPaper Class="pa-6 mb-4" Elevation="2">
|
||||
<MudGrid>
|
||||
<MudItem xs="12" sm="6">
|
||||
<MudTextField @bind-Value="_albumName" Label="Album Name" Required="true" RequiredError="Album Name is required" Variant="Variant.Outlined" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6">
|
||||
<MudTextField @bind-Value="_artist" Label="Artist" Required="true" RequiredError="Artist is required" Variant="Variant.Outlined" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6">
|
||||
<MudTextField @bind-Value="_genre" Label="Genre" Variant="Variant.Outlined" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6">
|
||||
<MudTextField @bind-Value="_releaseDate" Label="Release Date (YYYY-MM-DD)" Placeholder="2024-01-15" Variant="Variant.Outlined" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6">
|
||||
<MudSelect T="ReleaseType" @bind-Value="_releaseType" Label="Release Type" Variant="Variant.Outlined">
|
||||
@foreach (var rt in Enum.GetValues<ReleaseType>())
|
||||
{
|
||||
<MudSelectItem T="ReleaseType" Value="rt">@rt</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6">
|
||||
<MudField Label="Cover Art" Variant="Variant.Outlined" InnerPadding="false">
|
||||
<MudStack Spacing="3">
|
||||
@if (_selectedImageFile is { } selectedImage)
|
||||
{
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||
<MudText Typo="Typo.body2" Color="Color.Default">Selected: @selectedImage.Name</MudText>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Clear"
|
||||
Color="Color.Error"
|
||||
Size="Size.Small"
|
||||
Disabled="_uploading"
|
||||
OnClick="ClearImage"
|
||||
aria-label="Cancel image selection" />
|
||||
</MudStack>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudText Typo="Typo.body2" Color="Color.Default">No cover art — optional.</MudText>
|
||||
}
|
||||
|
||||
<InputFile OnChange="HandleImageFileSelected" accept="image/*" disabled="@_uploading" />
|
||||
@if (_selectedImageFile is not null)
|
||||
{
|
||||
<MudText Typo="Typo.caption">Will upload on submit.</MudText>
|
||||
}
|
||||
</MudStack>
|
||||
</MudField>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
</MudPaper>
|
||||
<AlbumHeaderFields @bind-AlbumName="_albumName"
|
||||
@bind-Artist="_artist"
|
||||
@bind-Genre="_genre"
|
||||
@bind-ReleaseDate="_releaseDate"
|
||||
@bind-ReleaseType="_releaseType"
|
||||
@bind-SelectedImageFile="_selectedImageFile"
|
||||
Disabled="_uploading" />
|
||||
|
||||
<MudGrid>
|
||||
<MudItem xs="12" md="5">
|
||||
<MudPaper Class="pa-4" Elevation="2">
|
||||
<MudText Typo="Typo.h6" GutterBottom="true">Tracks</MudText>
|
||||
|
||||
<InputFile OnChange="HandleWavFilesSelected" accept=".wav,audio/wav,audio/x-wav" multiple disabled="@_uploading" />
|
||||
|
||||
@if (_tracks.Count == 0)
|
||||
{
|
||||
<MudText Typo="Typo.body2" Color="Color.Default" Class="mt-3">No tracks added yet.</MudText>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudList T="BatchTrackRow" Class="mt-3">
|
||||
@for (var i = 0; i < _tracks.Count; i++)
|
||||
{
|
||||
var index = i;
|
||||
var row = _tracks[index];
|
||||
<div style="@RowStyle(index)" @onclick="() => SelectRow(index)">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1" Class="pa-2">
|
||||
<MudText Typo="Typo.body2" Style="min-width: 1.5rem;">@(index + 1).</MudText>
|
||||
<MudText Typo="Typo.body2" Style="flex: 1 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">@row.TrackName</MudText>
|
||||
@StatusChip(row)
|
||||
<MudIconButton Icon="@Icons.Material.Filled.ArrowUpward"
|
||||
Size="Size.Small"
|
||||
Disabled="@(index == 0 || _uploading)"
|
||||
OnClick="@(() => MoveUp(index))"
|
||||
aria-label="Move track up" />
|
||||
<MudIconButton Icon="@Icons.Material.Filled.ArrowDownward"
|
||||
Size="Size.Small"
|
||||
Disabled="@(index == _tracks.Count - 1 || _uploading)"
|
||||
OnClick="@(() => MoveDown(index))"
|
||||
aria-label="Move track down" />
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete"
|
||||
Color="Color.Error"
|
||||
Size="Size.Small"
|
||||
Disabled="@_uploading"
|
||||
OnClick="@(() => RemoveRow(index))"
|
||||
aria-label="Remove track" />
|
||||
</MudStack>
|
||||
</div>
|
||||
}
|
||||
</MudList>
|
||||
}
|
||||
</MudPaper>
|
||||
<BatchTrackList Tracks="_tracks"
|
||||
@bind-SelectedIndex="_selectedIndex"
|
||||
Disabled="_uploading"
|
||||
OnWavFilesSelected="HandleWavFilesSelected"
|
||||
OnMoveUp="MoveUp"
|
||||
OnMoveDown="MoveDown"
|
||||
OnRemove="RemoveRow" />
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12" md="7">
|
||||
<MudPaper Class="pa-4" Elevation="2">
|
||||
@if (_selectedIndex < 0 || _tracks.Count == 0)
|
||||
{
|
||||
<MudText Typo="Typo.body1" Color="Color.Default">Select a track from the list to edit its details.</MudText>
|
||||
}
|
||||
else
|
||||
{
|
||||
var selected = _tracks[_selectedIndex];
|
||||
<MudStack Spacing="4">
|
||||
<MudTextField @bind-Value="selected.TrackName"
|
||||
Label="Track Name"
|
||||
Required="true"
|
||||
RequiredError="Track Name is required"
|
||||
Variant="Variant.Outlined"
|
||||
Disabled="_uploading" />
|
||||
|
||||
<MudField Label="WAV File" Variant="Variant.Outlined" InnerPadding="false">
|
||||
@if (selected.WavFile is { } wav)
|
||||
{
|
||||
<MudText Typo="Typo.body2">@wav.Name (@FormatBytes(wav.Size))</MudText>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudText Typo="Typo.body2" Color="Color.Error">No WAV file selected.</MudText>
|
||||
}
|
||||
</MudField>
|
||||
|
||||
@if (selected.Status == TrackUploadStatus.Failed)
|
||||
{
|
||||
<MudAlert Severity="Severity.Error">@selected.ErrorMessage</MudAlert>
|
||||
}
|
||||
</MudStack>
|
||||
}
|
||||
<BatchTrackDetail SelectedTrack="@(_selectedIndex >= 0 && _tracks.Count > 0 ? _tracks[_selectedIndex] : null)"
|
||||
Disabled="_uploading"
|
||||
TrackNameChanged="@(name => { if (_selectedIndex >= 0) { _tracks[_selectedIndex].TrackName = name; } })" />
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
@@ -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<BatchTrackRow> _tracks = new();
|
||||
private List<BatchRowModel> _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<IBrowserFile> 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 => @<MudChip T="string" Size="Size.Small" Color="Color.Info" Variant="Variant.Text">
|
||||
<MudProgressCircular Indeterminate="true" Size="Size.Small" Class="mr-1" />Uploading</MudChip>,
|
||||
TrackUploadStatus.Done => @<MudChip T="string" Size="Size.Small" Color="Color.Success" Variant="Variant.Text" Icon="@Icons.Material.Filled.CheckCircle">Done</MudChip>,
|
||||
TrackUploadStatus.Failed => @<MudChip T="string" Size="Size.Small" Color="Color.Error" Variant="Variant.Text" Icon="@Icons.Material.Filled.Error">Failed</MudChip>,
|
||||
_ => @<MudChip T="string" Size="Size.Small" Color="Color.Default" Variant="Variant.Text">Queued</MudChip>
|
||||
};
|
||||
|
||||
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";
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user