This commit is contained in:
EugeneTes
2026-03-19 09:03:37 +01:00
parent 14fe822962
commit 83597b8763
55 changed files with 2136 additions and 32 deletions

View File

@@ -0,0 +1,17 @@
@implements IDisposable
<div class="error-preview-panel">
@foreach (var error in _errors)
{
<div class="error-card" @onclick="() => OnErrorClicked(error)">
@{
var uri = error.ImageAnalysis?.ToBase64DataUri(50);
}
@if (!string.IsNullOrEmpty(uri))
{
<img src="@uri" alt="Error" class="error-thumbnail" />
}
<span class="error-title">@error.Title</span>
</div>
}
</div>

View File

@@ -0,0 +1,59 @@
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using Microsoft.AspNetCore.Components;
using VisionBuilder.UI.Common.ViewModel;
using VisionBuilder.UI.Common.ViewModel.Classes;
namespace VisionBuilder.UI.Blazor.Components;
public partial class ErrorPreview : ComponentBase, IDisposable
{
[Parameter] public ErrorsVM? ViewModel { get; set; }
private List<ErrorData> _errors = new();
private ErrorsVM? _subscribedVm;
protected override void OnParametersSet()
{
if (_subscribedVm != ViewModel)
{
Unsubscribe();
Subscribe();
}
}
private void Subscribe()
{
_subscribedVm = ViewModel;
if (ViewModel != null)
{
ViewModel.Errors.CollectionChanged += OnCollectionChanged;
_errors = new List<ErrorData>(ViewModel.Errors);
}
}
private void Unsubscribe()
{
if (_subscribedVm != null)
_subscribedVm.Errors.CollectionChanged -= OnCollectionChanged;
_subscribedVm = null;
}
private async void OnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
if (ViewModel != null)
_errors = new List<ErrorData>(ViewModel.Errors);
await InvokeAsync(StateHasChanged);
}
private async Task OnErrorClicked(ErrorData errorData)
{
if (ViewModel != null)
await ViewModel.ShowImage(errorData);
}
public void Dispose()
{
Unsubscribe();
}
}

View File

@@ -0,0 +1,12 @@
@implements IDisposable
<div class="preview-window @(_isError ? "preview-error" : "")">
@if (!string.IsNullOrEmpty(_dataUri))
{
<img src="@_dataUri" alt="Preview" class="preview-image" />
}
else
{
<div class="preview-placeholder">No image</div>
}
</div>

View File

@@ -0,0 +1,94 @@
using System.ComponentModel;
using Microsoft.AspNetCore.Components;
using OpenCvSharp;
using VisionBuilder.UI.Blazor.Helpers;
using VisionBuilder.UI.Common.ViewModel;
namespace VisionBuilder.UI.Blazor.Components;
public partial class PreviewWindow : ComponentBase, IDisposable
{
[Parameter] public PreviewVM? ViewModel { get; set; }
private string _dataUri = string.Empty;
private bool _isError;
private DateTime _lastRenderTime = DateTime.MinValue;
private Timer? _pendingTimer;
private PreviewVM? _subscribedVm;
protected override void OnParametersSet()
{
if (_subscribedVm != ViewModel)
{
Unsubscribe();
Subscribe();
}
}
private void Subscribe()
{
_subscribedVm = ViewModel;
if (ViewModel != null)
ViewModel.PropertyChanged += OnPropertyChanged;
}
private void Unsubscribe()
{
if (_subscribedVm != null)
_subscribedVm.PropertyChanged -= OnPropertyChanged;
_subscribedVm = null;
}
private async void OnPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName != nameof(PreviewVM.ImagePreview) && e.PropertyName != nameof(PreviewVM.IsError))
return;
var now = DateTime.UtcNow;
var elapsed = (now - _lastRenderTime).TotalMilliseconds;
if (elapsed < 100)
{
// Schedule a render for when the throttle window expires,
// so we always show the latest frame.
_pendingTimer?.Dispose();
_pendingTimer = new Timer(_ => RenderLatest(), null,
(int)(100 - elapsed) + 1, Timeout.Infinite);
return;
}
await RenderCurrentFrame();
}
private async void RenderLatest()
{
_pendingTimer?.Dispose();
_pendingTimer = null;
await RenderCurrentFrame();
}
private async Task RenderCurrentFrame()
{
_lastRenderTime = DateTime.UtcNow;
var mat = ViewModel?.ImagePreview;
var isError = ViewModel?.IsError ?? false;
string dataUri = string.Empty;
if (mat != null && !mat.Empty())
{
dataUri = mat.ToBase64DataUri();
}
_dataUri = dataUri;
_isError = isError;
await InvokeAsync(StateHasChanged);
}
public void Dispose()
{
_pendingTimer?.Dispose();
Unsubscribe();
}
}

View File

@@ -0,0 +1,75 @@
@using VisionBuilder.UI.Common
@implements IDisposable
<div class="single-camera-control">
<div class="camera-header">
<div class="camera-header-preview">
<span class="camera-label">@_cameraLabel</span>
</div>
<div class="camera-header-errors">
<span class="errors-label">Errors</span>
</div>
<div class="camera-header-stats"></div>
<div class="camera-header-buttons"></div>
</div>
<div class="camera-body">
<div class="camera-preview-area">
<PreviewWindow ViewModel="@ViewModel?.PreviewVm" />
</div>
<div class="camera-divider"></div>
<div class="camera-errors-area">
<ErrorPreview ViewModel="@ViewModel?.ErrorsVm" />
</div>
<div class="camera-divider"></div>
<div class="camera-stats-area">
<Stats ViewModel="@ViewModel?.StatisticsVm" />
</div>
<div class="camera-buttons-area">
<div class="camera-buttons-stack">
<MudButton Variant="Variant.Outlined" Color="Color.Primary" FullWidth="true"
Class="camera-btn"
Disabled="@(!_canSelectRecipe)"
OnClick="@OnSelectRecipe">
@_currentRecipeName
</MudButton>
<MudButton Variant="Variant.Filled" Color="Color.Primary" FullWidth="true"
Class="camera-btn"
Disabled="@(!_canStart)"
OnClick="@OnStart">
Start
</MudButton>
<MudButton Variant="Variant.Filled" Color="Color.Primary" FullWidth="true"
Class="camera-btn"
Disabled="@(!_canStop)"
OnClick="@OnStop">
Stop
</MudButton>
@if (_showPauseButton && _canPause)
{
<MudButton Variant="Variant.Outlined" Color="Color.Primary" FullWidth="true"
Class="camera-btn"
OnClick="@OnPause">
Pause
</MudButton>
}
@if (_showPauseButton && _canResume)
{
<MudButton Variant="Variant.Outlined" Color="Color.Primary" FullWidth="true"
Class="camera-btn"
OnClick="@OnResume">
Resume
</MudButton>
}
@if (_canHotReload)
{
<MudButton Variant="Variant.Outlined" Color="Color.Primary" FullWidth="true"
Class="camera-btn"
OnClick="@OnHotReload">
Hot Reload
</MudButton>
}
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,108 @@
using System.ComponentModel;
using Microsoft.AspNetCore.Components;
using VisionBuilder.UI.Common;
namespace VisionBuilder.UI.Blazor.Components;
public partial class SingleCameraControl : ComponentBase, IDisposable
{
[Parameter] public SingleCameraVM? ViewModel { get; set; }
private string _cameraLabel = "";
private string _currentRecipeName = "Select recipe";
private bool _canSelectRecipe;
private bool _canStart;
private bool _canStop;
private bool _canPause;
private bool _canResume;
private bool _canHotReload;
private bool _showPauseButton;
private SingleCameraVM? _subscribedVm;
protected override void OnParametersSet()
{
if (_subscribedVm != ViewModel)
{
Unsubscribe();
Subscribe();
}
}
private void Subscribe()
{
_subscribedVm = ViewModel;
if (ViewModel != null)
{
ViewModel.PropertyChanged += OnPropertyChanged;
UpdateState();
}
}
private void Unsubscribe()
{
if (_subscribedVm != null)
_subscribedVm.PropertyChanged -= OnPropertyChanged;
_subscribedVm = null;
}
private void UpdateState()
{
if (ViewModel == null) return;
_cameraLabel = ViewModel.CameraLabelAndStatus;
_currentRecipeName = ViewModel.CurrentRecipeName;
_canSelectRecipe = ViewModel.IsNotRunning;
_canStart = ViewModel.CanStart;
_canStop = ViewModel.CanStop;
_canPause = ViewModel.CanPause;
_canResume = ViewModel.CanResume;
_canHotReload = ViewModel.CanHotReload;
_showPauseButton = ViewModel.CanPause || ViewModel.CanResume;
}
private async void OnPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
UpdateState();
await InvokeAsync(StateHasChanged);
}
private async Task OnSelectRecipe()
{
if (ViewModel?.SelectRecipeCommand.CanExecute(null) == true)
await ViewModel.SelectRecipe();
}
private async Task OnStart()
{
if (ViewModel?.StartCommand.CanExecute(null) == true)
await ViewModel.Start();
}
private async Task OnStop()
{
if (ViewModel?.StopCommand.CanExecute(null) == true)
await ViewModel.Stop();
}
private void OnPause()
{
if (ViewModel?.PauseCommand.CanExecute(null) == true)
ViewModel.PauseCommand.Execute(null);
}
private void OnResume()
{
if (ViewModel?.ResumeCommand.CanExecute(null) == true)
ViewModel.ResumeCommand.Execute(null);
}
private void OnHotReload()
{
if (ViewModel?.HotReloadCommand.CanExecute(null) == true)
ViewModel.HotReloadCommand.Execute(null);
}
public void Dispose()
{
Unsubscribe();
}
}

View File

@@ -0,0 +1,34 @@
@implements IDisposable
<div class="stats-panel">
@if (!string.IsNullOrEmpty(_sessionStarted))
{
<div class="stats-field">
<span class="stats-field-label">Started at:</span>
<span class="stats-field-value stats-bold">@_sessionStarted</span>
</div>
<div class="stats-field">
<span class="stats-field-label">Processing Time:</span>
<span class="stats-field-value stats-bold">@_processingTime</span>
</div>
<div class="stats-field">
<span class="stats-field-label">Total:</span>
<span class="stats-field-value">@_total</span>
</div>
<div class="stats-field">
<span class="stats-field-label">Bad:</span>
<span class="stats-field-value">@_bad</span>
</div>
<div class="stats-field">
<span class="stats-field-label">Error rate:</span>
<span class="stats-field-value">@_errorRate</span>
</div>
@if (!string.IsNullOrEmpty(_statisticsDetails))
{
<div class="stats-field">
<span class="stats-field-label">Details:</span>
<pre class="stats-field-value stats-details-text">@_statisticsDetails</pre>
</div>
}
}
</div>

View File

@@ -0,0 +1,62 @@
using System.ComponentModel;
using Microsoft.AspNetCore.Components;
using VisionBuilder.UI.Common.ViewModel;
namespace VisionBuilder.UI.Blazor.Components;
public partial class Stats : ComponentBase, IDisposable
{
[Parameter] public StatisticsVM? ViewModel { get; set; }
private string _sessionStarted = "";
private string _recipeName = "";
private string _processingTime = "";
private int _good;
private int _bad;
private int _total;
private string _errorRate = "";
private string _statisticsDetails = "";
private StatisticsVM? _subscribedVm;
protected override void OnParametersSet()
{
if (_subscribedVm != ViewModel)
{
Unsubscribe();
Subscribe();
}
}
private void Subscribe()
{
_subscribedVm = ViewModel;
if (ViewModel != null)
ViewModel.PropertyChanged += OnPropertyChanged;
}
private void Unsubscribe()
{
if (_subscribedVm != null)
_subscribedVm.PropertyChanged -= OnPropertyChanged;
_subscribedVm = null;
}
private async void OnPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
_sessionStarted = ViewModel?.SessionStarted ?? "";
_recipeName = ViewModel?.RecipeName ?? "";
_processingTime = ViewModel?.ProcessingTime ?? "";
_good = ViewModel?.Good ?? 0;
_bad = ViewModel?.Bad ?? 0;
_total = ViewModel?.Total ?? 0;
_errorRate = ViewModel?.ErrorRate ?? "";
_statisticsDetails = ViewModel?.StatisticsDetails ?? "";
await InvokeAsync(StateHasChanged);
}
public void Dispose()
{
Unsubscribe();
}
}