@using Generic.Media.Components @using Microsoft.AspNetCore.Components.Forms @using System.Collections.Generic @using System.Linq @using System.Net.Http.Json @using System.Net.Http.Headers @using Blazored.LocalStorage @inject IHttpClientFactory HttpClientFactory @inject ILocalStorageService LocalStorage @if (SingleUploadMode && _items.Count == 1) { // Auto-select first item in single mode if not selected // Done in OnInitialized or after upload }
@if (_items.Any()) { } @if (!SingleUploadMode && _items.Count < MaxItems) {
}
@code { [Parameter] public List InitialUrls { get; set; } = new(); [Parameter] public EventCallback> OnChange { get; set; } [Parameter] public int MaxItems { get; set; } = 10; // Config Parameters [Parameter] public double? AspectRatio { get; set; } [Parameter] public int? TargetWidth { get; set; } [Parameter] public int? TargetHeight { get; set; } [Parameter] public bool SingleUploadMode { get; set; } [Parameter] public string? AiModelApiKey { get; set; } [Parameter] public string CdnBaseUrl { get; set; } = "http://localhost:2003/"; private List _items = new(); private List _deletedUrls = new(); private ImageCropper? _cropper; private MediaItemModel? _selectedItem; protected override void OnInitialized() { if (SingleUploadMode) MaxItems = 1; if (InitialUrls != null) { foreach (var url in InitialUrls) { _items.Add(new MediaItemModel { Url = url, Type = DetectMediaType(url), Order = _items.Count }); } // Auto Select first item if (_items.Any()) _selectedItem = _items.First(); } } // Called after render to ensure cropper updates with initially selected item if needed protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender && _selectedItem != null && _cropper != null) { await _cropper.OpenAsync(_selectedItem); } } private async Task HandleFileDrop(InputFileChangeEventArgs e) { var files = e.GetMultipleFiles(SingleUploadMode ? 1 : 20); // 20 as arbitrary max for batch if (SingleUploadMode && files.Count > 0) { // Clear existing for single mode var allItems = _items.ToList(); foreach (var item in allItems) RemoveItem(item); } MediaItemModel? lastAdded = null; foreach (var file in files) { if (_items.Count >= MaxItems) break; var newItem = new MediaItemModel { File = file, Type = file.ContentType.StartsWith("video") ? MediaType.Video : MediaType.Image, IsLocal = true, Order = _items.Count }; _items.Add(newItem); lastAdded = newItem; // Generate Preview for Images if (newItem.Type == MediaType.Image) { // Read async to avoid blocking UI? // Fire and forget task to load preview _ = LoadPreviewAsync(newItem); } } // Auto select the last added item to show in cropper if (lastAdded != null && lastAdded.Type == MediaType.Image) { await SelectItem(lastAdded); } } private async Task LoadPreviewAsync(MediaItemModel item) { try { if (item.File == null) return; var maxFileSize = 10L * 1024 * 1024; // 10MB var buffer = new byte[item.File.Size]; await item.File.OpenReadStream(maxFileSize).ReadAsync(buffer); var base64 = Convert.ToBase64String(buffer); item.PreviewUrl = $"data:{item.File.ContentType};base64,{base64}"; // If this item is currently selected, trigger update if (_selectedItem == item) { await SelectItem(item); } StateHasChanged(); } catch (Exception ex) { Console.WriteLine($"Failed to load preview: {ex.Message}"); } } private void HandleLinkAdded(string url) { if (_items.Count >= MaxItems) return; if (SingleUploadMode && _items.Count > 0) { var allItems = _items.ToList(); foreach (var item in allItems) RemoveItem(item); } var newItem = new MediaItemModel { Url = url, Type = DetectMediaType(url), Order = _items.Count }; _items.Add(newItem); if (newItem.Type == MediaType.Image) _ = SelectItem(newItem); } private void RemoveItem(MediaItemModel item) { _items.Remove(item); if (_selectedItem == item) { _selectedItem = null; // Clear cropper? if (_cropper != null) { // We can't really "clear" it easily without a Clear method, // but passing null to something that expects it might work. // For now, re-render will pass Item=null to ImageCropper } } // Track deletion if it was a remote URL if (!string.IsNullOrEmpty(item.Url) && !item.IsLocal) { _deletedUrls.Add(item.Url); } Reindex(); } private async Task SelectItem(MediaItemModel item) { _selectedItem = item; if (item.Type == MediaType.Image && _cropper != null) { await _cropper.OpenAsync(item); } StateHasChanged(); } private void OnCropSaved(MediaItemModel item) { // Item is modified by reference // Force refresh list to show new thumb StateHasChanged(); } private void OnCropCancel() { // Do nothing } private void UpdateOrder(List sortedItems) { _items = sortedItems; Reindex(); } private void Reindex() { for (int i = 0; i < _items.Count; i++) { _items[i].Order = i; } } private MediaType DetectMediaType(string url) { if (string.IsNullOrWhiteSpace(url)) return MediaType.Image; if (url.Contains("youtube.com") || url.Contains("youtu.be")) return MediaType.YouTube; if (url.Contains("vimeo.com")) return MediaType.Vimeo; if (url.EndsWith(".mp4") || url.EndsWith(".webm")) return MediaType.Video; return MediaType.Image; } public class UploadResult { public List FinalUrls { get; set; } = new(); public List DeletedUrls { get; set; } = new(); } public async Task ProcessUploadsAsync() { var result = new UploadResult(); result.DeletedUrls = new List(_deletedUrls); var client = HttpClientFactory.CreateClient("CDN"); var token = await LocalStorage.GetItemAsync("authToken"); if (!string.IsNullOrEmpty(token)) { client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); } foreach (var item in _items) { if (item.IsLocal) { // Handle Local File Upload try { using var content = new MultipartFormDataContent(); if (!string.IsNullOrEmpty(item.PreviewUrl) && item.PreviewUrl.StartsWith("data:")) { // Cropped or preview base64 var base64Data = item.PreviewUrl.Split(',')[1]; var bytes = Convert.FromBase64String(base64Data); var byteContent = new ByteArrayContent(bytes); byteContent.Headers.ContentType = new MediaTypeHeaderValue("image/webp"); content.Add(byteContent, "file", "image.webp"); } else if (item.File != null) { // Original file var fileContent = new StreamContent(item.File.OpenReadStream(10 * 1024 * 1024)); fileContent.Headers.ContentType = new MediaTypeHeaderValue(item.File.ContentType); content.Add(fileContent, "file", item.File.Name); } else { continue; } var response = await client.PostAsync("files/upload", content); if (response.IsSuccessStatusCode) { var uploadRes = await response.Content.ReadFromJsonAsync(); if (uploadRes != null && (!string.IsNullOrEmpty(uploadRes.Path) || !string.IsNullOrEmpty(uploadRes.Url))) { var fullUrl = !string.IsNullOrEmpty(uploadRes.Url) ? uploadRes.Url : $"{CdnBaseUrl.TrimEnd('/')}/{uploadRes.Path?.TrimStart('/')}"; result.FinalUrls.Add(fullUrl); item.Url = fullUrl; item.IsLocal = false; } } else { var errorMsg = await response.Content.ReadAsStringAsync(); throw new Exception($"Upload failed with status {response.StatusCode}: {errorMsg}"); } } catch (Exception ex) { Console.WriteLine($"Upload Error: {ex.Message}"); throw; // Re-throw to block save in parent } } else if (!string.IsNullOrEmpty(item.Url)) { result.FinalUrls.Add(item.Url); } } return result; } public void Clear() { _items.Clear(); _deletedUrls.Clear(); _selectedItem = null; StateHasChanged(); } private class UploadResponse { public string? Path { get; set; } public string? Url { get; set; } } }