347 lines
11 KiB
Plaintext
347 lines
11 KiB
Plaintext
@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
|
|
}
|
|
|
|
<div class="flex flex-col gap-6 w-full">
|
|
|
|
<!-- Section 1: Cropper / Viewer (Always Available) -->
|
|
<div class="w-full">
|
|
<ImageCropper @ref="_cropper" Item="@_selectedItem" OnCropSaved="@OnCropSaved" OnCancel="@OnCropCancel"
|
|
AspectRatio="@AspectRatio" TargetWidth="@TargetWidth" TargetHeight="@TargetHeight"
|
|
AiApiKey="@AiModelApiKey" />
|
|
</div>
|
|
|
|
<!-- Section 2: Dropzone & List -->
|
|
<div class="w-full">
|
|
<div class="mb-4">
|
|
<MediaDropZone OnFileDropped="@HandleFileDrop" CompactMode="@(_items.Any())" />
|
|
</div>
|
|
|
|
@if (_items.Any())
|
|
{
|
|
<SortableMediaList Items="@_items" OnRemove="@RemoveItem" OnSort="@UpdateOrder"
|
|
OnEdit="@((item) => SelectItem(item))" />
|
|
}
|
|
|
|
@if (!SingleUploadMode && _items.Count < MaxItems)
|
|
{
|
|
<div class="mt-4">
|
|
<ExternalLinkInput OnLinkAdded="@HandleLinkAdded" />
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
@code {
|
|
[Parameter] public List<string> InitialUrls { get; set; } = new();
|
|
[Parameter] public EventCallback<List<string>> 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<MediaItemModel> _items = new();
|
|
private List<string> _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<MediaItemModel> 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<string> FinalUrls { get; set; } = new();
|
|
public List<string> DeletedUrls { get; set; } = new();
|
|
}
|
|
|
|
public async Task<UploadResult> ProcessUploadsAsync()
|
|
{
|
|
var result = new UploadResult();
|
|
result.DeletedUrls = new List<string>(_deletedUrls);
|
|
|
|
var client = HttpClientFactory.CreateClient("CDN");
|
|
var token = await LocalStorage.GetItemAsync<string>("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<UploadResponse>();
|
|
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; }
|
|
}
|
|
} |