From d7c0883e6bb6386e226880f7d340d82f76339685 Mon Sep 17 00:00:00 2001 From: Alek-ban Date: Thu, 5 Mar 2026 09:32:24 +0300 Subject: [PATCH] feat: safe image loading add AsyncImageViewModel that loading images sequentially --- ...ineContentSeparatorCalibrationViewModel.cs | 30 ++++++---- .../ViewModels/AsyncImageViewModel.cs | 51 ++++++++++++++++ ...AmineContentSeparatorCalibrationView.axaml | 58 ++++++++++++++----- 3 files changed, 112 insertions(+), 27 deletions(-) create mode 100644 GSS2.UI.Core/ViewModels/AsyncImageViewModel.cs diff --git a/GSS2.UI.Core/ViewModels/AmineContentSeparatorCalibrationViewModel.cs b/GSS2.UI.Core/ViewModels/AmineContentSeparatorCalibrationViewModel.cs index fe3fa49..0940649 100644 --- a/GSS2.UI.Core/ViewModels/AmineContentSeparatorCalibrationViewModel.cs +++ b/GSS2.UI.Core/ViewModels/AmineContentSeparatorCalibrationViewModel.cs @@ -39,7 +39,7 @@ public partial class AmineContentSeparatorCalibrationViewModel : ViewModelBase [ObservableProperty] private Task? _captureImagesTask = null; - [ObservableProperty] private ObservableCollection> _images = new(); + [ObservableProperty] private ObservableCollection _images = new(); [ObservableProperty] private bool _imagesIsCapturing = false; [ObservableProperty] private string _imagesCapturingProgress = ""; [ObservableProperty] private int _imagesSectionCurrentIndex = 0; @@ -88,12 +88,16 @@ public partial class AmineContentSeparatorCalibrationViewModel : ViewModelBase partial void OnEditingRecordChanged(SeparatorRecord value) { var images = value.Images - .ToAsyncEnumerable() - .Select(i => new KeyValuePair(_imageStorageService.GetFullPath(i.ImagePath, "separator"), i)) - .Where(kv => kv.Key is not null) - .Select(kv => new KeyValuePair(new Bitmap(kv.Key!), kv.Value)); + .Select(i => + { + var path = _imageStorageService.GetFullPath(i.ImagePath, "separator"); + if (path is null) + return null; + return new AsyncImageRecordViewModel(path, i); + }) + .OfType(); - Images = new(images.ToBlockingEnumerable()); + Images = new(images); } [RelayCommand] private void ToggleMenu() => IsMenuOpen = !IsMenuOpen; @@ -277,10 +281,14 @@ public partial class AmineContentSeparatorCalibrationViewModel : ViewModelBase _illuminatorService.TurnOff(); var images = imageRecords - .ToAsyncEnumerable() - .Select(i => new KeyValuePair(_imageStorageService.GetFullPath(i.ImagePath, "separator"), i)) - .Where(kv => kv.Key is not null) - .Select(kv => new KeyValuePair(new Bitmap(kv.Key!), kv.Value)); + .Select(i => + { + var path = _imageStorageService.GetFullPath(i.ImagePath, "separator"); + if (path is null) + return null; + return new AsyncImageRecordViewModel(path, i); + }) + .OfType(); Dispatcher.UIThread.Invoke(() => { @@ -288,7 +296,7 @@ public partial class AmineContentSeparatorCalibrationViewModel : ViewModelBase EditingRecord.Images.AddRange(imageRecords); Images.Clear(); - Images = new(images.ToBlockingEnumerable()); + Images = new(images); }); } catch (TaskCanceledException) diff --git a/GSS2.UI.Core/ViewModels/AsyncImageViewModel.cs b/GSS2.UI.Core/ViewModels/AsyncImageViewModel.cs new file mode 100644 index 0000000..8199745 --- /dev/null +++ b/GSS2.UI.Core/ViewModels/AsyncImageViewModel.cs @@ -0,0 +1,51 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Threading; + +using GSS2.Core.Analysis.AmineContent.Database.Calibration; + +namespace GSS2.UI.Core.ViewModels; + +public partial class AsyncImageRecordViewModel : ViewModelBase +{ + private static readonly SemaphoreSlim _semaphore = new(1, 1); + + private readonly string _path; + + [ObservableProperty] private ImageRecord _imageRecord; + [ObservableProperty] private IImage? _image; + [ObservableProperty] private bool _isLoading = true; + + public AsyncImageRecordViewModel(string path, ImageRecord imageRecord) + { + _path = path; + _imageRecord = imageRecord; + _ = LoadAsync(); + } + + private async Task LoadAsync() + { + await _semaphore.WaitAsync(); + + try + { + var bitmap = await Task.Run(() => new Bitmap(_path)); + + await Dispatcher.UIThread.InvokeAsync(() => + { + Image = bitmap; + IsLoading = false; + }); + } + catch + { + await Dispatcher.UIThread.InvokeAsync(() => IsLoading = false); + } + finally + { + _semaphore.Release(); + } + } +} \ No newline at end of file diff --git a/GSS2.UI.Core/Views/AmineContentSeparatorCalibrationView.axaml b/GSS2.UI.Core/Views/AmineContentSeparatorCalibrationView.axaml index 93a2a5a..7b33404 100644 --- a/GSS2.UI.Core/Views/AmineContentSeparatorCalibrationView.axaml +++ b/GSS2.UI.Core/Views/AmineContentSeparatorCalibrationView.axaml @@ -272,14 +272,33 @@ - + Grid.Column="0"> + + + + + + + + + + + + + Text="{Binding ImageRecord.Id}" /> + Text="{Binding ImageRecord.ImagePath}" /> + Text="{Binding ImageRecord.VisibleIntensity}" /> + Text="{Binding ImageRecord.Uv365Intensity}" /> + Text="{Binding ImageRecord.Uv254Intensity}" /> @@ -389,12 +408,19 @@ - + IsVisible="{Binding ImagesIsCapturing}"> + + + + + +