diff --git a/LANCommander.SDK/AsyncEventHandler.cs b/LANCommander.SDK/AsyncEventHandler.cs new file mode 100644 index 00000000..98e875d2 --- /dev/null +++ b/LANCommander.SDK/AsyncEventHandler.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace LANCommander.SDK; + +public class AsyncEventHandler +{ + public event Func? EventRaised; + + public async Task InvokeAsync(T args) + { + if (EventRaised is null) + return; + + var subscribers = EventRaised.GetInvocationList() + .Cast>(); + + var tasks = new List(); + 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>(); + + foreach (var sub in subscribers) + await SafeInvoke(sub, args); + } + + private async Task SafeInvoke(Func subscriber, T args) + { + try { + await subscriber.Invoke(args); + } + catch (Exception ex) { + Console.WriteLine($"Async event subscriber failed: {ex.Message}"); + } + } +} \ No newline at end of file diff --git a/LANCommander.Server.ImportExport/ImportContext.cs b/LANCommander.Server.ImportExport/ImportContext.cs index 5d9db8b1..91f59683 100644 --- a/LANCommander.Server.ImportExport/ImportContext.cs +++ b/LANCommander.Server.ImportExport/ImportContext.cs @@ -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 Queue { get; } = new(); private IEnumerable SelectedRecordIds { get; set; } = []; - public EventHandler OnImportStarted { get; set; } - public EventHandler OnImportStatusUpdate { get; set; } - public EventHandler OnImportComplete { get; set; } - public EventHandler OnImportError; + public AsyncEventHandler OnImportStarted { get; set; } = new(); + public AsyncEventHandler OnImportStatusUpdate { get; set; } = new(); + public AsyncEventHandler OnImportComplete { get; set; } = new(); + public AsyncEventHandler 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; diff --git a/LANCommander.Server.ImportExport/Services/ImportService.cs b/LANCommander.Server.ImportExport/Services/ImportService.cs index faa8f26f..5035ff5e 100644 --- a/LANCommander.Server.ImportExport/Services/ImportService.cs +++ b/LANCommander.Server.ImportExport/Services/ImportService.cs @@ -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 OnImportStarted; - public EventHandler OnImportComplete; - public EventHandler OnImportStatusUpdate; - public EventHandler OnImportError; + public AsyncEventHandler OnImportStarted = new(); + public AsyncEventHandler OnImportComplete = new(); + public AsyncEventHandler OnImportStatusUpdate = new(); + public AsyncEventHandler OnImportError = new(); private Dictionary _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; } diff --git a/LANCommander.Server/UI/Components/ImportUploadDialog.razor b/LANCommander.Server/UI/Components/ImportUploadDialog.razor index 996f43b4..56a2812b 100644 --- a/LANCommander.Server/UI/Components/ImportUploadDialog.razor +++ b/LANCommander.Server/UI/Components/ImportUploadDialog.razor @@ -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 -@inject GameService GameService @inject ArchiveService ArchiveService @inject StorageLocationService StorageLocationService @inject ImportContextFactory ImportContextFactory @@ -76,7 +74,7 @@ else if (_stage == ImportStage.RecordSelection) @foreach (var item in group) { - + @@ -105,7 +103,24 @@ else if (_stage == ImportStage.RecordSelection) } else if (_stage == ImportStage.Importing) { - + + + + + @foreach (var error in _errors) + { + + } + + +} +else if (_stage == ImportStage.Complete) +{ + + + + + } @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 ImportItems = new List(); + IEnumerable ImportItems = new List(); 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 _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;