@page "/tracks/{Id:long}" @using DeepDrftManager.Services @using Microsoft.AspNetCore.Components.Forms @attribute [Authorize] @inject ICmsTrackService CmsTrackService @inject IHttpClientFactory HttpClientFactory @inject ISnackbar Snackbar @inject IDialogService DialogService @inject NavigationManager Nav @inject ILogger Logger Edit Track — DeepDrft CMS Back to tracks @if (_loading) { } else if (_track is null) { Track not found. } else { Edit Track @_track.EntryKey Vault reference — not editable. @if (ImagePreviewUrl is { } previewUrl) { } else if (_selectedImageFile is not null) { New image selected (not yet saved). } else { No cover art set. } @if (_selectedImageFile is { } selected) { Selected: @selected.Name (will upload on save) } Delete Save Changes } @code { [Parameter] public long Id { get; set; } private TrackDto? _track; private TrackEditForm _form = new(); private bool _loading = true; private bool _busy; private IBrowserFile? _selectedImageFile; private bool CanSave => !string.IsNullOrWhiteSpace(_form.TrackName) && !string.IsNullOrWhiteSpace(_form.Artist); // The image endpoint (GET api/image/{entryKey}) is unauthenticated, so the browser can hit // DeepDrftAPI directly. Base address comes from the same named client the CMS uses for writes. private string? ImagePreviewUrl { get { if (string.IsNullOrEmpty(_form.ImagePath)) return null; var baseAddress = HttpClientFactory.CreateClient("DeepDrft.Content.Cms").BaseAddress; if (baseAddress is null) return null; return new Uri(baseAddress, $"api/image/{Uri.EscapeDataString(_form.ImagePath)}").ToString(); } } protected override async Task OnInitializedAsync() { await LoadAsync(); } private async Task LoadAsync() { _loading = true; var result = await CmsTrackService.GetByIdAsync(Id); _track = result.Success ? result.Value : null; if (_track is not null) { _form = TrackEditForm.From(_track); } _loading = false; } private async Task SaveAsync() { if (_track is null || !CanSave) return; _busy = true; try { // Upload any newly picked cover art first; abort the save if it fails so we never // persist metadata pointing at an image that was never stored. if (_selectedImageFile is { } file) { await using var imageStream = file.OpenReadStream(maxAllowedSize: 50_000_000); var uploadResult = await CmsTrackService.UploadImageAsync( imageStream, file.Name, file.ContentType); if (!uploadResult.Success) { var uploadError = uploadResult.Messages.FirstOrDefault()?.Message ?? "Unknown error"; Snackbar.Add($"Image upload failed: {uploadError}", Severity.Error); return; } _form.ImagePath = uploadResult.Value; _selectedImageFile = null; } // Metadata update over HTTP — EntryKey is immutable and not sent. The Content API // loads the authoritative row and applies these fields. imagePath is tri-state: an // explicit empty string clears the link, a value sets it. var releaseDate = _form.ReleaseDate is { } d ? DateOnly.FromDateTime(d) : (DateOnly?)null; var updated = await CmsTrackService.UpdateAsync( Id, _form.TrackName, _form.Artist, string.IsNullOrWhiteSpace(_form.Album) ? null : _form.Album, string.IsNullOrWhiteSpace(_form.Genre) ? null : _form.Genre, releaseDate, string.IsNullOrEmpty(_form.ImagePath) ? "" : _form.ImagePath); if (updated.Success) { Snackbar.Add("Track updated.", Severity.Success); await LoadAsync(); } else { var error = updated.Messages.FirstOrDefault()?.Message ?? "Unknown error"; Snackbar.Add($"Save failed: {error}", Severity.Error); } } catch (Exception ex) { Logger.LogError(ex, "Save failed for track {TrackId}", Id); Snackbar.Add("Save failed — please try again.", Severity.Error); } finally { _busy = false; } } private void HandleImageFileSelected(InputFileChangeEventArgs e) { _selectedImageFile = e.File; } private void ClearImage() { _form.ImagePath = null; _selectedImageFile = null; } private async Task ConfirmDelete() { if (_track is null) return; var confirmed = await DialogService.ShowMessageBox( "Delete track", $"Permanently delete \"{_track.TrackName}\" by {_track.Artist}? This cannot be undone.", yesText: "Delete", cancelText: "Cancel"); if (confirmed != true) return; _busy = true; try { var result = await CmsTrackService.DeleteTrackAsync(Id); if (result.Success) { Snackbar.Add("Track deleted.", Severity.Success); Nav.NavigateTo("/tracks"); } else { var error = result.Messages.FirstOrDefault()?.Message ?? "Unknown error"; Snackbar.Add($"Delete failed: {error}", Severity.Error); } } catch (Exception ex) { Logger.LogError(ex, "Delete failed for track {TrackId}", Id); Snackbar.Add("Delete failed — please try again.", Severity.Error); } finally { _busy = false; } } private sealed class TrackEditForm { public string TrackName { get; set; } = string.Empty; public string Artist { get; set; } = string.Empty; public string? Album { get; set; } public string? Genre { get; set; } public string? ImagePath { get; set; } public DateTime? ReleaseDate { get; set; } public static TrackEditForm From(TrackDto track) => new() { TrackName = track.TrackName, Artist = track.Artist, Album = track.Album, Genre = track.Genre, ImagePath = track.ImagePath, ReleaseDate = track.ReleaseDate is { } d ? d.ToDateTime(TimeOnly.MinValue) : null }; } }