@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; }
}
}