web UI
This commit is contained in:
17
VisionBuilder.UI.Blazor/Components/ErrorPreview.razor
Normal file
17
VisionBuilder.UI.Blazor/Components/ErrorPreview.razor
Normal 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>
|
||||
59
VisionBuilder.UI.Blazor/Components/ErrorPreview.razor.cs
Normal file
59
VisionBuilder.UI.Blazor/Components/ErrorPreview.razor.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
12
VisionBuilder.UI.Blazor/Components/PreviewWindow.razor
Normal file
12
VisionBuilder.UI.Blazor/Components/PreviewWindow.razor
Normal 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>
|
||||
94
VisionBuilder.UI.Blazor/Components/PreviewWindow.razor.cs
Normal file
94
VisionBuilder.UI.Blazor/Components/PreviewWindow.razor.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
75
VisionBuilder.UI.Blazor/Components/SingleCameraControl.razor
Normal file
75
VisionBuilder.UI.Blazor/Components/SingleCameraControl.razor
Normal 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>
|
||||
108
VisionBuilder.UI.Blazor/Components/SingleCameraControl.razor.cs
Normal file
108
VisionBuilder.UI.Blazor/Components/SingleCameraControl.razor.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
34
VisionBuilder.UI.Blazor/Components/Stats.razor
Normal file
34
VisionBuilder.UI.Blazor/Components/Stats.razor
Normal 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>
|
||||
62
VisionBuilder.UI.Blazor/Components/Stats.razor.cs
Normal file
62
VisionBuilder.UI.Blazor/Components/Stats.razor.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user