Add import progress/success stage to dialog

This commit is contained in:
Pat Hartl 2025-12-03 18:32:27 -06:00
parent cf27436a71
commit 3d991dc601
4 changed files with 138 additions and 50 deletions

View file

@ -0,0 +1,48 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace LANCommander.SDK;
public class AsyncEventHandler<T>
{
public event Func<T, Task>? EventRaised;
public async Task InvokeAsync(T args)
{
if (EventRaised is null)
return;
var subscribers = EventRaised.GetInvocationList()
.Cast<Func<T, Task>>();
var tasks = new List<Task>();
foreach (var sub in subscribers)
tasks.Add(SafeInvoke(sub, args));
await Task.WhenAll(tasks);
}
public async Task InvokeSequentialAsync(T args)
{
if (EventRaised is null)
return;
var subscribers = EventRaised.GetInvocationList()
.Cast<Func<T, Task>>();
foreach (var sub in subscribers)
await SafeInvoke(sub, args);
}
private async Task SafeInvoke(Func<T, Task> subscriber, T args)
{
try {
await subscriber.Invoke(args);
}
catch (Exception ex) {
Console.WriteLine($"Async event subscriber failed: {ex.Message}");
}
}
}

View file

@ -1,3 +1,4 @@
using LANCommander.SDK;
using LANCommander.SDK.Enums;
using LANCommander.SDK.Helpers;
using LANCommander.Server.Data.Models;
@ -26,10 +27,10 @@ public class ImportContext : IDisposable
private Queue<IImportItemInfo> Queue { get; } = new();
private IEnumerable<Guid> SelectedRecordIds { get; set; } = [];
public EventHandler<ImportStatusUpdate> OnImportStarted { get; set; }
public EventHandler<ImportStatusUpdate> OnImportStatusUpdate { get; set; }
public EventHandler<ImportStatusUpdate> OnImportComplete { get; set; }
public EventHandler<ImportStatusUpdate> OnImportError;
public AsyncEventHandler<ImportStatusUpdate> OnImportStarted { get; set; } = new();
public AsyncEventHandler<ImportStatusUpdate> OnImportStatusUpdate { get; set; } = new();
public AsyncEventHandler<ImportStatusUpdate> OnImportComplete { get; set; } = new();
public AsyncEventHandler<ImportStatusUpdate> OnImportError = new();
private readonly ImportService _importService;
private readonly StorageLocationService _storageLocationService;
@ -272,11 +273,11 @@ public class ImportContext : IDisposable
public async Task ImportQueueAsync()
{
OnImportStarted?.Invoke(this, new ImportStatusUpdate
await OnImportStarted?.InvokeAsync(new ImportStatusUpdate
{
Index = -1,
Total = Queue.Count,
});
})!;
int deferred = 0;
@ -284,12 +285,12 @@ public class ImportContext : IDisposable
{
var queueItem = Queue.Dequeue();
OnImportStatusUpdate?.Invoke(this, new ImportStatusUpdate
await OnImportStatusUpdate?.InvokeAsync(new ImportStatusUpdate
{
CurrentItem = queueItem,
Index = Processed,
Total = Total,
});
})!;
var success = await TryImportAsync(queueItem);
@ -306,11 +307,11 @@ public class ImportContext : IDisposable
throw new InvalidOperationException("Import deadlocked: remaining jobs cannot be satisfied.");
}
OnImportComplete?.Invoke(this, new ImportStatusUpdate
await OnImportComplete?.InvokeAsync(new ImportStatusUpdate
{
Index = Total - 1,
Total = Total,
});
})!;
_importService.RemoveContext(Id.Value);
}
@ -392,13 +393,13 @@ public class ImportContext : IDisposable
{
_logger.LogError(ex, "Error importing record {RecordName}", queueItem.Name);
OnImportError?.Invoke(this, new ImportStatusUpdate
await OnImportError?.InvokeAsync(new ImportStatusUpdate
{
CurrentItem = CurrentItem,
Index = Processed,
Total = Total,
Error = ex.Message,
});
})!;
}
return false;

View file

@ -1,14 +1,14 @@
using LANCommander.Server.ImportExport.Importers;
using LANCommander.SDK;
using LANCommander.Server.ImportExport.Models;
namespace LANCommander.Server.ImportExport.Services;
public class ImportService : IDisposable
{
public EventHandler<ImportStatusUpdate> OnImportStarted;
public EventHandler<ImportStatusUpdate> OnImportComplete;
public EventHandler<ImportStatusUpdate> OnImportStatusUpdate;
public EventHandler<ImportStatusUpdate> OnImportError;
public AsyncEventHandler<ImportStatusUpdate> OnImportStarted = new();
public AsyncEventHandler<ImportStatusUpdate> OnImportComplete = new();
public AsyncEventHandler<ImportStatusUpdate> OnImportStatusUpdate = new();
public AsyncEventHandler<ImportStatusUpdate> OnImportError = new();
private Dictionary<Guid, ImportContext> _importContexts = new();
@ -20,10 +20,10 @@ public class ImportService : IDisposable
_importContexts.Add(id, context);
context.OnImportStarted += OnImportStarted;
context.OnImportComplete += OnImportComplete;
context.OnImportStatusUpdate += OnImportStatusUpdate;
context.OnImportError += OnImportError;
context.OnImportStarted = OnImportStarted;
context.OnImportComplete = OnImportComplete;
context.OnImportStatusUpdate = OnImportStatusUpdate;
context.OnImportError = OnImportError;
return id;
}

View file

@ -2,9 +2,7 @@
@using LANCommander.Server.ImportExport
@using LANCommander.Server.ImportExport.Factories
@using LANCommander.Server.ImportExport.Models
@using LANCommander.Server.ImportExport.Services
@inherits FeedbackComponent<ImportDialogOptions>
@inject GameService GameService
@inject ArchiveService ArchiveService
@inject StorageLocationService StorageLocationService
@inject ImportContextFactory ImportContextFactory
@ -76,7 +74,7 @@ else if (_stage == ImportStage.RecordSelection)
<ChildContent>
@foreach (var item in group)
{
<TreeNode Key="@item.Id.ToString()">
<TreeNode Key="@item.Key">
<TitleTemplate>
<GridRow>
<GridCol Flex=@("auto")>
@ -105,7 +103,24 @@ else if (_stage == ImportStage.RecordSelection)
}
else if (_stage == ImportStage.Importing)
{
<Progress Type="ProgressType.Circle" Percent="_progress" />
<Flex Align="FlexAlign.Center" Direction="FlexDirection.Vertical" Gap="FlexGap.Middle">
<Progress Type="ProgressType.Circle" Percent="_importProgress" />
<Flex Gap="FlexGap.Small" Direction="FlexDirection.Vertical">
@foreach (var error in _errors)
{
<Alert Type="AlertType.Error" Message="@error" />
}
</Flex>
</Flex>
}
else if (_stage == ImportStage.Complete)
{
<Result Status="ResultStatus.Success" Title="Import Complete!">
<Extra>
<Button Type="ButtonType.Primary" OnClick="CloseFeedbackAsync">Close</Button>
</Extra>
</Result>
}
@code {
@ -113,7 +128,8 @@ else if (_stage == ImportStage.Importing)
{
Upload,
RecordSelection,
Importing
Importing,
Complete,
};
IBrowserFile File;
@ -121,7 +137,7 @@ else if (_stage == ImportStage.Importing)
StorageLocation StorageLocation = new();
ImportContext ImportContext;
IEnumerable<ImportItemInfo> ImportItems = new List<ImportItemInfo>();
IEnumerable<IImportItemInfo> ImportItems = new List<IImportItemInfo>();
string RootPath = Path.GetPathRoot(Directory.GetCurrentDirectory());
@ -131,37 +147,64 @@ else if (_stage == ImportStage.Importing)
ImportStage _stage = ImportStage.Upload;
string[] _selectedKeys;
int _progress {
get
{
return ImportContext.Processed / ImportContext.Total;
}
}
List<string> _errors = new();
int _importProgress = -1;
protected override async Task OnInitializedAsync()
{
ImportContext = ImportContextFactory.Create();
ImportContext.OnRecordError += RecordError;
ImportContext.OnRecordAdded += RecordAdded;
ImportContext.OnRecordProcessed += RecordProcessed;
ImportContext.OnImportStarted.EventRaised += ImportStarted;
ImportContext.OnImportComplete.EventRaised += ImportComplete;
ImportContext.OnImportStatusUpdate.EventRaised += ImportStatusUpdate;
ImportContext.OnImportError.EventRaised += ImportError;
StorageLocation = await StorageLocationService.FirstAsync(l => l.Default && l.Type == StorageLocationType.Archive);
}
private void RecordProcessed(object? sender, object e)
private async Task ImportStarted(ImportStatusUpdate status)
{
_stage = ImportStage.Importing;
_importProgress = -1;
await Task.Yield();
await InvokeStateHasChangedAsync();
}
private void RecordAdded(object? sender, object e)
private async Task ImportComplete(ImportStatusUpdate status)
{
_stage = ImportStage.Complete;
_importProgress = 100;
await Clear();
ImportContext.Dispose();
await Task.Yield();
await InvokeStateHasChangedAsync();
}
private async Task ImportStatusUpdate(ImportStatusUpdate status) => await UpdateProgress(status);
private void RecordError(object? sender, object e)
private async Task ImportError(ImportStatusUpdate status)
{
if (status.Error != null)
_errors.Add(status.Error);
await Task.Yield();
await InvokeStateHasChangedAsync();
}
private async Task UpdateProgress(ImportStatusUpdate? status)
{
if (status != null)
{
_importProgress = status.Total != 0 ? (int)Math.Floor((status.Index + 1) / (decimal)status.Total) : 100;
await Task.Yield();
await InvokeStateHasChangedAsync();
}
}
async Task Upload()
@ -183,17 +226,13 @@ else if (_stage == ImportStage.Importing)
await ImportContext.PrepareImportQueueAsync(selectedGuids, StorageLocation.Id);
await ImportContext.ImportQueueAsync();
MessageService.Success("Game successfully imported!");
MessageService.Success("Import started!");
}
catch (Exception ex)
{
MessageService.Error("Game could not be imported!");
MessageService.Error("Import failed!");
Logger?.LogError(ex, "An unknown error occurred while trying to import");
}
await Clear();
ImportContext.Dispose();
await CloseFeedbackAsync();
}
async Task Clear()
@ -235,7 +274,7 @@ else if (_stage == ImportStage.Importing)
ImportItems = await ImportContext.InitializeImportAsync(archivePath);
_selectedKeys = ImportItems.Select(i => i.Id.ToString()).ToArray();
_selectedKeys = ImportItems.Select(i => i.Key).ToArray();
_stage = ImportStage.RecordSelection;
@ -277,7 +316,7 @@ else if (_stage == ImportStage.Importing)
{
ImportItems = await ImportContext.InitializeImportAsync(path);
_selectedKeys = ImportItems.Select(i => i.Id.ToString()).ToArray();
_selectedKeys = ImportItems.Select(i => i.Key).ToArray();
_stage = ImportStage.RecordSelection;