feat: safe image loading

add AsyncImageViewModel that loading images sequentially
This commit is contained in:
2026-03-05 09:32:24 +03:00
parent fe46722d55
commit d7c0883e6b
3 changed files with 112 additions and 27 deletions
@@ -39,7 +39,7 @@ public partial class AmineContentSeparatorCalibrationViewModel : ViewModelBase
[ObservableProperty] private Task? _captureImagesTask = null; [ObservableProperty] private Task? _captureImagesTask = null;
[ObservableProperty] private ObservableCollection<KeyValuePair<IImage, ImageRecord>> _images = new(); [ObservableProperty] private ObservableCollection<AsyncImageRecordViewModel> _images = new();
[ObservableProperty] private bool _imagesIsCapturing = false; [ObservableProperty] private bool _imagesIsCapturing = false;
[ObservableProperty] private string _imagesCapturingProgress = ""; [ObservableProperty] private string _imagesCapturingProgress = "";
[ObservableProperty] private int _imagesSectionCurrentIndex = 0; [ObservableProperty] private int _imagesSectionCurrentIndex = 0;
@@ -88,12 +88,16 @@ public partial class AmineContentSeparatorCalibrationViewModel : ViewModelBase
partial void OnEditingRecordChanged(SeparatorRecord value) partial void OnEditingRecordChanged(SeparatorRecord value)
{ {
var images = value.Images var images = value.Images
.ToAsyncEnumerable() .Select(i =>
.Select(i => new KeyValuePair<string?, ImageRecord>(_imageStorageService.GetFullPath(i.ImagePath, "separator"), i)) {
.Where(kv => kv.Key is not null) var path = _imageStorageService.GetFullPath(i.ImagePath, "separator");
.Select(kv => new KeyValuePair<IImage, ImageRecord>(new Bitmap(kv.Key!), kv.Value)); if (path is null)
return null;
return new AsyncImageRecordViewModel(path, i);
})
.OfType<AsyncImageRecordViewModel>();
Images = new(images.ToBlockingEnumerable()); Images = new(images);
} }
[RelayCommand] private void ToggleMenu() => IsMenuOpen = !IsMenuOpen; [RelayCommand] private void ToggleMenu() => IsMenuOpen = !IsMenuOpen;
@@ -277,10 +281,14 @@ public partial class AmineContentSeparatorCalibrationViewModel : ViewModelBase
_illuminatorService.TurnOff(); _illuminatorService.TurnOff();
var images = imageRecords var images = imageRecords
.ToAsyncEnumerable() .Select(i =>
.Select(i => new KeyValuePair<string?, ImageRecord>(_imageStorageService.GetFullPath(i.ImagePath, "separator"), i)) {
.Where(kv => kv.Key is not null) var path = _imageStorageService.GetFullPath(i.ImagePath, "separator");
.Select(kv => new KeyValuePair<IImage, ImageRecord>(new Bitmap(kv.Key!), kv.Value)); if (path is null)
return null;
return new AsyncImageRecordViewModel(path, i);
})
.OfType<AsyncImageRecordViewModel>();
Dispatcher.UIThread.Invoke(() => Dispatcher.UIThread.Invoke(() =>
{ {
@@ -288,7 +296,7 @@ public partial class AmineContentSeparatorCalibrationViewModel : ViewModelBase
EditingRecord.Images.AddRange(imageRecords); EditingRecord.Images.AddRange(imageRecords);
Images.Clear(); Images.Clear();
Images = new(images.ToBlockingEnumerable()); Images = new(images);
}); });
} }
catch (TaskCanceledException) catch (TaskCanceledException)
@@ -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();
}
}
}
@@ -272,14 +272,33 @@
<RowDefinition Height="*" /> <RowDefinition Height="*" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<core:ZoomPanImage <Grid
Grid.Row="0" Grid.Row="0"
Grid.RowSpan="11" Grid.RowSpan="11"
Grid.Column="0" Grid.Column="0">
<core:ZoomPanImage
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
Background="Black" Background="Black"
ImageSource="{Binding Key}" /> ImageSource="{Binding Image}"
IsVisible="{Binding IsLoading, Converter={x:Static BoolConverters.Not}}" />
<StackPanel
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
IsVisible="{Binding IsLoading}">
<ProgressBar IsIndeterminate="True" />
<TextBlock
HorizontalAlignment="Center"
FontSize="18"
Text="Изображение загружается" />
</StackPanel>
</Grid>
<TextBlock <TextBlock
Grid.Row="0" Grid.Row="0"
@@ -291,7 +310,7 @@
Grid.Column="1" Grid.Column="1"
Classes="singleline" Classes="singleline"
IsReadOnly="True" IsReadOnly="True"
Text="{Binding Value.Id}" /> Text="{Binding ImageRecord.Id}" />
<TextBlock <TextBlock
Grid.Row="2" Grid.Row="2"
@@ -303,7 +322,7 @@
Grid.Column="1" Grid.Column="1"
Classes="singleline" Classes="singleline"
IsReadOnly="True" IsReadOnly="True"
Text="{Binding Value.ImagePath}" /> Text="{Binding ImageRecord.ImagePath}" />
<TextBlock <TextBlock
Grid.Row="4" Grid.Row="4"
@@ -315,7 +334,7 @@
Grid.Column="1" Grid.Column="1"
Classes="singleline" Classes="singleline"
IsReadOnly="True" IsReadOnly="True"
Text="{Binding Value.VisibleIntensity}" /> Text="{Binding ImageRecord.VisibleIntensity}" />
<TextBlock <TextBlock
Grid.Row="6" Grid.Row="6"
@@ -327,7 +346,7 @@
Grid.Column="1" Grid.Column="1"
Classes="singleline" Classes="singleline"
IsReadOnly="True" IsReadOnly="True"
Text="{Binding Value.Uv365Intensity}" /> Text="{Binding ImageRecord.Uv365Intensity}" />
<TextBlock <TextBlock
Grid.Row="8" Grid.Row="8"
@@ -339,7 +358,7 @@
Grid.Column="1" Grid.Column="1"
Classes="singleline" Classes="singleline"
IsReadOnly="True" IsReadOnly="True"
Text="{Binding Value.Uv254Intensity}" /> Text="{Binding ImageRecord.Uv254Intensity}" />
</Grid> </Grid>
@@ -389,13 +408,20 @@
</TextBlock> </TextBlock>
<StackPanel
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
IsVisible="{Binding ImagesIsCapturing}">
<ProgressBar IsIndeterminate="True" />
<TextBlock <TextBlock
HorizontalAlignment="Center" HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="18" FontSize="18"
IsVisible="{Binding ImagesIsCapturing}"
Text="{Binding ImagesCapturingProgress}" /> Text="{Binding ImagesCapturingProgress}" />
</StackPanel>
</Grid> </Grid>
<!-- Кнопки --> <!-- Кнопки -->