Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
313 changes: 197 additions & 116 deletions Valour/Client/Components/Menus/Modals/Upload/FileUploadComponent.razor
Original file line number Diff line number Diff line change
@@ -1,116 +1,197 @@
@inherits Modal<FileUploadComponent.ModalParams>
@inject IJSRuntime JsRuntime
@implements IDisposable

@if (Data.Attachment is null)
{
<h5>Loading content...</h5>
return;
}

<BasicModalLayout Title="Upload File" Icon="cloud-upload-fill" MaxWidth="600px">
<MainArea>
<div class="attachment-holder">
@if (_loaded)
{
switch (Data.Attachment.Type)
{
case MessageAttachmentType.Image:
{
if (_imageReady)
{
<ImageAttachmentComponent Message="@Data.Message" Attachment="@Data.Attachment"/>
}

break;
}
case MessageAttachmentType.Video:
<VideoAttachmentComponent Message="@Data.Message" Attachment="@Data.Attachment"/>
break;
case MessageAttachmentType.Audio:
<AudioAttachmentComponent Message="@Data.Message" Attachment="@Data.Attachment"/>
break;
default:
<FileAttachmentComponent Message="@Data.Message" Attachment="@Data.Attachment"/>
break;
}
}
</div>
</MainArea>
<ButtonArea>
<div class="basic-modal-buttons">
<button @onclick="@OnClickCancel" class="v-btn">Cancel</button>
<button @onclick="@OnClickConfirm" class="v-btn primary" disabled="@_isUploading">Upload</button>
</div>
</ButtonArea>
</BasicModalLayout>

@code
{
public class ModalParams
{
public byte[] Bytes { get; set; }
public Func<Task> OnConfirm;
public Message Message;
public MessageAttachment Attachment;
}

private DotNetObjectReference<FileUploadComponent> _thisRef;

private bool _imageReady;
private bool _loaded = false;
private bool _isUploading = false;

protected override async Task OnInitializedAsync(){

Console.WriteLine(JsonSerializer.Serialize(Data.Attachment));

_thisRef = DotNetObjectReference.Create(this);

// Load into blob form and get url
var blobUrl = await JsRuntime.InvokeAsync<string>("createBlob", Data.Bytes, Data.Attachment.MimeType);

// Build attachment object
Data.Attachment.Location = blobUrl;

if (Data.Attachment.Type == MessageAttachmentType.Image)
{
_imageReady = false;
await JsRuntime.InvokeVoidAsync("getImageSize", blobUrl, _thisRef);
}

_loaded = true;
}

[JSInvokable]
public void SetImageSize(int width, int height)
{
Data.Attachment.Width = width;
Data.Attachment.Height = height;

Console.WriteLine($"Set image size to {width},{height}");

_imageReady = true;

StateHasChanged();
}

public async Task OnKeyDown(KeyboardEventArgs e){
if (e.Key.ToLower() == "enter")
await OnClickConfirm();
}

public void OnClickCancel() => Close();

public async Task OnClickConfirm(){
if (_isUploading) return;
_isUploading = true;
await Data.OnConfirm.Invoke();
Close();
}

public void Dispose()
{
_thisRef?.Dispose();
}
}
@inherits Modal<FileUploadComponent.ModalParams>
@inject IJSRuntime JsRuntime
@inject UploadService UploadSvc
@implements IDisposable

@if (Data.Attachment is null)
{
<h5>Loading content...</h5>
return;
}

<BasicModalLayout Title="Upload File" Icon="cloud-upload-fill" MaxWidth="600px">
<MainArea>
<div class="attachment-holder">
@if (_loaded)
{
switch (Data.Attachment.Type)
{
case MessageAttachmentType.Image:
{
if (_imageReady)
{
<ImageAttachmentComponent Message="@Data.Message" Attachment="@Data.Attachment"/>
}

break;
}
case MessageAttachmentType.Video:
<VideoAttachmentComponent Message="@Data.Message" Attachment="@Data.Attachment"/>
break;
case MessageAttachmentType.Audio:
<AudioAttachmentComponent Message="@Data.Message" Attachment="@Data.Attachment"/>
break;
default:
<FileAttachmentComponent Message="@Data.Message" Attachment="@Data.Attachment"/>
break;
}
}
</div>
@if (_isUploading)
{
<div class="upload-progress-container">
<div class="upload-progress-bar" style="width: @(_progress?.Percent ?? 0)%"></div>
</div>
<div class="upload-progress-info">
<span class="upload-progress-text">@(_progress?.Percent ?? 0)%</span>
<span class="upload-progress-bytes">@(_progress?.FormattedUploaded ?? "0 B") / @(_progress?.FormattedTotal ?? "0 B")</span>
</div>
}
@if (_uploadError is not null)
{
<div class="upload-error">Upload failed: @_uploadError</div>
}
</MainArea>
<ButtonArea>
<div class="basic-modal-buttons">
@if (_isUploading)
{
<button @onclick="@OnClickCancelUpload" class="v-btn danger">Cancel Upload</button>
}
else
{
<button @onclick="@OnClickCancel" class="v-btn">Cancel</button>
<button @onclick="@OnClickConfirm" class="v-btn primary">Upload</button>
}
</div>
</ButtonArea>
</BasicModalLayout>

@code
{
public class ModalParams
{
public byte[] Bytes { get; set; }
public Func<Task> OnConfirm;
public Message Message;
public MessageAttachment Attachment;
public string UploadUrl { get; set; }
public string AuthToken { get; set; }
public Action<string> OnUploadSuccess { get; set; }
}

private DotNetObjectReference<FileUploadComponent> _thisRef;

private bool _imageReady;
private bool _loaded = false;
private bool _isUploading = false;
private UploadProgressInfo? _progress;
private string _uploadError;

private CancellationTokenSource? _uploadCts;

protected override async Task OnInitializedAsync()
{
Console.WriteLine(JsonSerializer.Serialize(Data.Attachment));

_thisRef = DotNetObjectReference.Create(this);

// Load into blob form and get url
var blobUrl = await JsRuntime.InvokeAsync<string>("createBlob", Data.Bytes, Data.Attachment.MimeType);

// Build attachment object
Data.Attachment.Location = blobUrl;

if (Data.Attachment.Type == MessageAttachmentType.Image)
{
_imageReady = false;
await JsRuntime.InvokeVoidAsync("getImageSize", blobUrl, _thisRef);
}

_loaded = true;
}

[JSInvokable]
public void SetImageSize(int width, int height)
{
Data.Attachment.Width = width;
Data.Attachment.Height = height;

Console.WriteLine($"Set image size to {width},{height}");

_imageReady = true;

StateHasChanged();
}

public async Task OnKeyDown(KeyboardEventArgs e)
{
if (e.Key.ToLower() == "enter" && !_isUploading)
await OnClickConfirm();
}

public void OnClickCancel() => Close();

public void OnClickCancelUpload()
{
_uploadCts?.Cancel();
}

public async Task OnClickConfirm()
{
if (_isUploading) return;

// New path: use UploadService for real progress
if (!string.IsNullOrEmpty(Data.UploadUrl))
{
_isUploading = true;
_progress = null;
_uploadError = null;
_uploadCts = new CancellationTokenSource();
StateHasChanged();

var result = await UploadSvc.UploadAsync(
Data.UploadUrl,
Data.Bytes,
Data.Attachment.MimeType,
Data.Attachment.FileName,
Data.AuthToken,
onProgress: p =>
{
_progress = p;
InvokeAsync(StateHasChanged);
},
cancellationToken: _uploadCts.Token);

_isUploading = false;
_uploadCts.Dispose();
_uploadCts = null;

if (result.Success)
{
Data.OnUploadSuccess?.Invoke(result.Response);
Close();
}
else
{
_uploadError = result.Error ?? "Unknown error";
}

StateHasChanged();
}
else
{
// Fallback: old method without progress
_isUploading = true;
await Data.OnConfirm.Invoke();
Close();
}
}

public void Dispose()
{
_uploadCts?.Cancel();
_uploadCts?.Dispose();
_thisRef?.Dispose();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,55 @@
text-align: center;
margin-top: 10px;
margin-bottom: 10px;
}

.upload-progress-container {
width: 100%;
height: 8px;
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
overflow: hidden;
margin-top: 12px;
}

.upload-progress-bar {
height: 100%;
background: linear-gradient(90deg, #5865F2, #EB459E);
border-radius: 4px;
transition: width 0.2s ease;
}

.upload-progress-info {
display: flex;
justify-content: space-between;
margin-top: 6px;
font-size: 12px;
color: rgba(255, 255, 255, 0.6);
}

.upload-progress-text {
font-weight: 600;
color: rgba(255, 255, 255, 0.9);
}

.upload-progress-bytes {
color: rgba(255, 255, 255, 0.5);
}

.upload-error {
color: #ED4245;
font-size: 13px;
margin-top: 8px;
padding: 8px;
background: rgba(237, 66, 69, 0.1);
border-radius: 6px;
}

::deep .v-btn.danger {
background: #ED4245;
color: white;
}

::deep .v-btn.danger:hover {
background: #c03537;
}
Loading