2024-10-14 23:25:35 +02:00
|
|
|
|
using ImageCatalog_2.Commands;
|
2026-03-12 18:48:13 +01:00
|
|
|
|
using ImageCatalog_2.Models;
|
2024-10-14 23:25:35 +02:00
|
|
|
|
using ImageCatalog_2.Services;
|
2026-03-12 18:48:13 +01:00
|
|
|
|
using ImageCatalog_2.ViewModels;
|
2024-10-14 23:05:18 +02:00
|
|
|
|
using System;
|
2024-10-14 22:55:52 +02:00
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
|
using System.ComponentModel;
|
2024-10-14 23:25:35 +02:00
|
|
|
|
using System.Diagnostics;
|
2026-02-26 19:17:23 +01:00
|
|
|
|
#if WINDOWS
|
2026-02-04 19:48:03 +01:00
|
|
|
|
using System.Drawing.Text;
|
2026-02-26 19:17:23 +01:00
|
|
|
|
#endif
|
2026-05-09 12:09:05 +02:00
|
|
|
|
using System.IO;
|
2024-10-14 22:55:52 +02:00
|
|
|
|
using System.Linq;
|
2026-02-26 19:17:23 +01:00
|
|
|
|
using System.Runtime.InteropServices;
|
2024-10-14 22:55:52 +02:00
|
|
|
|
using System.Text;
|
2026-02-04 23:16:06 +01:00
|
|
|
|
using System.Threading;
|
2024-10-14 22:55:52 +02:00
|
|
|
|
using System.Threading.Tasks;
|
2024-10-14 23:25:35 +02:00
|
|
|
|
using System.Windows.Input;
|
2026-05-09 17:53:15 +02:00
|
|
|
|
using AIFotoONLUS.Core;
|
2026-02-04 23:16:06 +01:00
|
|
|
|
using AutoMapper;
|
|
|
|
|
|
using MaddoShared;
|
2025-07-29 11:07:49 +02:00
|
|
|
|
using Microsoft.Extensions.Logging;
|
2024-10-14 22:55:52 +02:00
|
|
|
|
|
|
|
|
|
|
namespace ImageCatalog_2
|
|
|
|
|
|
{
|
|
|
|
|
|
public class DataModel : ViewModelBase
|
|
|
|
|
|
{
|
2024-10-14 23:25:35 +02:00
|
|
|
|
public ICommand TestCommand { get; }
|
|
|
|
|
|
public ICommand AsyncTestCommand { get; }
|
2025-07-29 11:07:49 +02:00
|
|
|
|
public ICommand AsyncCancelOperationCommand { get; }
|
2025-07-23 17:16:06 +02:00
|
|
|
|
public ICommand ProcessImagesCommand { get; }
|
2026-02-04 19:48:03 +01:00
|
|
|
|
public ICommand SelectSourceFolderCommand { get; }
|
|
|
|
|
|
public ICommand SelectDestinationFolderCommand { get; }
|
|
|
|
|
|
public ICommand SelectLogoFileCommand { get; }
|
|
|
|
|
|
public ICommand SaveSettingsCommand { get; }
|
|
|
|
|
|
public ICommand LoadSettingsCommand { get; }
|
|
|
|
|
|
public ICommand SelectColorCommand { get; }
|
2026-02-15 11:13:23 +01:00
|
|
|
|
public ICommand SelectTransparentColorCommand { get; }
|
2026-02-16 18:32:04 +01:00
|
|
|
|
public ICommand SelectModelsFolderCommand { get; }
|
|
|
|
|
|
public ICommand SelectCsvOutputCommand { get; }
|
2026-03-12 18:48:13 +01:00
|
|
|
|
public ICommand StartAiCommand { get; }
|
2026-05-09 12:09:05 +02:00
|
|
|
|
public ICommand StartFaceEncoderCommand { get; }
|
|
|
|
|
|
public ICommand StopFaceEncoderCommand { get; }
|
2024-10-14 23:25:35 +02:00
|
|
|
|
|
2024-10-14 23:05:18 +02:00
|
|
|
|
private readonly ITestService _service;
|
2025-07-29 11:10:54 +02:00
|
|
|
|
private readonly ILogger<DataModel> _logger;
|
2026-02-04 19:48:03 +01:00
|
|
|
|
private readonly ISettingsService _settingsService;
|
2026-02-21 15:53:52 +01:00
|
|
|
|
private readonly ImageCreationService _imageCreationService;
|
2026-03-12 18:48:13 +01:00
|
|
|
|
private readonly IAiExtractionService _aiExtractionService;
|
|
|
|
|
|
private readonly IImageProcessingCoordinator _imageProcessingCoordinator;
|
|
|
|
|
|
private readonly ProcessingStateViewModel _processing;
|
|
|
|
|
|
private readonly PathSettingsViewModel _paths;
|
|
|
|
|
|
private readonly AiSettingsViewModel _ai;
|
|
|
|
|
|
private readonly RaceUploadSettingsViewModel _raceUpload;
|
|
|
|
|
|
private readonly VisualSettingsViewModel _visual;
|
2026-02-04 23:16:06 +01:00
|
|
|
|
private readonly PicSettings _picSettings;
|
|
|
|
|
|
private readonly IMapper _mapper;
|
2026-05-09 12:09:05 +02:00
|
|
|
|
private readonly AsyncCommand _startFaceEncoderCommand;
|
|
|
|
|
|
private readonly AsyncCommand _stopFaceEncoderCommand;
|
|
|
|
|
|
private readonly object _faceEncoderProcessLock = new();
|
|
|
|
|
|
private Process? _faceEncoderProcess;
|
|
|
|
|
|
private CancellationTokenSource? _faceEncoderWatcherTokenSource;
|
|
|
|
|
|
private Task? _faceEncoderWatcherTask;
|
2026-05-09 15:46:41 +02:00
|
|
|
|
private CancellationTokenSource? _faceEncoderLogWatcherTokenSource;
|
|
|
|
|
|
private Task? _faceEncoderLogWatcherTask;
|
2026-05-09 12:09:05 +02:00
|
|
|
|
private bool _hasStartedFaceEncoderInSession;
|
2026-02-04 19:48:03 +01:00
|
|
|
|
|
|
|
|
|
|
// ComboBox collections
|
|
|
|
|
|
public List<string> AvailableFonts { get; }
|
|
|
|
|
|
public List<string> VerticalPositions { get; } = new() { "Alto", "Centro", "Basso" };
|
|
|
|
|
|
public List<string> HorizontalAlignments { get; } = new() { "Sinistra", "Centro", "Destra" };
|
2025-07-29 11:10:54 +02:00
|
|
|
|
|
2026-03-12 18:48:13 +01:00
|
|
|
|
[CLSCompliant(false)]
|
2026-02-28 21:34:45 +01:00
|
|
|
|
public DataModel(ITestService testService, ISettingsService settingsService,
|
2026-03-12 18:48:13 +01:00
|
|
|
|
ImageCreationService imageCreationService, IAiExtractionService aiExtractionService, IImageProcessingCoordinator imageProcessingCoordinator, PicSettings picSettings,
|
2026-02-14 22:18:56 +01:00
|
|
|
|
IMapper mapper, ILogger<DataModel> logger, MaddoShared.IVersionProvider? versionProvider = null)
|
2024-10-14 23:05:18 +02:00
|
|
|
|
{
|
|
|
|
|
|
_service = testService;
|
2025-07-29 11:07:49 +02:00
|
|
|
|
_logger = logger;
|
2026-02-04 19:48:03 +01:00
|
|
|
|
_settingsService = settingsService;
|
2026-02-04 23:16:06 +01:00
|
|
|
|
_imageCreationService = imageCreationService;
|
2026-03-12 18:48:13 +01:00
|
|
|
|
_aiExtractionService = aiExtractionService;
|
|
|
|
|
|
_imageProcessingCoordinator = imageProcessingCoordinator;
|
|
|
|
|
|
_processing = new ProcessingStateViewModel();
|
|
|
|
|
|
_processing.PropertyChanged += OnProcessingPropertyChanged;
|
|
|
|
|
|
_paths = new PathSettingsViewModel();
|
|
|
|
|
|
_paths.PropertyChanged += OnPathsPropertyChanged;
|
|
|
|
|
|
_ai = new AiSettingsViewModel();
|
|
|
|
|
|
_ai.PropertyChanged += OnAiPropertyChanged;
|
|
|
|
|
|
_raceUpload = new RaceUploadSettingsViewModel();
|
|
|
|
|
|
_raceUpload.PropertyChanged += OnRaceUploadPropertyChanged;
|
|
|
|
|
|
_visual = new VisualSettingsViewModel();
|
|
|
|
|
|
_visual.PropertyChanged += OnVisualPropertyChanged;
|
2026-02-04 23:16:06 +01:00
|
|
|
|
_picSettings = picSettings;
|
|
|
|
|
|
_mapper = mapper;
|
2026-02-14 22:18:56 +01:00
|
|
|
|
// Populate AppVersion from version provider when available
|
|
|
|
|
|
AppVersion = versionProvider?.GetVersionString() ?? string.Empty;
|
2024-10-14 23:25:35 +02:00
|
|
|
|
|
|
|
|
|
|
TestCommand = new RelayCommand(Test);
|
|
|
|
|
|
AsyncTestCommand = new AsyncCommand(TestAsync);
|
2025-07-29 11:07:49 +02:00
|
|
|
|
AsyncCancelOperationCommand = new AsyncCommand(CancelOperation);
|
2025-07-23 17:16:06 +02:00
|
|
|
|
ProcessImagesCommand = new AsyncCommand(ProcessImages);
|
2026-02-16 18:32:04 +01:00
|
|
|
|
SelectModelsFolderCommand = new RelayCommand(SelectModelsFolder);
|
|
|
|
|
|
SelectCsvOutputCommand = new RelayCommand(SelectCsvOutput);
|
2026-03-12 18:48:13 +01:00
|
|
|
|
StartAiCommand = new AsyncCommand(StartAiAsync);
|
2026-05-09 12:09:05 +02:00
|
|
|
|
_startFaceEncoderCommand = new AsyncCommand(RunFaceEncoderAsync, CanRunFaceEncoder);
|
|
|
|
|
|
_stopFaceEncoderCommand = new AsyncCommand(() => StopFaceEncoderAsync("Arresto richiesto dall'utente."), CanStopFaceEncoder);
|
|
|
|
|
|
StartFaceEncoderCommand = _startFaceEncoderCommand;
|
|
|
|
|
|
StopFaceEncoderCommand = _stopFaceEncoderCommand;
|
2026-02-28 21:34:45 +01:00
|
|
|
|
|
2026-02-04 19:48:03 +01:00
|
|
|
|
SelectSourceFolderCommand = new RelayCommand(SelectSourceFolder);
|
|
|
|
|
|
SelectDestinationFolderCommand = new RelayCommand(SelectDestinationFolder);
|
|
|
|
|
|
SelectLogoFileCommand = new RelayCommand(SelectLogoFile);
|
|
|
|
|
|
SaveSettingsCommand = new RelayCommand(SaveSettings);
|
|
|
|
|
|
LoadSettingsCommand = new RelayCommand(LoadSettings);
|
|
|
|
|
|
SelectColorCommand = new RelayCommand(SelectColor);
|
2026-02-15 11:13:23 +01:00
|
|
|
|
SelectTransparentColorCommand = new RelayCommand(SelectTransparentColor);
|
2026-02-04 19:48:03 +01:00
|
|
|
|
|
|
|
|
|
|
// Load available fonts
|
|
|
|
|
|
AvailableFonts = LoadAvailableFonts();
|
2026-05-09 17:53:15 +02:00
|
|
|
|
RefreshNumberAiGpuCapabilities();
|
2026-05-09 12:09:05 +02:00
|
|
|
|
RefreshFaceExecutableCapabilities();
|
2026-02-04 19:48:03 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 18:48:13 +01:00
|
|
|
|
private async Task StartAiAsync()
|
2026-02-16 18:32:04 +01:00
|
|
|
|
{
|
2026-03-12 18:48:13 +01:00
|
|
|
|
MainToken = new CancellationTokenSource();
|
|
|
|
|
|
try
|
2026-02-16 18:32:04 +01:00
|
|
|
|
{
|
2026-03-12 18:48:13 +01:00
|
|
|
|
await RunAiExtractionCoreAsync(MainToken.Token, useDestination: true, recursive: true).ConfigureAwait(false);
|
2026-02-16 18:32:04 +01:00
|
|
|
|
}
|
2026-03-12 18:48:13 +01:00
|
|
|
|
catch (OperationCanceledException)
|
2026-02-16 18:32:04 +01:00
|
|
|
|
{
|
2026-05-09 18:54:20 +02:00
|
|
|
|
await InvokeOnUiThreadAsync(() => NumberAiStatsSummary = "OCR annullato.").ConfigureAwait(false);
|
2026-02-16 18:32:04 +01:00
|
|
|
|
}
|
2026-05-09 17:53:15 +02:00
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
_logger.LogError(ex, "AI extraction failed");
|
|
|
|
|
|
if (UseNumberAiGpu)
|
|
|
|
|
|
{
|
|
|
|
|
|
RefreshNumberAiGpuCapabilities();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-09 18:54:20 +02:00
|
|
|
|
await InvokeOnUiThreadAsync(() => NumberAiStatsSummary = $"Errore OCR: {ex.GetBaseException().Message}").ConfigureAwait(false);
|
|
|
|
|
|
|
2026-05-09 17:53:15 +02:00
|
|
|
|
await ShowErrorMessageAsync("Errore AI", ex.GetBaseException().Message).ConfigureAwait(false);
|
|
|
|
|
|
}
|
2026-03-12 18:48:13 +01:00
|
|
|
|
finally
|
2026-02-16 18:32:04 +01:00
|
|
|
|
{
|
2026-03-12 18:48:13 +01:00
|
|
|
|
MainToken = null;
|
2026-02-16 18:32:04 +01:00
|
|
|
|
}
|
2026-03-12 18:48:13 +01:00
|
|
|
|
}
|
2026-02-16 18:32:04 +01:00
|
|
|
|
|
2026-05-09 17:27:05 +02:00
|
|
|
|
private async Task RunAiExtractionCoreAsync(CancellationToken token, bool useDestination = false, bool recursive = false, bool failOnInvalidPath = false)
|
2026-03-12 18:48:13 +01:00
|
|
|
|
{
|
|
|
|
|
|
var searchRoot = useDestination ? DestinationPath : SourcePath;
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(searchRoot) || !System.IO.Directory.Exists(searchRoot))
|
2026-02-16 18:32:04 +01:00
|
|
|
|
{
|
2026-03-12 18:48:13 +01:00
|
|
|
|
_logger.LogWarning("AI extraction path invalid: {Path}", searchRoot);
|
2026-05-09 17:27:05 +02:00
|
|
|
|
if (failOnInvalidPath)
|
|
|
|
|
|
{
|
|
|
|
|
|
throw new DirectoryNotFoundException($"AI extraction path invalid: {searchRoot}");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 18:48:13 +01:00
|
|
|
|
return;
|
2026-02-16 18:32:04 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 18:48:13 +01:00
|
|
|
|
await InvokeOnUiThreadAsync(() =>
|
2026-02-16 18:32:04 +01:00
|
|
|
|
{
|
2026-03-12 18:48:13 +01:00
|
|
|
|
PreviewResults.Clear();
|
|
|
|
|
|
AiProgress = 0;
|
2026-05-09 18:54:20 +02:00
|
|
|
|
NumberAiStatsSummary = BuildNumberAiIdleSummary();
|
2026-03-12 18:48:13 +01:00
|
|
|
|
}).ConfigureAwait(false);
|
2026-02-16 18:32:04 +01:00
|
|
|
|
|
2026-05-09 18:54:20 +02:00
|
|
|
|
var summary = await _aiExtractionService.RunAsync(
|
2026-03-12 18:48:13 +01:00
|
|
|
|
new AiExtractionRequest
|
2026-02-16 18:32:04 +01:00
|
|
|
|
{
|
2026-03-12 18:48:13 +01:00
|
|
|
|
SearchRoot = searchRoot,
|
|
|
|
|
|
Recursive = recursive,
|
2026-05-09 17:53:15 +02:00
|
|
|
|
IncludeThumbnails = IncludeNumberAiThumbnails,
|
2026-05-09 17:27:05 +02:00
|
|
|
|
ModelsFolderPath = ModelsFolderPath,
|
|
|
|
|
|
UseGpu = UseNumberAiGpu,
|
2026-05-09 18:54:20 +02:00
|
|
|
|
WorkloadLevel = NumberAiWorkloadLevel,
|
2026-03-12 18:48:13 +01:00
|
|
|
|
CsvOutputPath = CsvOutputPath
|
|
|
|
|
|
},
|
|
|
|
|
|
token,
|
2026-05-09 17:53:15 +02:00
|
|
|
|
result => InvokeOnUiThreadAsync(() =>
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!string.IsNullOrWhiteSpace(result.Text))
|
|
|
|
|
|
{
|
|
|
|
|
|
PreviewResults.Add(result);
|
|
|
|
|
|
}
|
|
|
|
|
|
}),
|
2026-05-09 18:54:20 +02:00
|
|
|
|
progress => InvokeOnUiThreadAsync(() =>
|
|
|
|
|
|
{
|
|
|
|
|
|
AiProgress = progress.PercentComplete;
|
|
|
|
|
|
NumberAiStatsSummary = BuildNumberAiProgressSummary(progress);
|
|
|
|
|
|
})).ConfigureAwait(false);
|
|
|
|
|
|
|
|
|
|
|
|
await InvokeOnUiThreadAsync(() =>
|
|
|
|
|
|
{
|
|
|
|
|
|
AiProgress = summary.TotalFiles > 0 ? 100 : 0;
|
|
|
|
|
|
NumberAiStatsSummary = BuildNumberAiCompletionSummary(summary);
|
|
|
|
|
|
}).ConfigureAwait(false);
|
2026-02-16 18:32:04 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 18:43:07 +01:00
|
|
|
|
/// <summary>
|
2026-05-09 14:04:21 +02:00
|
|
|
|
/// Optional UI-thread invoker set by the active UI layer.
|
2026-02-26 18:43:07 +01:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
public Action<Action>? UiInvoker { get; set; }
|
|
|
|
|
|
|
2026-02-16 18:32:04 +01:00
|
|
|
|
private Task InvokeOnUiThreadAsync(Action action)
|
|
|
|
|
|
{
|
|
|
|
|
|
return Task.Run(() =>
|
|
|
|
|
|
{
|
2026-02-26 18:43:07 +01:00
|
|
|
|
if (UiInvoker != null)
|
|
|
|
|
|
UiInvoker(action);
|
|
|
|
|
|
else
|
2026-05-09 14:04:21 +02:00
|
|
|
|
action();
|
2026-02-16 18:32:04 +01:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 18:48:13 +01:00
|
|
|
|
public AiSettingsViewModel Ai => _ai;
|
|
|
|
|
|
public RaceUploadSettingsViewModel RaceUpload => _raceUpload;
|
|
|
|
|
|
|
2026-02-16 18:32:04 +01:00
|
|
|
|
// AI properties
|
|
|
|
|
|
public bool ExtractNumbers
|
|
|
|
|
|
{
|
2026-03-12 18:48:13 +01:00
|
|
|
|
get => _ai.ExtractNumbers;
|
|
|
|
|
|
set => _ai.ExtractNumbers = value;
|
2026-02-16 18:32:04 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public string ModelsFolderPath
|
|
|
|
|
|
{
|
2026-03-12 18:48:13 +01:00
|
|
|
|
get => _ai.ModelsFolderPath;
|
2026-05-09 17:53:15 +02:00
|
|
|
|
set
|
|
|
|
|
|
{
|
|
|
|
|
|
_ai.ModelsFolderPath = value;
|
|
|
|
|
|
RefreshNumberAiGpuCapabilities();
|
|
|
|
|
|
}
|
2026-02-16 18:32:04 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public string CsvOutputPath
|
|
|
|
|
|
{
|
2026-03-12 18:48:13 +01:00
|
|
|
|
get => _ai.CsvOutputPath;
|
|
|
|
|
|
set => _ai.CsvOutputPath = value;
|
2026-02-16 18:32:04 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-09 17:27:05 +02:00
|
|
|
|
public bool UseNumberAiGpu
|
|
|
|
|
|
{
|
|
|
|
|
|
get => _ai.UseNumberAiGpu;
|
2026-05-09 17:53:15 +02:00
|
|
|
|
set => SetUseNumberAiGpu(value);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public bool NumberAiGpuOptionEnabled
|
|
|
|
|
|
{
|
|
|
|
|
|
get => _ai.NumberAiGpuOptionEnabled;
|
|
|
|
|
|
private set => _ai.NumberAiGpuOptionEnabled = value;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public bool IncludeNumberAiThumbnails
|
|
|
|
|
|
{
|
|
|
|
|
|
get => _ai.IncludeNumberAiThumbnails;
|
|
|
|
|
|
set => _ai.IncludeNumberAiThumbnails = value;
|
2026-05-09 17:27:05 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-09 18:54:20 +02:00
|
|
|
|
public IReadOnlyList<int> NumberAiWorkloadOptions { get; } = [1, 2, 3, 4, 5];
|
|
|
|
|
|
|
|
|
|
|
|
public int NumberAiWorkloadLevel
|
|
|
|
|
|
{
|
|
|
|
|
|
get => _ai.NumberAiWorkloadLevel;
|
|
|
|
|
|
set => _ai.NumberAiWorkloadLevel = NormalizeNumberAiWorkloadLevel(value);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public string NumberAiStatsSummary
|
|
|
|
|
|
{
|
|
|
|
|
|
get => _ai.NumberAiStatsSummary;
|
|
|
|
|
|
private set => _ai.NumberAiStatsSummary = value;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-28 21:34:45 +01:00
|
|
|
|
public string FaceExecutablePath
|
|
|
|
|
|
{
|
2026-03-12 18:48:13 +01:00
|
|
|
|
get => _ai.FaceExecutablePath;
|
2026-05-09 12:09:05 +02:00
|
|
|
|
set
|
|
|
|
|
|
{
|
|
|
|
|
|
var normalizedValue = value ?? string.Empty;
|
|
|
|
|
|
if (string.Equals(_ai.FaceExecutablePath, normalizedValue, StringComparison.Ordinal))
|
|
|
|
|
|
{
|
|
|
|
|
|
RefreshFaceExecutableCapabilities();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
_ai.FaceExecutablePath = normalizedValue;
|
|
|
|
|
|
RefreshFaceExecutableCapabilities();
|
|
|
|
|
|
}
|
2026-02-28 21:34:45 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public string FaceOutputFolderPath
|
|
|
|
|
|
{
|
2026-03-12 18:48:13 +01:00
|
|
|
|
get => _ai.FaceOutputFolderPath;
|
|
|
|
|
|
set => _ai.FaceOutputFolderPath = value;
|
2026-02-28 21:34:45 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-09 12:09:05 +02:00
|
|
|
|
public bool FaceRecursive
|
|
|
|
|
|
{
|
|
|
|
|
|
get => _ai.FaceRecursive;
|
|
|
|
|
|
set => _ai.FaceRecursive = value;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-09 15:46:41 +02:00
|
|
|
|
public bool FaceIncludeThumbnails
|
|
|
|
|
|
{
|
|
|
|
|
|
get => _ai.FaceIncludeThumbnails;
|
|
|
|
|
|
set => _ai.FaceIncludeThumbnails = value;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public IReadOnlyList<int> FaceParallelismOptions { get; } = [1, 2, 3, 4, 5];
|
|
|
|
|
|
|
|
|
|
|
|
public int FaceParallelism
|
|
|
|
|
|
{
|
|
|
|
|
|
get => _ai.FaceParallelism;
|
|
|
|
|
|
set => _ai.FaceParallelism = value;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public int FaceMinSize
|
|
|
|
|
|
{
|
|
|
|
|
|
get => _ai.FaceMinSize;
|
|
|
|
|
|
set => _ai.FaceMinSize = value;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public bool FaceUpsample
|
|
|
|
|
|
{
|
|
|
|
|
|
get => _ai.FaceUpsample;
|
|
|
|
|
|
set => _ai.FaceUpsample = value;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-09 12:09:05 +02:00
|
|
|
|
public bool FaceGpuOptionEnabled => _ai.FaceGpuOptionEnabled;
|
|
|
|
|
|
|
|
|
|
|
|
public bool UseFaceGpu
|
|
|
|
|
|
{
|
|
|
|
|
|
get => _ai.UseFaceGpu;
|
|
|
|
|
|
set => SetUseFaceGpu(value);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public bool IsFaceEncoderRunning
|
|
|
|
|
|
{
|
|
|
|
|
|
get => _ai.IsFaceEncoderRunning;
|
|
|
|
|
|
private set => _ai.IsFaceEncoderRunning = value;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public string FaceStatusMessage
|
|
|
|
|
|
{
|
|
|
|
|
|
get => _ai.FaceStatusMessage;
|
|
|
|
|
|
private set => _ai.FaceStatusMessage = value;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public string FaceCommandOutput
|
|
|
|
|
|
{
|
|
|
|
|
|
get => _ai.FaceCommandOutput;
|
|
|
|
|
|
private set => _ai.FaceCommandOutput = value;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-28 21:34:45 +01:00
|
|
|
|
// Race upload settings
|
|
|
|
|
|
public string ApiLogin
|
|
|
|
|
|
{
|
2026-03-12 18:48:13 +01:00
|
|
|
|
get => _raceUpload.ApiLogin;
|
|
|
|
|
|
set => _raceUpload.ApiLogin = value;
|
2026-02-28 21:34:45 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public string ApiPassword
|
|
|
|
|
|
{
|
2026-03-12 18:48:13 +01:00
|
|
|
|
get => _raceUpload.ApiPassword;
|
|
|
|
|
|
set => _raceUpload.ApiPassword = value;
|
2026-02-28 21:34:45 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public string ApiRaceDescription
|
|
|
|
|
|
{
|
2026-03-12 18:48:13 +01:00
|
|
|
|
get => _raceUpload.ApiRaceDescription;
|
|
|
|
|
|
set => _raceUpload.ApiRaceDescription = value;
|
2026-02-28 21:34:45 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public string ApiRaceTypeId
|
|
|
|
|
|
{
|
2026-03-12 18:48:13 +01:00
|
|
|
|
get => _raceUpload.ApiRaceTypeId;
|
|
|
|
|
|
set => _raceUpload.ApiRaceTypeId = value;
|
2026-02-28 21:34:45 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public DateTime ApiRaceStartDate
|
|
|
|
|
|
{
|
2026-03-12 18:48:13 +01:00
|
|
|
|
get => _raceUpload.ApiRaceStartDate;
|
|
|
|
|
|
set => _raceUpload.ApiRaceStartDate = value;
|
2026-02-28 21:34:45 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public DateTime ApiRaceEndDate
|
|
|
|
|
|
{
|
2026-03-12 18:48:13 +01:00
|
|
|
|
get => _raceUpload.ApiRaceEndDate;
|
|
|
|
|
|
set => _raceUpload.ApiRaceEndDate = value;
|
2026-02-28 21:34:45 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public string ApiPathBase
|
|
|
|
|
|
{
|
2026-03-12 18:48:13 +01:00
|
|
|
|
get => _raceUpload.ApiPathBase;
|
|
|
|
|
|
set => _raceUpload.ApiPathBase = value;
|
2026-02-28 21:34:45 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public string ApiLocalita
|
|
|
|
|
|
{
|
2026-03-12 18:48:13 +01:00
|
|
|
|
get => _raceUpload.ApiLocalita;
|
|
|
|
|
|
set => _raceUpload.ApiLocalita = value;
|
2026-02-28 21:34:45 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public int ApiEventoInLineaIndex
|
|
|
|
|
|
{
|
2026-03-12 18:48:13 +01:00
|
|
|
|
get => _raceUpload.ApiEventoInLineaIndex;
|
|
|
|
|
|
set => _raceUpload.ApiEventoInLineaIndex = value;
|
2026-02-28 21:34:45 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public int ApiTipoIndexValue
|
|
|
|
|
|
{
|
2026-03-12 18:48:13 +01:00
|
|
|
|
get => _raceUpload.ApiTipoIndexValue;
|
|
|
|
|
|
set => _raceUpload.ApiTipoIndexValue = value;
|
2026-02-28 21:34:45 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public int ApiFreeEventIndex
|
|
|
|
|
|
{
|
2026-03-12 18:48:13 +01:00
|
|
|
|
get => _raceUpload.ApiFreeEventIndex;
|
|
|
|
|
|
set => _raceUpload.ApiFreeEventIndex = value;
|
2026-02-28 21:34:45 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public string ApiRaceId
|
|
|
|
|
|
{
|
2026-03-12 18:48:13 +01:00
|
|
|
|
get => _raceUpload.ApiRaceId;
|
|
|
|
|
|
set => _raceUpload.ApiRaceId = value;
|
2026-02-28 21:34:45 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public string ApiRemoteProcessedBasePath
|
|
|
|
|
|
{
|
2026-03-12 18:48:13 +01:00
|
|
|
|
get => _raceUpload.ApiRemoteProcessedBasePath;
|
|
|
|
|
|
set => _raceUpload.ApiRemoteProcessedBasePath = value;
|
2026-02-28 21:34:45 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-16 18:32:04 +01:00
|
|
|
|
// Preview results for DataGrid
|
2026-03-12 18:48:13 +01:00
|
|
|
|
public System.Collections.ObjectModel.ObservableCollection<AiResultItem> PreviewResults => _ai.PreviewResults;
|
2026-02-16 18:32:04 +01:00
|
|
|
|
|
2026-03-12 18:48:13 +01:00
|
|
|
|
public double AiProgress
|
2026-02-16 18:32:04 +01:00
|
|
|
|
{
|
2026-03-12 18:48:13 +01:00
|
|
|
|
get => _ai.AiProgress;
|
|
|
|
|
|
set => _ai.AiProgress = value;
|
2026-02-16 18:32:04 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-09 18:54:20 +02:00
|
|
|
|
private string BuildNumberAiIdleSummary()
|
|
|
|
|
|
{
|
|
|
|
|
|
var workerCount = ResolveNumberAiWorkerCount(UseNumberAiGpu, NumberAiWorkloadLevel);
|
2026-05-09 19:31:21 +02:00
|
|
|
|
var unit = UseNumberAiGpu ? "batch" : "worker";
|
|
|
|
|
|
return $"In attesa. Carico {NumberAiWorkloadLevel}/5, {workerCount} {unit}, 0.00 img/s.";
|
2026-05-09 18:54:20 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static string BuildNumberAiProgressSummary(AiExtractionProgressUpdate progress)
|
|
|
|
|
|
{
|
2026-05-09 19:31:21 +02:00
|
|
|
|
var unit = progress.UseGpu ? "batch" : "worker";
|
|
|
|
|
|
return $"{progress.ProcessedFiles}/{progress.TotalFiles} immagini, media {progress.AverageImagesPerSecond:F2} img/s, carico {progress.WorkloadLevel}/5, {progress.WorkerCount} {unit}.";
|
2026-05-09 18:54:20 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static string BuildNumberAiCompletionSummary(AiExtractionRunSummary summary)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (summary.TotalFiles == 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
return "Nessuna immagine trovata per OCR.";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-09 19:31:21 +02:00
|
|
|
|
var unit = summary.UseGpu ? "batch" : "worker";
|
|
|
|
|
|
return $"Completato: {summary.ProcessedFiles}/{summary.TotalFiles} immagini, media finale {summary.AverageImagesPerSecond:F2} img/s, errori {summary.FailedFiles}, carico {summary.WorkloadLevel}/5, {summary.WorkerCount} {unit}.";
|
2026-05-09 18:54:20 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-04 19:48:03 +01:00
|
|
|
|
private List<string> LoadAvailableFonts()
|
|
|
|
|
|
{
|
2026-02-26 19:17:23 +01:00
|
|
|
|
#if WINDOWS
|
2026-02-04 19:48:03 +01:00
|
|
|
|
var fonts = new List<string>();
|
|
|
|
|
|
using (var installedFonts = new InstalledFontCollection())
|
|
|
|
|
|
{
|
|
|
|
|
|
fonts.AddRange(installedFonts.Families.Select(f => f.Name));
|
|
|
|
|
|
}
|
|
|
|
|
|
return fonts;
|
2026-02-26 19:17:23 +01:00
|
|
|
|
#else
|
|
|
|
|
|
return new List<string>();
|
|
|
|
|
|
#endif
|
2024-10-14 23:05:18 +02:00
|
|
|
|
}
|
2024-10-14 22:55:52 +02:00
|
|
|
|
|
2025-07-29 11:07:49 +02:00
|
|
|
|
private CancellationTokenSource? _mainToken;
|
|
|
|
|
|
|
|
|
|
|
|
public CancellationTokenSource? MainToken
|
|
|
|
|
|
{
|
|
|
|
|
|
get => _mainToken;
|
|
|
|
|
|
set
|
|
|
|
|
|
{
|
|
|
|
|
|
_mainToken = value;
|
|
|
|
|
|
NotifyPropertyChanged();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-10-14 22:55:52 +02:00
|
|
|
|
public string SourcePath
|
|
|
|
|
|
{
|
2026-03-12 18:48:13 +01:00
|
|
|
|
get => _paths.SourcePath;
|
|
|
|
|
|
set => _paths.SourcePath = value;
|
2024-10-14 22:55:52 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public string DestinationPath
|
|
|
|
|
|
{
|
2026-03-12 18:48:13 +01:00
|
|
|
|
get => _paths.DestinationPath;
|
|
|
|
|
|
set => _paths.DestinationPath = value;
|
2024-10-14 22:55:52 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-28 09:49:55 +02:00
|
|
|
|
public string HorizontalText
|
|
|
|
|
|
{
|
2026-03-12 18:48:13 +01:00
|
|
|
|
get => _visual.HorizontalText;
|
|
|
|
|
|
set => _visual.HorizontalText = value;
|
2025-07-28 09:49:55 +02:00
|
|
|
|
}
|
2025-07-29 11:10:54 +02:00
|
|
|
|
|
2025-07-28 10:34:03 +02:00
|
|
|
|
public string VerticalText
|
|
|
|
|
|
{
|
2026-03-12 18:48:13 +01:00
|
|
|
|
get => _visual.VerticalText;
|
|
|
|
|
|
set => _visual.VerticalText = value;
|
2025-07-28 10:34:03 +02:00
|
|
|
|
}
|
2025-07-28 09:49:55 +02:00
|
|
|
|
|
2025-07-28 14:45:03 +02:00
|
|
|
|
public bool OverwriteImages
|
|
|
|
|
|
{
|
2026-03-12 18:48:13 +01:00
|
|
|
|
get => _visual.OverwriteImages;
|
|
|
|
|
|
set => _visual.OverwriteImages = value;
|
2025-07-28 14:45:03 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-10-14 23:48:21 +02:00
|
|
|
|
private bool _uiEnabled = true;
|
|
|
|
|
|
|
|
|
|
|
|
public bool UiEnabled
|
|
|
|
|
|
{
|
2025-07-28 10:34:03 +02:00
|
|
|
|
get => _uiEnabled;
|
2024-10-14 23:48:21 +02:00
|
|
|
|
set
|
|
|
|
|
|
{
|
|
|
|
|
|
_uiEnabled = value;
|
|
|
|
|
|
NotifyPropertyChanged();
|
2026-02-04 21:12:27 +01:00
|
|
|
|
NotifyPropertyChanged(nameof(UiDisabled));
|
2024-10-14 23:48:21 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-07-29 11:10:54 +02:00
|
|
|
|
|
2025-07-29 10:34:23 +02:00
|
|
|
|
public bool UiDisabled => !_uiEnabled;
|
|
|
|
|
|
|
2026-03-12 18:48:13 +01:00
|
|
|
|
public ProcessingStateViewModel Processing => _processing;
|
|
|
|
|
|
public PathSettingsViewModel Paths => _paths;
|
|
|
|
|
|
public VisualSettingsViewModel Visual => _visual;
|
2025-07-29 10:34:23 +02:00
|
|
|
|
|
2026-03-12 18:48:13 +01:00
|
|
|
|
private void OnProcessingPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
2025-07-29 10:34:23 +02:00
|
|
|
|
{
|
2026-03-12 18:48:13 +01:00
|
|
|
|
if (string.IsNullOrWhiteSpace(e.PropertyName))
|
2025-07-29 10:34:23 +02:00
|
|
|
|
{
|
2026-03-12 18:48:13 +01:00
|
|
|
|
return;
|
2025-07-29 10:34:23 +02:00
|
|
|
|
}
|
2026-03-12 18:48:13 +01:00
|
|
|
|
|
|
|
|
|
|
// Keep existing DataModel bindings working while state lives in child viewmodel.
|
|
|
|
|
|
NotifyPropertyChanged(e.PropertyName);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void OnPathsPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(e.PropertyName))
|
|
|
|
|
|
{
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
NotifyPropertyChanged(e.PropertyName);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void OnAiPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(e.PropertyName))
|
|
|
|
|
|
{
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
NotifyPropertyChanged(e.PropertyName);
|
2026-05-09 12:09:05 +02:00
|
|
|
|
UpdateFaceEncoderCommandStates();
|
2026-03-12 18:48:13 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void OnRaceUploadPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(e.PropertyName))
|
|
|
|
|
|
{
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
NotifyPropertyChanged(e.PropertyName);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void OnVisualPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(e.PropertyName))
|
|
|
|
|
|
{
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
NotifyPropertyChanged(e.PropertyName);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public string SpeedCounter
|
|
|
|
|
|
{
|
|
|
|
|
|
get => _processing.SpeedCounter;
|
|
|
|
|
|
set => _processing.SpeedCounter = value;
|
2025-07-29 10:34:23 +02:00
|
|
|
|
}
|
2024-10-14 23:48:21 +02:00
|
|
|
|
|
2025-09-19 09:53:31 +02:00
|
|
|
|
private int _chunkSize;
|
|
|
|
|
|
|
|
|
|
|
|
public int ChunkSize
|
|
|
|
|
|
{
|
|
|
|
|
|
get => _chunkSize;
|
|
|
|
|
|
set
|
|
|
|
|
|
{
|
|
|
|
|
|
_chunkSize = value;
|
|
|
|
|
|
NotifyPropertyChanged();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private int _threadsCount;
|
|
|
|
|
|
|
|
|
|
|
|
public int ThreadsCount
|
|
|
|
|
|
{
|
|
|
|
|
|
get => _threadsCount;
|
|
|
|
|
|
set
|
|
|
|
|
|
{
|
|
|
|
|
|
_threadsCount = value;
|
|
|
|
|
|
NotifyPropertyChanged();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-04 19:48:03 +01:00
|
|
|
|
// Thumbnail settings
|
|
|
|
|
|
public string ThumbnailPrefix
|
|
|
|
|
|
{
|
2026-03-12 18:48:13 +01:00
|
|
|
|
get => _visual.ThumbnailPrefix;
|
|
|
|
|
|
set => _visual.ThumbnailPrefix = value;
|
2026-02-04 19:48:03 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public int ThumbnailHeight
|
|
|
|
|
|
{
|
2026-03-12 18:48:13 +01:00
|
|
|
|
get => _visual.ThumbnailHeight;
|
|
|
|
|
|
set => _visual.ThumbnailHeight = value;
|
2026-02-04 19:48:03 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public int ThumbnailWidth
|
|
|
|
|
|
{
|
2026-03-12 18:48:13 +01:00
|
|
|
|
get => _visual.ThumbnailWidth;
|
|
|
|
|
|
set => _visual.ThumbnailWidth = value;
|
2026-02-04 19:48:03 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Big photo settings
|
|
|
|
|
|
public int PhotoBigHeight
|
|
|
|
|
|
{
|
2026-03-12 18:48:13 +01:00
|
|
|
|
get => _visual.PhotoBigHeight;
|
|
|
|
|
|
set => _visual.PhotoBigHeight = value;
|
2026-02-04 19:48:03 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public int PhotoBigWidth
|
|
|
|
|
|
{
|
2026-03-12 18:48:13 +01:00
|
|
|
|
get => _visual.PhotoBigWidth;
|
|
|
|
|
|
set => _visual.PhotoBigWidth = value;
|
2026-02-04 19:48:03 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Font settings
|
|
|
|
|
|
public int FontSize
|
|
|
|
|
|
{
|
2026-03-12 18:48:13 +01:00
|
|
|
|
get => _visual.FontSize;
|
|
|
|
|
|
set => _visual.FontSize = value;
|
2026-02-04 19:48:03 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public int FontSizeThumbnail
|
|
|
|
|
|
{
|
2026-03-12 18:48:13 +01:00
|
|
|
|
get => _visual.FontSizeThumbnail;
|
|
|
|
|
|
set => _visual.FontSizeThumbnail = value;
|
2026-02-04 19:48:03 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public string FontName
|
|
|
|
|
|
{
|
2026-03-12 18:48:13 +01:00
|
|
|
|
get => _visual.FontName;
|
|
|
|
|
|
set => _visual.FontName = value;
|
2026-02-04 19:48:03 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public bool FontBold
|
|
|
|
|
|
{
|
2026-03-12 18:48:13 +01:00
|
|
|
|
get => _visual.FontBold;
|
|
|
|
|
|
set => _visual.FontBold = value;
|
2026-02-04 19:48:03 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Text settings
|
|
|
|
|
|
public int TextTransparency
|
|
|
|
|
|
{
|
2026-03-12 18:48:13 +01:00
|
|
|
|
get => _visual.TextTransparency;
|
|
|
|
|
|
set => _visual.TextTransparency = value;
|
2026-02-04 19:48:03 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public int TextMargin
|
|
|
|
|
|
{
|
2026-03-12 18:48:13 +01:00
|
|
|
|
get => _visual.TextMargin;
|
|
|
|
|
|
set => _visual.TextMargin = value;
|
2026-02-04 19:48:03 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public string TextColorRGB
|
|
|
|
|
|
{
|
2026-03-12 18:48:13 +01:00
|
|
|
|
get => _visual.TextColorRGB;
|
|
|
|
|
|
set => _visual.TextColorRGB = value;
|
2026-02-04 19:48:03 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-15 11:13:23 +01:00
|
|
|
|
public string TransparentColor
|
|
|
|
|
|
{
|
2026-03-12 18:48:13 +01:00
|
|
|
|
get => _visual.TransparentColor;
|
|
|
|
|
|
set => _visual.TransparentColor = value;
|
2026-02-15 11:13:23 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public bool UseTransparentColor
|
|
|
|
|
|
{
|
2026-03-12 18:48:13 +01:00
|
|
|
|
get => _visual.UseTransparentColor;
|
|
|
|
|
|
set => _visual.UseTransparentColor = value;
|
2026-02-15 11:13:23 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-04 19:48:03 +01:00
|
|
|
|
// Logo/Watermark settings
|
|
|
|
|
|
public string LogoFile
|
|
|
|
|
|
{
|
2026-03-12 18:48:13 +01:00
|
|
|
|
get => _visual.LogoFile;
|
|
|
|
|
|
set => _visual.LogoFile = value;
|
2026-02-04 19:48:03 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public int LogoHeight
|
|
|
|
|
|
{
|
2026-03-12 18:48:13 +01:00
|
|
|
|
get => _visual.LogoHeight;
|
|
|
|
|
|
set => _visual.LogoHeight = value;
|
2026-02-04 19:48:03 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public int LogoWidth
|
|
|
|
|
|
{
|
2026-03-12 18:48:13 +01:00
|
|
|
|
get => _visual.LogoWidth;
|
|
|
|
|
|
set => _visual.LogoWidth = value;
|
2026-02-04 19:48:03 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public int LogoMargin
|
|
|
|
|
|
{
|
2026-03-12 18:48:13 +01:00
|
|
|
|
get => _visual.LogoMargin;
|
|
|
|
|
|
set => _visual.LogoMargin = value;
|
2026-02-04 19:48:03 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public int LogoTransparency
|
|
|
|
|
|
{
|
2026-03-12 18:48:13 +01:00
|
|
|
|
get => _visual.LogoTransparency;
|
|
|
|
|
|
set => _visual.LogoTransparency = value;
|
2026-02-04 19:48:03 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-15 01:03:26 +01:00
|
|
|
|
// Image library selection (UI radio buttons bind to the boolean helpers)
|
2026-02-26 19:17:23 +01:00
|
|
|
|
private string _imageLibrary = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "System.Graphics" : "ImageSharp";
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Whether the application is running on Windows. Used by cross-platform UIs to show/hide Windows-only options.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public bool IsRunningOnWindows => RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
|
2026-02-15 01:03:26 +01:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// The selected image processing library. Possible values: "System.Graphics" or "ImageSharp".
|
|
|
|
|
|
/// This value is mirrored into PicSettings.ImageCreatorProvider so the runtime mapper picks the implementation.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public string ImageLibrary
|
|
|
|
|
|
{
|
|
|
|
|
|
get => _imageLibrary;
|
|
|
|
|
|
set
|
|
|
|
|
|
{
|
|
|
|
|
|
if (_imageLibrary == value) return;
|
|
|
|
|
|
_imageLibrary = value;
|
|
|
|
|
|
// Reflect selection into PicSettings so mapper can resolve at runtime
|
|
|
|
|
|
_picSettings.ImageCreatorProvider = string.Equals(value, "ImageSharp", StringComparison.OrdinalIgnoreCase)
|
|
|
|
|
|
? "ALTERNATE"
|
|
|
|
|
|
: "Sharp";
|
|
|
|
|
|
NotifyPropertyChanged();
|
|
|
|
|
|
NotifyPropertyChanged(nameof(UseSystemGraphics));
|
|
|
|
|
|
NotifyPropertyChanged(nameof(UseImageSharp));
|
2026-02-26 19:17:23 +01:00
|
|
|
|
NotifyPropertyChanged(nameof(IsRunningOnWindows));
|
2026-02-15 01:03:26 +01:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public bool UseSystemGraphics
|
|
|
|
|
|
{
|
|
|
|
|
|
get => string.Equals(ImageLibrary, "System.Graphics", StringComparison.OrdinalIgnoreCase);
|
|
|
|
|
|
set
|
|
|
|
|
|
{
|
|
|
|
|
|
if (value) ImageLibrary = "System.Graphics";
|
|
|
|
|
|
NotifyPropertyChanged();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public bool UseImageSharp
|
|
|
|
|
|
{
|
|
|
|
|
|
get => string.Equals(ImageLibrary, "ImageSharp", StringComparison.OrdinalIgnoreCase);
|
|
|
|
|
|
set
|
|
|
|
|
|
{
|
|
|
|
|
|
if (value) ImageLibrary = "ImageSharp";
|
|
|
|
|
|
NotifyPropertyChanged();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-04 19:48:03 +01:00
|
|
|
|
// Folder division settings
|
|
|
|
|
|
private int _filesPerFolder = 99;
|
|
|
|
|
|
public int FilesPerFolder
|
|
|
|
|
|
{
|
|
|
|
|
|
get => _filesPerFolder;
|
|
|
|
|
|
set
|
|
|
|
|
|
{
|
|
|
|
|
|
_filesPerFolder = value;
|
|
|
|
|
|
NotifyPropertyChanged();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private string _folderSuffix = "";
|
|
|
|
|
|
public string FolderSuffix
|
|
|
|
|
|
{
|
|
|
|
|
|
get => _folderSuffix;
|
|
|
|
|
|
set
|
|
|
|
|
|
{
|
|
|
|
|
|
_folderSuffix = value;
|
|
|
|
|
|
NotifyPropertyChanged();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private int _counterDigits = 2;
|
|
|
|
|
|
public int CounterDigits
|
|
|
|
|
|
{
|
|
|
|
|
|
get => _counterDigits;
|
|
|
|
|
|
set
|
|
|
|
|
|
{
|
|
|
|
|
|
_counterDigits = value;
|
|
|
|
|
|
NotifyPropertyChanged();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Vertical text settings
|
|
|
|
|
|
public int VerticalTextSize
|
|
|
|
|
|
{
|
2026-03-12 18:48:13 +01:00
|
|
|
|
get => _visual.VerticalTextSize;
|
|
|
|
|
|
set => _visual.VerticalTextSize = value;
|
2026-02-04 19:48:03 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public int VerticalTextMargin
|
|
|
|
|
|
{
|
2026-03-12 18:48:13 +01:00
|
|
|
|
get => _visual.VerticalTextMargin;
|
|
|
|
|
|
set => _visual.VerticalTextMargin = value;
|
2026-02-04 19:48:03 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// JPEG compression settings
|
|
|
|
|
|
public int JpegQuality
|
|
|
|
|
|
{
|
2026-03-12 18:48:13 +01:00
|
|
|
|
get => _visual.JpegQuality;
|
|
|
|
|
|
set => _visual.JpegQuality = value;
|
2026-02-04 19:48:03 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public int JpegQualityThumbnail
|
|
|
|
|
|
{
|
2026-03-12 18:48:13 +01:00
|
|
|
|
get => _visual.JpegQualityThumbnail;
|
|
|
|
|
|
set => _visual.JpegQualityThumbnail = value;
|
2026-02-04 19:48:03 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// CheckBox settings
|
|
|
|
|
|
private bool _createThumbnails = true;
|
|
|
|
|
|
public bool CreateThumbnails
|
|
|
|
|
|
{
|
|
|
|
|
|
get => _createThumbnails;
|
|
|
|
|
|
set
|
|
|
|
|
|
{
|
|
|
|
|
|
_createThumbnails = value;
|
|
|
|
|
|
NotifyPropertyChanged();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private bool _automaticRotation;
|
|
|
|
|
|
public bool AutomaticRotation
|
|
|
|
|
|
{
|
|
|
|
|
|
get => _automaticRotation;
|
|
|
|
|
|
set
|
|
|
|
|
|
{
|
|
|
|
|
|
_automaticRotation = value;
|
|
|
|
|
|
NotifyPropertyChanged();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private bool _forceJpeg;
|
|
|
|
|
|
public bool ForceJpeg
|
|
|
|
|
|
{
|
|
|
|
|
|
get => _forceJpeg;
|
|
|
|
|
|
set
|
|
|
|
|
|
{
|
|
|
|
|
|
_forceJpeg = value;
|
|
|
|
|
|
NotifyPropertyChanged();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private bool _updateSubdirectories;
|
|
|
|
|
|
public bool UpdateSubdirectories
|
|
|
|
|
|
{
|
|
|
|
|
|
get => _updateSubdirectories;
|
|
|
|
|
|
set
|
|
|
|
|
|
{
|
|
|
|
|
|
_updateSubdirectories = value;
|
|
|
|
|
|
NotifyPropertyChanged();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private bool _createSubfolders;
|
|
|
|
|
|
public bool CreateSubfolders
|
|
|
|
|
|
{
|
|
|
|
|
|
get => _createSubfolders;
|
|
|
|
|
|
set
|
|
|
|
|
|
{
|
|
|
|
|
|
_createSubfolders = value;
|
|
|
|
|
|
NotifyPropertyChanged();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private bool _addTime;
|
|
|
|
|
|
public bool AddTime
|
|
|
|
|
|
{
|
|
|
|
|
|
get => _addTime;
|
|
|
|
|
|
set
|
|
|
|
|
|
{
|
|
|
|
|
|
_addTime = value;
|
|
|
|
|
|
NotifyPropertyChanged();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private bool _addRaceTime;
|
|
|
|
|
|
public bool AddRaceTime
|
|
|
|
|
|
{
|
|
|
|
|
|
get => _addRaceTime;
|
|
|
|
|
|
set
|
|
|
|
|
|
{
|
|
|
|
|
|
_addRaceTime = value;
|
|
|
|
|
|
NotifyPropertyChanged();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public bool AddLogo
|
|
|
|
|
|
{
|
2026-03-12 18:48:13 +01:00
|
|
|
|
get => _visual.AddLogo;
|
|
|
|
|
|
set => _visual.AddLogo = value;
|
2026-02-04 19:48:03 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public bool KeepOriginalDimensions
|
|
|
|
|
|
{
|
2026-03-12 18:48:13 +01:00
|
|
|
|
get => _visual.KeepOriginalDimensions;
|
|
|
|
|
|
set => _visual.KeepOriginalDimensions = value;
|
2026-02-04 19:48:03 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private bool _showDate;
|
|
|
|
|
|
public bool ShowDate
|
|
|
|
|
|
{
|
|
|
|
|
|
get => _showDate;
|
|
|
|
|
|
set
|
|
|
|
|
|
{
|
|
|
|
|
|
_showDate = value;
|
|
|
|
|
|
NotifyPropertyChanged();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private bool _showPhotoNumber;
|
|
|
|
|
|
public bool ShowPhotoNumber
|
|
|
|
|
|
{
|
|
|
|
|
|
get => _showPhotoNumber;
|
|
|
|
|
|
set
|
|
|
|
|
|
{
|
2026-02-16 18:58:15 +01:00
|
|
|
|
if (_showPhotoNumber == value) return;
|
2026-02-04 19:48:03 +01:00
|
|
|
|
_showPhotoNumber = value;
|
2026-02-16 18:58:15 +01:00
|
|
|
|
if (value)
|
|
|
|
|
|
{
|
|
|
|
|
|
// ensure mutually exclusive choices
|
|
|
|
|
|
_addTimeToThumbnails = false;
|
|
|
|
|
|
_addTextToThumbnails = false;
|
|
|
|
|
|
_addNumberAndTimeToThumbnails = false;
|
|
|
|
|
|
_addRaceTimeToThumbnails = false;
|
|
|
|
|
|
NotifyPropertyChanged(nameof(AddTimeToThumbnails));
|
|
|
|
|
|
NotifyPropertyChanged(nameof(AddTextToThumbnails));
|
|
|
|
|
|
NotifyPropertyChanged(nameof(AddNumberAndTimeToThumbnails));
|
|
|
|
|
|
NotifyPropertyChanged(nameof(AddRaceTimeToThumbnails));
|
|
|
|
|
|
}
|
2026-02-04 19:48:03 +01:00
|
|
|
|
NotifyPropertyChanged();
|
2026-02-16 18:58:15 +01:00
|
|
|
|
NotifyPropertyChanged(nameof(ThumbnailMode));
|
2026-02-04 19:48:03 +01:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private bool _shutdownSystem;
|
|
|
|
|
|
public bool ShutdownSystem
|
|
|
|
|
|
{
|
|
|
|
|
|
get => _shutdownSystem;
|
|
|
|
|
|
set
|
|
|
|
|
|
{
|
|
|
|
|
|
_shutdownSystem = value;
|
|
|
|
|
|
NotifyPropertyChanged();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ComboBox position/alignment settings
|
|
|
|
|
|
private string _verticalPosition = "Basso";
|
|
|
|
|
|
public string VerticalPosition
|
|
|
|
|
|
{
|
|
|
|
|
|
get => _verticalPosition;
|
|
|
|
|
|
set
|
|
|
|
|
|
{
|
|
|
|
|
|
_verticalPosition = value;
|
|
|
|
|
|
NotifyPropertyChanged();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private string _horizontalAlignment = "Centro";
|
|
|
|
|
|
public string HorizontalAlignment
|
|
|
|
|
|
{
|
|
|
|
|
|
get => _horizontalAlignment;
|
|
|
|
|
|
set
|
|
|
|
|
|
{
|
|
|
|
|
|
_horizontalAlignment = value;
|
|
|
|
|
|
NotifyPropertyChanged();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private string _logoHorizontalPosition = "Destra";
|
|
|
|
|
|
public string LogoHorizontalPosition
|
|
|
|
|
|
{
|
|
|
|
|
|
get => _logoHorizontalPosition;
|
|
|
|
|
|
set
|
|
|
|
|
|
{
|
|
|
|
|
|
_logoHorizontalPosition = value;
|
|
|
|
|
|
NotifyPropertyChanged();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private string _logoVerticalPosition = "Basso";
|
|
|
|
|
|
public string LogoVerticalPosition
|
|
|
|
|
|
{
|
|
|
|
|
|
get => _logoVerticalPosition;
|
|
|
|
|
|
set
|
|
|
|
|
|
{
|
|
|
|
|
|
_logoVerticalPosition = value;
|
|
|
|
|
|
NotifyPropertyChanged();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// RadioButton settings
|
|
|
|
|
|
private bool _useProgressiveNumbering = true;
|
|
|
|
|
|
public bool UseProgressiveNumbering
|
|
|
|
|
|
{
|
|
|
|
|
|
get => _useProgressiveNumbering;
|
|
|
|
|
|
set
|
|
|
|
|
|
{
|
|
|
|
|
|
_useProgressiveNumbering = value;
|
|
|
|
|
|
NotifyPropertyChanged();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private bool _useFileNumbering;
|
|
|
|
|
|
public bool UseFileNumbering
|
|
|
|
|
|
{
|
|
|
|
|
|
get => _useFileNumbering;
|
|
|
|
|
|
set
|
|
|
|
|
|
{
|
|
|
|
|
|
_useFileNumbering = value;
|
|
|
|
|
|
NotifyPropertyChanged();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private bool _useParallelProcessing = true;
|
|
|
|
|
|
public bool UseParallelProcessing
|
|
|
|
|
|
{
|
|
|
|
|
|
get => _useParallelProcessing;
|
|
|
|
|
|
set
|
|
|
|
|
|
{
|
|
|
|
|
|
_useParallelProcessing = value;
|
|
|
|
|
|
NotifyPropertyChanged();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private bool _useSequentialProcessing;
|
|
|
|
|
|
public bool UseSequentialProcessing
|
|
|
|
|
|
{
|
|
|
|
|
|
get => _useSequentialProcessing;
|
|
|
|
|
|
set
|
|
|
|
|
|
{
|
|
|
|
|
|
_useSequentialProcessing = value;
|
|
|
|
|
|
NotifyPropertyChanged();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Additional settings that were missing
|
|
|
|
|
|
private bool _addTimeToThumbnails;
|
|
|
|
|
|
public bool AddTimeToThumbnails
|
|
|
|
|
|
{
|
2026-02-16 19:55:37 +01:00
|
|
|
|
get => _thumbnailOption == ThumbnailOptionEnum.Time;
|
2026-02-04 19:48:03 +01:00
|
|
|
|
set
|
|
|
|
|
|
{
|
2026-02-16 18:58:15 +01:00
|
|
|
|
if (value)
|
|
|
|
|
|
{
|
2026-02-16 19:55:37 +01:00
|
|
|
|
ThumbnailOption = ThumbnailOptionEnum.Time;
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (_thumbnailOption == ThumbnailOptionEnum.Time)
|
|
|
|
|
|
{
|
|
|
|
|
|
ThumbnailOption = ThumbnailOptionEnum.None;
|
2026-02-16 18:58:15 +01:00
|
|
|
|
}
|
2026-02-04 19:48:03 +01:00
|
|
|
|
NotifyPropertyChanged();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private bool _showFileNameOnThumbnails;
|
|
|
|
|
|
public bool ShowFileNameOnThumbnails
|
|
|
|
|
|
{
|
2026-02-16 19:55:37 +01:00
|
|
|
|
get => _thumbnailOption == ThumbnailOptionEnum.FileName;
|
2026-02-04 19:48:03 +01:00
|
|
|
|
set
|
|
|
|
|
|
{
|
2026-02-16 19:55:37 +01:00
|
|
|
|
if (value)
|
|
|
|
|
|
ThumbnailOption = ThumbnailOptionEnum.FileName;
|
|
|
|
|
|
else if (_thumbnailOption == ThumbnailOptionEnum.FileName)
|
|
|
|
|
|
ThumbnailOption = ThumbnailOptionEnum.None;
|
2026-02-04 19:48:03 +01:00
|
|
|
|
NotifyPropertyChanged();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private DateTime _raceStartDate = DateTime.Now;
|
|
|
|
|
|
public DateTime RaceStartDate
|
|
|
|
|
|
{
|
|
|
|
|
|
get => _raceStartDate;
|
|
|
|
|
|
set
|
|
|
|
|
|
{
|
|
|
|
|
|
_raceStartDate = value;
|
|
|
|
|
|
NotifyPropertyChanged();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private string _timeLabel = "";
|
|
|
|
|
|
public string TimeLabel
|
|
|
|
|
|
{
|
|
|
|
|
|
get => _timeLabel;
|
|
|
|
|
|
set
|
|
|
|
|
|
{
|
|
|
|
|
|
_timeLabel = value;
|
|
|
|
|
|
NotifyPropertyChanged();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-04 22:10:16 +01:00
|
|
|
|
private string _bigPhotoSuffix = "";
|
|
|
|
|
|
public string BigPhotoSuffix
|
|
|
|
|
|
{
|
|
|
|
|
|
get => _bigPhotoSuffix;
|
|
|
|
|
|
set
|
|
|
|
|
|
{
|
|
|
|
|
|
_bigPhotoSuffix = value;
|
|
|
|
|
|
NotifyPropertyChanged();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private bool _addTextToThumbnails;
|
|
|
|
|
|
public bool AddTextToThumbnails
|
|
|
|
|
|
{
|
2026-02-16 19:55:37 +01:00
|
|
|
|
get => _thumbnailOption == ThumbnailOptionEnum.Text;
|
2026-02-04 22:10:16 +01:00
|
|
|
|
set
|
|
|
|
|
|
{
|
2026-02-16 18:58:15 +01:00
|
|
|
|
if (value)
|
2026-02-16 19:55:37 +01:00
|
|
|
|
ThumbnailOption = ThumbnailOptionEnum.Text;
|
|
|
|
|
|
else if (_thumbnailOption == ThumbnailOptionEnum.Text)
|
|
|
|
|
|
ThumbnailOption = ThumbnailOptionEnum.None;
|
2026-02-04 22:10:16 +01:00
|
|
|
|
NotifyPropertyChanged();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private bool _addRaceTimeToThumbnails;
|
|
|
|
|
|
public bool AddRaceTimeToThumbnails
|
|
|
|
|
|
{
|
2026-02-16 19:55:37 +01:00
|
|
|
|
get => _thumbnailOption == ThumbnailOptionEnum.RaceTime;
|
2026-02-04 22:10:16 +01:00
|
|
|
|
set
|
|
|
|
|
|
{
|
2026-02-16 18:58:15 +01:00
|
|
|
|
if (value)
|
2026-02-16 19:55:37 +01:00
|
|
|
|
ThumbnailOption = ThumbnailOptionEnum.RaceTime;
|
|
|
|
|
|
else if (_thumbnailOption == ThumbnailOptionEnum.RaceTime)
|
|
|
|
|
|
ThumbnailOption = ThumbnailOptionEnum.None;
|
2026-02-04 22:10:16 +01:00
|
|
|
|
NotifyPropertyChanged();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private bool _addNumberAndTimeToThumbnails;
|
|
|
|
|
|
public bool AddNumberAndTimeToThumbnails
|
|
|
|
|
|
{
|
2026-02-16 19:55:37 +01:00
|
|
|
|
get => _thumbnailOption == ThumbnailOptionEnum.FileNameAndTime;
|
2026-02-04 22:10:16 +01:00
|
|
|
|
set
|
|
|
|
|
|
{
|
2026-02-16 18:58:15 +01:00
|
|
|
|
if (value)
|
2026-02-16 19:55:37 +01:00
|
|
|
|
ThumbnailOption = ThumbnailOptionEnum.FileNameAndTime;
|
|
|
|
|
|
else if (_thumbnailOption == ThumbnailOptionEnum.FileNameAndTime)
|
|
|
|
|
|
ThumbnailOption = ThumbnailOptionEnum.None;
|
|
|
|
|
|
NotifyPropertyChanged();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// New enum and authoritative property for thumbnail selection
|
|
|
|
|
|
public enum ThumbnailOptionEnum
|
|
|
|
|
|
{
|
|
|
|
|
|
None = 0,
|
|
|
|
|
|
Text = 1,
|
|
|
|
|
|
FileName = 2,
|
|
|
|
|
|
Time = 3,
|
|
|
|
|
|
FileNameAndTime = 4,
|
|
|
|
|
|
RaceTime = 5
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private ThumbnailOptionEnum _thumbnailOption = ThumbnailOptionEnum.None;
|
|
|
|
|
|
|
|
|
|
|
|
// Name matches DTO property so SettingsService mapping works
|
|
|
|
|
|
public ThumbnailOptionEnum ThumbnailOption
|
|
|
|
|
|
{
|
|
|
|
|
|
get => _thumbnailOption;
|
|
|
|
|
|
set
|
|
|
|
|
|
{
|
|
|
|
|
|
if (_thumbnailOption == value) return;
|
|
|
|
|
|
_thumbnailOption = value;
|
|
|
|
|
|
// Notify all dependent properties so UI updates
|
|
|
|
|
|
NotifyPropertyChanged();
|
|
|
|
|
|
NotifyPropertyChanged(nameof(AddTextToThumbnails));
|
|
|
|
|
|
NotifyPropertyChanged(nameof(AddTimeToThumbnails));
|
|
|
|
|
|
NotifyPropertyChanged(nameof(ShowPhotoNumber));
|
|
|
|
|
|
NotifyPropertyChanged(nameof(AddNumberAndTimeToThumbnails));
|
|
|
|
|
|
NotifyPropertyChanged(nameof(AddRaceTimeToThumbnails));
|
|
|
|
|
|
NotifyPropertyChanged(nameof(ShowFileNameOnThumbnails));
|
|
|
|
|
|
NotifyPropertyChanged(nameof(ThumbnailMode));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Helper int property to bind ComboBox SelectedIndex in the WinForms designer
|
|
|
|
|
|
public int ThumbnailOptionIndex
|
|
|
|
|
|
{
|
|
|
|
|
|
get => (int)ThumbnailOption;
|
|
|
|
|
|
set
|
|
|
|
|
|
{
|
|
|
|
|
|
var opt = (ThumbnailOptionEnum)value;
|
|
|
|
|
|
if (opt == ThumbnailOption) return;
|
|
|
|
|
|
ThumbnailOption = opt;
|
2026-02-04 22:10:16 +01:00
|
|
|
|
NotifyPropertyChanged();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-16 18:58:15 +01:00
|
|
|
|
// Single authoritative thumbnail mode string to avoid conflicting bindings
|
|
|
|
|
|
// Possible values: "None", "Text", "Time", "Number", "NumberAndTime", "RaceTime"
|
|
|
|
|
|
public string ThumbnailMode
|
|
|
|
|
|
{
|
|
|
|
|
|
get
|
|
|
|
|
|
{
|
2026-02-16 19:55:37 +01:00
|
|
|
|
return _thumbnailOption switch
|
|
|
|
|
|
{
|
|
|
|
|
|
ThumbnailOptionEnum.Text => "Text",
|
|
|
|
|
|
ThumbnailOptionEnum.Time => "Time",
|
|
|
|
|
|
ThumbnailOptionEnum.FileName => "Number",
|
|
|
|
|
|
ThumbnailOptionEnum.FileNameAndTime => "NumberAndTime",
|
|
|
|
|
|
ThumbnailOptionEnum.RaceTime => "RaceTime",
|
|
|
|
|
|
_ => "None",
|
|
|
|
|
|
};
|
2026-02-16 18:58:15 +01:00
|
|
|
|
}
|
|
|
|
|
|
set
|
|
|
|
|
|
{
|
2026-02-16 19:55:37 +01:00
|
|
|
|
// Map incoming string to enum and set the authoritative property
|
2026-02-16 18:58:15 +01:00
|
|
|
|
switch ((value ?? string.Empty).ToLowerInvariant())
|
|
|
|
|
|
{
|
|
|
|
|
|
case "text":
|
2026-02-16 19:55:37 +01:00
|
|
|
|
ThumbnailOption = ThumbnailOptionEnum.Text;
|
2026-02-16 18:58:15 +01:00
|
|
|
|
break;
|
|
|
|
|
|
case "time":
|
2026-02-16 19:55:37 +01:00
|
|
|
|
ThumbnailOption = ThumbnailOptionEnum.Time;
|
2026-02-16 18:58:15 +01:00
|
|
|
|
break;
|
|
|
|
|
|
case "number":
|
2026-02-16 19:55:37 +01:00
|
|
|
|
ThumbnailOption = ThumbnailOptionEnum.FileName;
|
2026-02-16 18:58:15 +01:00
|
|
|
|
break;
|
|
|
|
|
|
case "numberandtime":
|
2026-02-16 19:55:37 +01:00
|
|
|
|
ThumbnailOption = ThumbnailOptionEnum.FileNameAndTime;
|
2026-02-16 18:58:15 +01:00
|
|
|
|
break;
|
|
|
|
|
|
case "racetime":
|
2026-02-16 19:55:37 +01:00
|
|
|
|
ThumbnailOption = ThumbnailOptionEnum.RaceTime;
|
2026-02-16 18:58:15 +01:00
|
|
|
|
break;
|
|
|
|
|
|
default:
|
2026-02-16 19:55:37 +01:00
|
|
|
|
ThumbnailOption = ThumbnailOptionEnum.None;
|
2026-02-16 18:58:15 +01:00
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-04 23:16:06 +01:00
|
|
|
|
// Image processing progress and status
|
|
|
|
|
|
public string ProcessingStatus
|
|
|
|
|
|
{
|
2026-03-12 18:48:13 +01:00
|
|
|
|
get => _processing.ProcessingStatus;
|
|
|
|
|
|
set => _processing.ProcessingStatus = value;
|
2026-02-04 23:16:06 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public int ProcessedImagesCount
|
|
|
|
|
|
{
|
2026-03-12 18:48:13 +01:00
|
|
|
|
get => _processing.ProcessedImagesCount;
|
|
|
|
|
|
set => _processing.ProcessedImagesCount = value;
|
2026-02-04 23:16:06 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public int TotalImagesCount
|
|
|
|
|
|
{
|
2026-03-12 18:48:13 +01:00
|
|
|
|
get => _processing.TotalImagesCount;
|
|
|
|
|
|
set => _processing.TotalImagesCount = value;
|
2026-02-04 23:16:06 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public int ProgressBarValue
|
|
|
|
|
|
{
|
2026-03-12 18:48:13 +01:00
|
|
|
|
get => _processing.ProgressBarValue;
|
|
|
|
|
|
set => _processing.ProgressBarValue = value;
|
2026-02-04 23:16:06 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public int ProgressBarMaximum
|
|
|
|
|
|
{
|
2026-03-12 18:48:13 +01:00
|
|
|
|
get => _processing.ProgressBarMaximum;
|
|
|
|
|
|
set => _processing.ProgressBarMaximum = value;
|
2026-02-04 23:16:06 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-10-14 23:25:35 +02:00
|
|
|
|
private void Test(object parameter)
|
|
|
|
|
|
{
|
|
|
|
|
|
Debug.WriteLine("Yep");
|
2024-10-14 23:48:21 +02:00
|
|
|
|
this.UiEnabled = !this.UiEnabled;
|
2024-10-14 23:25:35 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async Task TestAsync()
|
|
|
|
|
|
{
|
|
|
|
|
|
Debug.WriteLine("Yep c");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-09 17:27:05 +02:00
|
|
|
|
public async Task ProcessImages()
|
2025-07-23 17:16:06 +02:00
|
|
|
|
{
|
2026-02-04 23:16:06 +01:00
|
|
|
|
_logger.LogInformation("Avvio elaborazione...");
|
|
|
|
|
|
UiEnabled = false;
|
|
|
|
|
|
|
|
|
|
|
|
MainToken?.Dispose();
|
|
|
|
|
|
MainToken = new CancellationTokenSource();
|
|
|
|
|
|
var token = MainToken.Token;
|
|
|
|
|
|
|
2026-03-12 18:48:13 +01:00
|
|
|
|
// Normalize paths
|
|
|
|
|
|
_paths.NormalizePaths();
|
2026-02-28 21:34:45 +01:00
|
|
|
|
|
2026-02-04 23:16:06 +01:00
|
|
|
|
// Reset counters
|
2026-03-12 18:48:13 +01:00
|
|
|
|
_processing.ResetForRun();
|
2026-02-28 21:34:45 +01:00
|
|
|
|
|
2026-02-04 23:16:06 +01:00
|
|
|
|
// Update PicSettings from DataModel using AutoMapper
|
|
|
|
|
|
_mapper.Map(this, _picSettings);
|
2026-02-16 18:58:15 +01:00
|
|
|
|
// Explicitly ensure thumbnail-related flags are applied to PicSettings
|
|
|
|
|
|
// because AutoMapper may not map differently-named properties.
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
_picSettings.AggiungiScritteMiniature = this.AddTextToThumbnails;
|
|
|
|
|
|
_picSettings.UsaOrarioMiniatura = this.AddTimeToThumbnails;
|
|
|
|
|
|
_picSettings.AggNumTempMin = this.AddNumberAndTimeToThumbnails;
|
|
|
|
|
|
_picSettings.AggTempoGaraMin = this.AddRaceTimeToThumbnails;
|
|
|
|
|
|
_picSettings.CreaMiniature = this.CreateThumbnails;
|
|
|
|
|
|
_picSettings.LarghezzaSmall = this.ThumbnailWidth;
|
|
|
|
|
|
_picSettings.AltezzaSmall = this.ThumbnailHeight;
|
|
|
|
|
|
_picSettings.DimMin = this.FontSizeThumbnail;
|
|
|
|
|
|
_picSettings.JpegQualityMin = this.JpegQualityThumbnail;
|
|
|
|
|
|
}
|
|
|
|
|
|
catch
|
|
|
|
|
|
{
|
|
|
|
|
|
// Best-effort; do not fail processing on mapping issues
|
|
|
|
|
|
}
|
2026-02-28 21:34:45 +01:00
|
|
|
|
|
2026-02-21 15:53:52 +01:00
|
|
|
|
var imageCreationOptions = new ImageCreationService.Options
|
2026-02-04 23:16:06 +01:00
|
|
|
|
{
|
|
|
|
|
|
AggiornaSottodirectory = UpdateSubdirectories,
|
|
|
|
|
|
CreaSottocartelle = CreateSubfolders,
|
|
|
|
|
|
FilePerCartella = FilesPerFolder,
|
|
|
|
|
|
SuffissoCartelle = FolderSuffix,
|
|
|
|
|
|
CifreContatore = CounterDigits,
|
|
|
|
|
|
NumerazioneType = UseProgressiveNumbering ? NumerazioneType.Progressiva : NumerazioneType.Files,
|
|
|
|
|
|
SourcePath = SourcePath,
|
|
|
|
|
|
DestinationPath = DestinationPath,
|
|
|
|
|
|
MaxThreads = ThreadsCount,
|
|
|
|
|
|
ChunksSize = ChunkSize,
|
|
|
|
|
|
LinearExecution = UseSequentialProcessing
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
2026-03-12 18:48:13 +01:00
|
|
|
|
var runResult = await _imageProcessingCoordinator.RunAsync(
|
|
|
|
|
|
new ImageProcessingRunRequest { Options = imageCreationOptions },
|
|
|
|
|
|
token,
|
|
|
|
|
|
update =>
|
|
|
|
|
|
{
|
|
|
|
|
|
ProcessedImagesCount = update.Processed;
|
|
|
|
|
|
TotalImagesCount = update.Total;
|
|
|
|
|
|
ProgressBarMaximum = update.Total;
|
|
|
|
|
|
ProgressBarValue = update.Processed;
|
|
|
|
|
|
ProcessingStatus = update.Status;
|
|
|
|
|
|
},
|
|
|
|
|
|
speed => { SpeedCounter = speed; }).ConfigureAwait(false);
|
2026-02-16 18:32:04 +01:00
|
|
|
|
|
2026-03-12 18:48:13 +01:00
|
|
|
|
SpeedCounter = runResult.FinalSpeedCounter;
|
2026-02-04 23:16:06 +01:00
|
|
|
|
}
|
|
|
|
|
|
catch (OperationCanceledException)
|
|
|
|
|
|
{
|
|
|
|
|
|
_logger.LogInformation("Operazione Cancellata");
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
_logger.LogError(ex, "Errore durante l'elaborazione delle immagini");
|
|
|
|
|
|
ProcessingStatus = $"Errore: {ex.Message}";
|
|
|
|
|
|
}
|
|
|
|
|
|
finally
|
|
|
|
|
|
{
|
|
|
|
|
|
MainToken?.Dispose();
|
|
|
|
|
|
MainToken = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ProcessingStatus = "Finito";
|
|
|
|
|
|
UiEnabled = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-09 17:27:05 +02:00
|
|
|
|
public async Task RunNumberAiAsync(CancellationToken token)
|
|
|
|
|
|
{
|
|
|
|
|
|
await RunAiExtractionCoreAsync(token, useDestination: true, recursive: true, failOnInvalidPath: true).ConfigureAwait(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public async Task RunFaceAiAsync(CancellationToken token)
|
|
|
|
|
|
{
|
|
|
|
|
|
using var registration = token.Register(() => _ = StopFaceEncoderAsync("Arresto face encoder richiesto dalla CLI.", waitForExit: true));
|
|
|
|
|
|
await RunFaceEncoderAsync().ConfigureAwait(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-29 11:07:49 +02:00
|
|
|
|
private async Task CancelOperation()
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
2026-02-15 11:14:19 +01:00
|
|
|
|
var tokenSource = MainToken;
|
|
|
|
|
|
if (tokenSource is not null)
|
|
|
|
|
|
{
|
|
|
|
|
|
// Cancel synchronously and return to caller. Some CTSource implementations
|
|
|
|
|
|
// may provide async helpers but cancelling is immediate.
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
tokenSource.Cancel();
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
_logger.LogWarning(ex, "Exception while cancelling token");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-07-29 11:07:49 +02:00
|
|
|
|
|
|
|
|
|
|
UiEnabled = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception e)
|
|
|
|
|
|
{
|
2026-02-15 11:14:19 +01:00
|
|
|
|
_logger.LogError(e, "Error canceling the token");
|
2025-07-29 11:07:49 +02:00
|
|
|
|
_logger.LogInformation("Ignora questo errore");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-04 19:48:03 +01:00
|
|
|
|
|
2026-05-09 12:09:05 +02:00
|
|
|
|
public async Task StopFaceEncoderAsync(string reason, bool waitForExit = true)
|
|
|
|
|
|
{
|
|
|
|
|
|
var trackedProcess = GetTrackedFaceEncoderProcess();
|
|
|
|
|
|
var ownsProcess = false;
|
|
|
|
|
|
var process = trackedProcess;
|
|
|
|
|
|
|
|
|
|
|
|
if (process is null)
|
|
|
|
|
|
{
|
|
|
|
|
|
process = FindConfiguredFaceEncoderProcess();
|
|
|
|
|
|
ownsProcess = process is not null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (process is null)
|
|
|
|
|
|
{
|
|
|
|
|
|
await InvokeOnUiThreadAsync(() =>
|
|
|
|
|
|
{
|
|
|
|
|
|
IsFaceEncoderRunning = false;
|
|
|
|
|
|
FaceStatusMessage = "Face encoder non in esecuzione.";
|
|
|
|
|
|
}).ConfigureAwait(false);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
await InvokeOnUiThreadAsync(() => FaceStatusMessage = reason).ConfigureAwait(false);
|
|
|
|
|
|
|
|
|
|
|
|
var gracefulStopRequested = TryRequestFaceEncoderStop(process);
|
|
|
|
|
|
if (waitForExit)
|
|
|
|
|
|
{
|
|
|
|
|
|
var exited = await WaitForProcessExitAsync(process, TimeSpan.FromSeconds(5)).ConfigureAwait(false);
|
|
|
|
|
|
if (!exited)
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
process.Kill(entireProcessTree: true);
|
|
|
|
|
|
exited = await WaitForProcessExitAsync(process, TimeSpan.FromSeconds(5)).ConfigureAwait(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
_logger.LogWarning(ex, "Unable to terminate face encoder process {ProcessId}", process.Id);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await InvokeOnUiThreadAsync(() =>
|
|
|
|
|
|
{
|
|
|
|
|
|
IsFaceEncoderRunning = !exited && IsProcessAlive(process);
|
|
|
|
|
|
FaceStatusMessage = exited
|
|
|
|
|
|
? "Face encoder arrestato."
|
|
|
|
|
|
: gracefulStopRequested
|
|
|
|
|
|
? "Segnale di arresto inviato al face encoder."
|
|
|
|
|
|
: "Arresto forzato del face encoder richiesto.";
|
|
|
|
|
|
}).ConfigureAwait(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
finally
|
|
|
|
|
|
{
|
|
|
|
|
|
if (ownsProcess)
|
|
|
|
|
|
{
|
|
|
|
|
|
process.Dispose();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private bool CanRunFaceEncoder()
|
|
|
|
|
|
{
|
|
|
|
|
|
return !IsFaceEncoderRunning;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private bool CanStopFaceEncoder()
|
|
|
|
|
|
{
|
|
|
|
|
|
return IsFaceEncoderRunning;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void UpdateFaceEncoderCommandStates()
|
|
|
|
|
|
{
|
|
|
|
|
|
_startFaceEncoderCommand?.RaiseCanExecuteChanged();
|
|
|
|
|
|
_stopFaceEncoderCommand?.RaiseCanExecuteChanged();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async Task RunFaceEncoderAsync()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (IsFaceEncoderRunning)
|
|
|
|
|
|
{
|
|
|
|
|
|
FaceStatusMessage = "Face encoder gia in esecuzione.";
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-09 15:46:41 +02:00
|
|
|
|
var executableRootPath = NormalizeFilePathArgument(FaceExecutablePath);
|
|
|
|
|
|
var outputFolderPath = NormalizeDirectoryPathArgument(FaceOutputFolderPath);
|
2026-05-09 12:09:05 +02:00
|
|
|
|
var imagesFolder = NormalizeDirectoryPathArgument(DestinationPath);
|
2026-05-09 15:46:41 +02:00
|
|
|
|
var executablePath = ResolveConfiguredFaceEncoderExecutablePath(executableRootPath, UseFaceGpu);
|
2026-05-09 12:09:05 +02:00
|
|
|
|
|
2026-05-09 15:46:41 +02:00
|
|
|
|
if (string.IsNullOrWhiteSpace(executableRootPath))
|
2026-05-09 12:09:05 +02:00
|
|
|
|
{
|
2026-05-09 15:46:41 +02:00
|
|
|
|
FaceStatusMessage = "Percorso cartella face encoder non valido.";
|
2026-05-09 12:09:05 +02:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-09 15:46:41 +02:00
|
|
|
|
if (string.IsNullOrWhiteSpace(executablePath) || !File.Exists(executablePath))
|
2026-05-09 12:09:05 +02:00
|
|
|
|
{
|
2026-05-09 15:46:41 +02:00
|
|
|
|
FaceStatusMessage = UseFaceGpu
|
|
|
|
|
|
? "Impossibile trovare face_encoder_gpu.exe nella cartella selezionata."
|
|
|
|
|
|
: "Impossibile trovare face_encoder_cpu.exe nella cartella selezionata.";
|
2026-05-09 12:09:05 +02:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-09 15:46:41 +02:00
|
|
|
|
if (string.IsNullOrWhiteSpace(imagesFolder) || !Directory.Exists(imagesFolder))
|
2026-05-09 12:09:05 +02:00
|
|
|
|
{
|
2026-05-09 15:46:41 +02:00
|
|
|
|
FaceStatusMessage = "Cartella Destinazione non valida.";
|
2026-05-09 12:09:05 +02:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-09 15:46:41 +02:00
|
|
|
|
if (string.IsNullOrWhiteSpace(outputFolderPath))
|
2026-05-09 12:09:05 +02:00
|
|
|
|
{
|
2026-05-09 15:46:41 +02:00
|
|
|
|
FaceStatusMessage = "Inserisci la cartella di output per encodings e log.";
|
2026-05-09 12:09:05 +02:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
2026-05-09 15:46:41 +02:00
|
|
|
|
Directory.CreateDirectory(outputFolderPath);
|
2026-05-09 12:09:05 +02:00
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
2026-05-09 15:46:41 +02:00
|
|
|
|
_logger.LogError(ex, "Unable to create face output directory: {OutputFolderPath}", outputFolderPath);
|
|
|
|
|
|
FaceStatusMessage = "Impossibile creare la cartella di output.";
|
2026-05-09 12:09:05 +02:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-09 15:46:41 +02:00
|
|
|
|
var parallelism = NormalizeFaceParallelism(FaceParallelism);
|
|
|
|
|
|
var minSize = NormalizeFaceMinSize(FaceMinSize);
|
|
|
|
|
|
var outputFiles = BuildFaceEncoderOutputPaths(outputFolderPath, imagesFolder, DateTime.Now);
|
|
|
|
|
|
|
|
|
|
|
|
FaceExecutablePath = executableRootPath;
|
|
|
|
|
|
FaceOutputFolderPath = outputFolderPath;
|
2026-05-09 12:09:05 +02:00
|
|
|
|
FaceCommandOutput = string.Empty;
|
|
|
|
|
|
FaceStatusMessage = "Esecuzione face encoder in corso...";
|
|
|
|
|
|
|
2026-05-09 15:46:41 +02:00
|
|
|
|
var transcriptLines = new StringBuilder();
|
2026-05-09 12:09:05 +02:00
|
|
|
|
var outputLines = new StringBuilder();
|
|
|
|
|
|
var errorLines = new StringBuilder();
|
|
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
var processStartInfo = new ProcessStartInfo
|
|
|
|
|
|
{
|
|
|
|
|
|
FileName = executablePath,
|
|
|
|
|
|
WorkingDirectory = Path.GetDirectoryName(executablePath) ?? Environment.CurrentDirectory,
|
|
|
|
|
|
UseShellExecute = false,
|
|
|
|
|
|
RedirectStandardOutput = true,
|
|
|
|
|
|
RedirectStandardError = true,
|
2026-05-09 15:46:41 +02:00
|
|
|
|
RedirectStandardInput = false,
|
2026-05-09 12:09:05 +02:00
|
|
|
|
CreateNoWindow = false,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
processStartInfo.ArgumentList.Add("--images");
|
|
|
|
|
|
processStartInfo.ArgumentList.Add(imagesFolder);
|
|
|
|
|
|
processStartInfo.ArgumentList.Add("--out");
|
2026-05-09 15:46:41 +02:00
|
|
|
|
processStartInfo.ArgumentList.Add(outputFiles.OutputFilePath);
|
|
|
|
|
|
processStartInfo.ArgumentList.Add("--log");
|
|
|
|
|
|
processStartInfo.ArgumentList.Add(outputFiles.LogFilePath);
|
2026-05-09 12:09:05 +02:00
|
|
|
|
|
|
|
|
|
|
if (FaceRecursive)
|
|
|
|
|
|
{
|
|
|
|
|
|
processStartInfo.ArgumentList.Add("--recursive");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-09 15:46:41 +02:00
|
|
|
|
if (FaceIncludeThumbnails)
|
|
|
|
|
|
{
|
|
|
|
|
|
processStartInfo.ArgumentList.Add("--include-tn");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
processStartInfo.ArgumentList.Add(UseFaceGpu ? "--multiprocess" : "--multicore");
|
|
|
|
|
|
processStartInfo.ArgumentList.Add(parallelism.ToString());
|
|
|
|
|
|
processStartInfo.ArgumentList.Add("--min-size");
|
|
|
|
|
|
processStartInfo.ArgumentList.Add(minSize.ToString());
|
|
|
|
|
|
|
|
|
|
|
|
if (FaceUpsample)
|
|
|
|
|
|
{
|
|
|
|
|
|
processStartInfo.ArgumentList.Add("--upsample");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-09 12:09:05 +02:00
|
|
|
|
using var process = new Process { StartInfo = processStartInfo, EnableRaisingEvents = true };
|
2026-05-09 15:46:41 +02:00
|
|
|
|
process.OutputDataReceived += (_, args) => AppendFaceProcessOutput(outputLines, transcriptLines, args.Data, isError: false);
|
|
|
|
|
|
process.ErrorDataReceived += (_, args) => AppendFaceProcessOutput(errorLines, transcriptLines, args.Data, isError: true);
|
|
|
|
|
|
process.Exited += (_, _) =>
|
|
|
|
|
|
{
|
|
|
|
|
|
_ = InvokeOnUiThreadAsync(() =>
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!ComputeIsFaceEncoderRunning())
|
|
|
|
|
|
{
|
|
|
|
|
|
IsFaceEncoderRunning = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
2026-05-09 12:09:05 +02:00
|
|
|
|
|
|
|
|
|
|
if (!process.Start())
|
|
|
|
|
|
{
|
|
|
|
|
|
throw new InvalidOperationException("Avvio face encoder fallito.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
_hasStartedFaceEncoderInSession = true;
|
|
|
|
|
|
EnsureFaceEncoderWatcherStarted();
|
|
|
|
|
|
TrackFaceEncoderProcess(process);
|
|
|
|
|
|
await InvokeOnUiThreadAsync(() => IsFaceEncoderRunning = true).ConfigureAwait(false);
|
|
|
|
|
|
|
2026-05-09 15:46:41 +02:00
|
|
|
|
if (UseFaceGpu)
|
|
|
|
|
|
{
|
|
|
|
|
|
StartFaceEncoderLogWatcher(outputFiles.LogFilePath, outputLines, transcriptLines);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-09 12:09:05 +02:00
|
|
|
|
process.BeginOutputReadLine();
|
|
|
|
|
|
process.BeginErrorReadLine();
|
|
|
|
|
|
await process.WaitForExitAsync().ConfigureAwait(false);
|
|
|
|
|
|
|
2026-05-09 15:46:41 +02:00
|
|
|
|
var summary = BuildFaceEncoderSummary(process.ExitCode, processStartInfo, outputFiles.OutputFilePath, outputFiles.LogFilePath, outputLines, errorLines);
|
2026-05-09 12:09:05 +02:00
|
|
|
|
await InvokeOnUiThreadAsync(() =>
|
|
|
|
|
|
{
|
2026-05-09 15:46:41 +02:00
|
|
|
|
FaceCommandOutput = string.IsNullOrWhiteSpace(FaceCommandOutput)
|
|
|
|
|
|
? summary
|
|
|
|
|
|
: $"{FaceCommandOutput.TrimEnd()}\n\n{summary}";
|
2026-05-09 12:09:05 +02:00
|
|
|
|
FaceStatusMessage = process.ExitCode == 0
|
|
|
|
|
|
? "Face encoder completato."
|
|
|
|
|
|
: $"Face encoder terminato con errore (code {process.ExitCode}).";
|
|
|
|
|
|
}).ConfigureAwait(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
2026-05-09 15:46:41 +02:00
|
|
|
|
Console.Error.WriteLine(ex);
|
2026-05-09 12:09:05 +02:00
|
|
|
|
_logger.LogError(ex, "Face encoder execution failed.");
|
|
|
|
|
|
await InvokeOnUiThreadAsync(() =>
|
|
|
|
|
|
{
|
|
|
|
|
|
FaceCommandOutput = ex.ToString();
|
|
|
|
|
|
FaceStatusMessage = "Errore durante esecuzione face encoder.";
|
|
|
|
|
|
}).ConfigureAwait(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
finally
|
|
|
|
|
|
{
|
2026-05-09 15:46:41 +02:00
|
|
|
|
await StopFaceEncoderLogWatcherAsync().ConfigureAwait(false);
|
2026-05-09 12:09:05 +02:00
|
|
|
|
ClearTrackedFaceEncoderProcess();
|
|
|
|
|
|
await InvokeOnUiThreadAsync(() => IsFaceEncoderRunning = ComputeIsFaceEncoderRunning()).ConfigureAwait(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async Task WatchFaceEncoderProcessAsync(CancellationToken token)
|
|
|
|
|
|
{
|
|
|
|
|
|
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(1));
|
|
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
while (await timer.WaitForNextTickAsync(token).ConfigureAwait(false))
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!_hasStartedFaceEncoderInSession)
|
|
|
|
|
|
{
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var isRunning = ComputeIsFaceEncoderRunning();
|
|
|
|
|
|
if (isRunning != IsFaceEncoderRunning)
|
|
|
|
|
|
{
|
|
|
|
|
|
await InvokeOnUiThreadAsync(() => IsFaceEncoderRunning = isRunning).ConfigureAwait(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (OperationCanceledException)
|
|
|
|
|
|
{
|
|
|
|
|
|
// App shutdown.
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void EnsureFaceEncoderWatcherStarted()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (_faceEncoderWatcherTask is not null)
|
|
|
|
|
|
{
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
_faceEncoderWatcherTokenSource = new CancellationTokenSource();
|
|
|
|
|
|
_faceEncoderWatcherTask = WatchFaceEncoderProcessAsync(_faceEncoderWatcherTokenSource.Token);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-09 15:46:41 +02:00
|
|
|
|
private void StartFaceEncoderLogWatcher(string logFilePath, StringBuilder outputLines, StringBuilder transcriptLines)
|
2026-05-09 12:09:05 +02:00
|
|
|
|
{
|
2026-05-09 15:46:41 +02:00
|
|
|
|
_faceEncoderLogWatcherTokenSource?.Cancel();
|
|
|
|
|
|
_faceEncoderLogWatcherTokenSource?.Dispose();
|
|
|
|
|
|
|
|
|
|
|
|
_faceEncoderLogWatcherTokenSource = new CancellationTokenSource();
|
|
|
|
|
|
_faceEncoderLogWatcherTask = WatchFaceEncoderLogFileAsync(logFilePath, outputLines, transcriptLines, _faceEncoderLogWatcherTokenSource.Token);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async Task StopFaceEncoderLogWatcherAsync()
|
|
|
|
|
|
{
|
|
|
|
|
|
var tokenSource = _faceEncoderLogWatcherTokenSource;
|
|
|
|
|
|
var task = _faceEncoderLogWatcherTask;
|
|
|
|
|
|
|
|
|
|
|
|
_faceEncoderLogWatcherTokenSource = null;
|
|
|
|
|
|
_faceEncoderLogWatcherTask = null;
|
|
|
|
|
|
|
|
|
|
|
|
if (tokenSource is null)
|
2026-05-09 12:09:05 +02:00
|
|
|
|
{
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-09 15:46:41 +02:00
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
await tokenSource.CancelAsync().ConfigureAwait(false);
|
|
|
|
|
|
if (task is not null)
|
|
|
|
|
|
{
|
|
|
|
|
|
await task.ConfigureAwait(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (OperationCanceledException)
|
|
|
|
|
|
{
|
|
|
|
|
|
// Expected when shutting down the watcher.
|
|
|
|
|
|
}
|
|
|
|
|
|
finally
|
|
|
|
|
|
{
|
|
|
|
|
|
tokenSource.Dispose();
|
|
|
|
|
|
}
|
2026-05-09 12:09:05 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-09 15:46:41 +02:00
|
|
|
|
private async Task WatchFaceEncoderLogFileAsync(string logFilePath, StringBuilder outputLines, StringBuilder transcriptLines, CancellationToken token)
|
2026-05-09 12:09:05 +02:00
|
|
|
|
{
|
2026-05-09 15:46:41 +02:00
|
|
|
|
long filePosition = 0;
|
|
|
|
|
|
|
|
|
|
|
|
while (!token.IsCancellationRequested)
|
2026-05-09 12:09:05 +02:00
|
|
|
|
{
|
2026-05-09 15:46:41 +02:00
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
if (File.Exists(logFilePath))
|
|
|
|
|
|
{
|
|
|
|
|
|
using var stream = new FileStream(logFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete);
|
|
|
|
|
|
if (filePosition > stream.Length)
|
|
|
|
|
|
{
|
|
|
|
|
|
filePosition = 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
stream.Seek(filePosition, SeekOrigin.Begin);
|
|
|
|
|
|
using var reader = new StreamReader(stream);
|
|
|
|
|
|
while (!reader.EndOfStream)
|
|
|
|
|
|
{
|
|
|
|
|
|
var line = await reader.ReadLineAsync(token).ConfigureAwait(false);
|
|
|
|
|
|
AppendFaceProcessOutput(outputLines, transcriptLines, line, isError: false);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
filePosition = stream.Position;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (OperationCanceledException)
|
2026-05-09 12:09:05 +02:00
|
|
|
|
{
|
2026-05-09 15:46:41 +02:00
|
|
|
|
throw;
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (IOException)
|
|
|
|
|
|
{
|
|
|
|
|
|
// Retry on the next polling interval while the encoder is still writing.
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (UnauthorizedAccessException)
|
|
|
|
|
|
{
|
|
|
|
|
|
// Retry on the next polling interval if the log file is transiently locked.
|
2026-05-09 12:09:05 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-09 15:46:41 +02:00
|
|
|
|
await Task.Delay(TimeSpan.FromMilliseconds(250), token).ConfigureAwait(false);
|
2026-05-09 12:09:05 +02:00
|
|
|
|
}
|
2026-05-09 15:46:41 +02:00
|
|
|
|
}
|
2026-05-09 12:09:05 +02:00
|
|
|
|
|
2026-05-09 15:46:41 +02:00
|
|
|
|
private void RefreshFaceExecutableCapabilities()
|
|
|
|
|
|
{
|
|
|
|
|
|
var executableRoot = NormalizeFilePathArgument(_ai.FaceExecutablePath);
|
|
|
|
|
|
var hasCpu = !string.IsNullOrWhiteSpace(ResolveConfiguredFaceEncoderExecutablePath(executableRoot, useGpu: false));
|
|
|
|
|
|
var hasGpu = !string.IsNullOrWhiteSpace(ResolveConfiguredFaceEncoderExecutablePath(executableRoot, useGpu: true));
|
2026-05-09 12:09:05 +02:00
|
|
|
|
|
2026-05-09 15:46:41 +02:00
|
|
|
|
_ai.FaceGpuOptionEnabled = hasCpu && hasGpu;
|
2026-05-09 12:09:05 +02:00
|
|
|
|
|
2026-05-09 15:46:41 +02:00
|
|
|
|
if (hasGpu && !hasCpu)
|
2026-05-09 12:09:05 +02:00
|
|
|
|
{
|
2026-05-09 15:46:41 +02:00
|
|
|
|
_ai.UseFaceGpu = true;
|
2026-05-09 12:09:05 +02:00
|
|
|
|
}
|
2026-05-09 15:46:41 +02:00
|
|
|
|
else if (!hasGpu)
|
2026-05-09 12:09:05 +02:00
|
|
|
|
{
|
|
|
|
|
|
_ai.UseFaceGpu = false;
|
|
|
|
|
|
}
|
2026-05-09 15:46:41 +02:00
|
|
|
|
}
|
2026-05-09 12:09:05 +02:00
|
|
|
|
|
2026-05-09 17:53:15 +02:00
|
|
|
|
private void RefreshNumberAiGpuCapabilities()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!TryBuildNumberAiModelConfiguration(out var configuration))
|
|
|
|
|
|
{
|
|
|
|
|
|
NumberAiGpuOptionEnabled = false;
|
|
|
|
|
|
_ai.UseNumberAiGpu = false;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
NumberAiGpuOptionEnabled = NumberRecognitionEngine.TryValidateGpuRuntime(configuration, _logger, out _);
|
|
|
|
|
|
if (!NumberAiGpuOptionEnabled)
|
|
|
|
|
|
{
|
|
|
|
|
|
_ai.UseNumberAiGpu = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void SetUseNumberAiGpu(bool value)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!NumberAiGpuOptionEnabled)
|
|
|
|
|
|
{
|
|
|
|
|
|
_ai.UseNumberAiGpu = false;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
_ai.UseNumberAiGpu = value;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private bool TryBuildNumberAiModelConfiguration(out ModelConfiguration configuration)
|
|
|
|
|
|
{
|
|
|
|
|
|
configuration = null!;
|
|
|
|
|
|
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(ModelsFolderPath))
|
|
|
|
|
|
{
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var modelsRoot = Path.GetFullPath(ModelsFolderPath.Trim().Trim('"'));
|
|
|
|
|
|
if (!Directory.Exists(modelsRoot))
|
|
|
|
|
|
{
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
configuration = new ModelConfiguration
|
|
|
|
|
|
{
|
|
|
|
|
|
DetectionCfg = Path.Combine(modelsRoot, "detection.cfg"),
|
|
|
|
|
|
DetectionWeights = Path.Combine(modelsRoot, "detection.weights"),
|
|
|
|
|
|
RecognitionCfg = Path.Combine(modelsRoot, "recognition.cfg"),
|
|
|
|
|
|
RecognitionWeights = Path.Combine(modelsRoot, "recognition.weights"),
|
|
|
|
|
|
UseGpu = true
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return File.Exists(configuration.DetectionCfg)
|
|
|
|
|
|
&& File.Exists(configuration.DetectionWeights)
|
|
|
|
|
|
&& File.Exists(configuration.RecognitionCfg)
|
|
|
|
|
|
&& File.Exists(configuration.RecognitionWeights);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private Task ShowErrorMessageAsync(string title, string message)
|
|
|
|
|
|
{
|
|
|
|
|
|
return InvokeOnUiThreadAsync(() =>
|
|
|
|
|
|
{
|
|
|
|
|
|
ShowMessageRequested?.Invoke(this, Tuple.Create(title, message, 0));
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-09 15:46:41 +02:00
|
|
|
|
private void SetUseFaceGpu(bool value)
|
|
|
|
|
|
{
|
|
|
|
|
|
var currentValue = _ai.UseFaceGpu;
|
|
|
|
|
|
if (!FaceGpuOptionEnabled)
|
2026-05-09 12:09:05 +02:00
|
|
|
|
{
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-09 15:46:41 +02:00
|
|
|
|
if (currentValue == value)
|
2026-05-09 12:09:05 +02:00
|
|
|
|
{
|
2026-05-09 15:46:41 +02:00
|
|
|
|
return;
|
2026-05-09 12:09:05 +02:00
|
|
|
|
}
|
2026-05-09 15:46:41 +02:00
|
|
|
|
|
|
|
|
|
|
_ai.UseFaceGpu = value;
|
|
|
|
|
|
|
|
|
|
|
|
var previousRecommendedUpsample = GetRecommendedFaceUpsample(currentValue);
|
|
|
|
|
|
if (_ai.FaceUpsample == previousRecommendedUpsample)
|
2026-05-09 12:09:05 +02:00
|
|
|
|
{
|
2026-05-09 15:46:41 +02:00
|
|
|
|
_ai.FaceUpsample = GetRecommendedFaceUpsample(value);
|
2026-05-09 12:09:05 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void TrackFaceEncoderProcess(Process process)
|
|
|
|
|
|
{
|
|
|
|
|
|
lock (_faceEncoderProcessLock)
|
|
|
|
|
|
{
|
|
|
|
|
|
_faceEncoderProcess = process;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void ClearTrackedFaceEncoderProcess()
|
|
|
|
|
|
{
|
|
|
|
|
|
lock (_faceEncoderProcessLock)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (_faceEncoderProcess is not null && _faceEncoderProcess.HasExited)
|
|
|
|
|
|
{
|
|
|
|
|
|
_faceEncoderProcess = null;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
_faceEncoderProcess = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private Process? GetTrackedFaceEncoderProcess()
|
|
|
|
|
|
{
|
|
|
|
|
|
lock (_faceEncoderProcessLock)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (_faceEncoderProcess is null)
|
|
|
|
|
|
{
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (_faceEncoderProcess.HasExited)
|
|
|
|
|
|
{
|
|
|
|
|
|
_faceEncoderProcess = null;
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return _faceEncoderProcess;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private Process? FindConfiguredFaceEncoderProcess()
|
|
|
|
|
|
{
|
2026-05-09 15:46:41 +02:00
|
|
|
|
var configuredExecutablePath = ResolveConfiguredFaceEncoderExecutablePath(FaceExecutablePath, UseFaceGpu);
|
2026-05-09 12:09:05 +02:00
|
|
|
|
if (string.IsNullOrWhiteSpace(configuredExecutablePath))
|
|
|
|
|
|
{
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var processName = Path.GetFileNameWithoutExtension(configuredExecutablePath);
|
|
|
|
|
|
foreach (var process in Process.GetProcessesByName(processName))
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!IsProcessAlive(process))
|
|
|
|
|
|
{
|
|
|
|
|
|
process.Dispose();
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (IsMatchingProcessPath(process, configuredExecutablePath))
|
|
|
|
|
|
{
|
|
|
|
|
|
return process;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
process.Dispose();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private bool ComputeIsFaceEncoderRunning()
|
|
|
|
|
|
{
|
|
|
|
|
|
var trackedProcess = GetTrackedFaceEncoderProcess();
|
|
|
|
|
|
if (trackedProcess is not null)
|
|
|
|
|
|
{
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!_hasStartedFaceEncoderInSession)
|
|
|
|
|
|
{
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
using var discoveredProcess = FindConfiguredFaceEncoderProcess();
|
|
|
|
|
|
return discoveredProcess is not null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static bool IsProcessAlive(Process process)
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
return !process.HasExited;
|
|
|
|
|
|
}
|
|
|
|
|
|
catch
|
|
|
|
|
|
{
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static bool IsMatchingProcessPath(Process process, string configuredExecutablePath)
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
var processPath = process.MainModule?.FileName;
|
|
|
|
|
|
return !string.IsNullOrWhiteSpace(processPath)
|
|
|
|
|
|
&& string.Equals(Path.GetFullPath(processPath), Path.GetFullPath(configuredExecutablePath), StringComparison.OrdinalIgnoreCase);
|
|
|
|
|
|
}
|
|
|
|
|
|
catch
|
|
|
|
|
|
{
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private bool TryRequestFaceEncoderStop(Process process)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!IsProcessAlive(process))
|
|
|
|
|
|
{
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#if WINDOWS
|
|
|
|
|
|
if (Program.TrySendConsoleInterrupt(process.Id))
|
|
|
|
|
|
{
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
return process.CloseMainWindow();
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
_logger.LogWarning(ex, "Unable to request graceful stop for face encoder process {ProcessId}", process.Id);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static async Task<bool> WaitForProcessExitAsync(Process process, TimeSpan timeout)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!IsProcessAlive(process))
|
|
|
|
|
|
{
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
using var cancellationTokenSource = new CancellationTokenSource(timeout);
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
await process.WaitForExitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (OperationCanceledException)
|
|
|
|
|
|
{
|
|
|
|
|
|
return !IsProcessAlive(process);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-09 15:46:41 +02:00
|
|
|
|
private void AppendFaceProcessOutput(StringBuilder builder, StringBuilder transcriptBuilder, string? line, bool isError)
|
2026-05-09 12:09:05 +02:00
|
|
|
|
{
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(line))
|
|
|
|
|
|
{
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
lock (builder)
|
|
|
|
|
|
{
|
|
|
|
|
|
builder.AppendLine(line);
|
|
|
|
|
|
}
|
2026-05-09 15:46:41 +02:00
|
|
|
|
|
|
|
|
|
|
if (isError)
|
|
|
|
|
|
{
|
|
|
|
|
|
Console.Error.WriteLine(line);
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
Console.WriteLine(line);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
string transcript;
|
|
|
|
|
|
lock (transcriptBuilder)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (isError)
|
|
|
|
|
|
{
|
|
|
|
|
|
transcriptBuilder.Append("[stderr] ");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
transcriptBuilder.AppendLine(line);
|
|
|
|
|
|
transcript = transcriptBuilder.ToString();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
_ = InvokeOnUiThreadAsync(() => FaceCommandOutput = transcript);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
internal static string? ResolveConfiguredFaceEncoderExecutablePath(string configuredPath, bool useGpu)
|
|
|
|
|
|
{
|
|
|
|
|
|
var variant = useGpu ? "gpu" : "cpu";
|
|
|
|
|
|
foreach (var candidate in EnumerateFaceEncoderExecutableCandidates(configuredPath, variant))
|
|
|
|
|
|
{
|
|
|
|
|
|
if (File.Exists(candidate))
|
|
|
|
|
|
{
|
|
|
|
|
|
return candidate;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
internal static (string OutputFilePath, string LogFilePath) BuildFaceEncoderOutputPaths(string outputFolderPath, string imagesFolderPath, DateTime timestamp)
|
|
|
|
|
|
{
|
|
|
|
|
|
var safeRaceName = BuildSafeFaceEncoderRaceName(imagesFolderPath);
|
|
|
|
|
|
var timestampToken = timestamp.ToString("yyyyMMdd_HHmmss");
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
Path.Combine(outputFolderPath, $"face_encodings_{timestampToken}_{safeRaceName}.pkl"),
|
|
|
|
|
|
Path.Combine(outputFolderPath, $"encoder_log_{timestampToken}_{safeRaceName}.txt"));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
internal static string BuildSafeFaceEncoderRaceName(string imagesFolderPath)
|
|
|
|
|
|
{
|
|
|
|
|
|
var raceName = new DirectoryInfo(imagesFolderPath).Name;
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(raceName))
|
|
|
|
|
|
{
|
|
|
|
|
|
return "race";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var invalidChars = Path.GetInvalidFileNameChars();
|
|
|
|
|
|
var builder = new StringBuilder(raceName.Length);
|
|
|
|
|
|
var previousWasSeparator = false;
|
|
|
|
|
|
|
|
|
|
|
|
foreach (var currentChar in raceName)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (char.IsWhiteSpace(currentChar) || invalidChars.Contains(currentChar))
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!previousWasSeparator)
|
|
|
|
|
|
{
|
|
|
|
|
|
builder.Append('_');
|
|
|
|
|
|
previousWasSeparator = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
builder.Append(currentChar);
|
|
|
|
|
|
previousWasSeparator = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return builder.ToString().Trim('_') switch
|
|
|
|
|
|
{
|
|
|
|
|
|
"" => "race",
|
|
|
|
|
|
var sanitized => sanitized
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static IEnumerable<string> EnumerateFaceEncoderExecutableCandidates(string configuredPath, string variant)
|
|
|
|
|
|
{
|
|
|
|
|
|
var normalizedPath = NormalizeFilePathArgument(configuredPath);
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(normalizedPath))
|
|
|
|
|
|
{
|
|
|
|
|
|
yield break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var executableName = $"face_encoder_{variant}.exe";
|
|
|
|
|
|
|
|
|
|
|
|
if (File.Exists(normalizedPath))
|
|
|
|
|
|
{
|
|
|
|
|
|
var fileDirectory = Path.GetDirectoryName(normalizedPath);
|
|
|
|
|
|
if (!string.IsNullOrWhiteSpace(fileDirectory))
|
|
|
|
|
|
{
|
|
|
|
|
|
yield return Path.Combine(fileDirectory, executableName);
|
|
|
|
|
|
|
|
|
|
|
|
var parentDirectory = Directory.GetParent(fileDirectory)?.FullName;
|
|
|
|
|
|
if (!string.IsNullOrWhiteSpace(parentDirectory))
|
|
|
|
|
|
{
|
|
|
|
|
|
yield return Path.Combine(parentDirectory, $"face_encoder_{variant}", executableName);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
yield return normalizedPath;
|
|
|
|
|
|
yield break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
yield return Path.Combine(normalizedPath, executableName);
|
|
|
|
|
|
yield return Path.Combine(normalizedPath, $"face_encoder_{variant}", executableName);
|
|
|
|
|
|
|
|
|
|
|
|
var leafName = Path.GetFileName(normalizedPath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar));
|
|
|
|
|
|
if (leafName.Equals("face_encoder_cpu", StringComparison.OrdinalIgnoreCase)
|
|
|
|
|
|
|| leafName.Equals("face_encoder_gpu", StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
|
{
|
|
|
|
|
|
var parentDirectory = Directory.GetParent(normalizedPath)?.FullName;
|
|
|
|
|
|
if (!string.IsNullOrWhiteSpace(parentDirectory))
|
|
|
|
|
|
{
|
|
|
|
|
|
yield return Path.Combine(parentDirectory, $"face_encoder_{variant}", executableName);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static int NormalizeFaceParallelism(int value)
|
|
|
|
|
|
{
|
|
|
|
|
|
return value is >= 1 and <= 5 ? value : 3;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-09 18:54:20 +02:00
|
|
|
|
private static int NormalizeNumberAiWorkloadLevel(int value)
|
|
|
|
|
|
{
|
|
|
|
|
|
return value is >= 1 and <= 5 ? value : 3;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static int ResolveNumberAiWorkerCount(bool useGpu, int workloadLevel)
|
|
|
|
|
|
{
|
|
|
|
|
|
var normalized = NormalizeNumberAiWorkloadLevel(workloadLevel);
|
|
|
|
|
|
var maxWorkers = Math.Max(1, Environment.ProcessorCount);
|
|
|
|
|
|
var requestedWorkers = useGpu
|
|
|
|
|
|
? normalized switch
|
|
|
|
|
|
{
|
2026-05-09 19:31:21 +02:00
|
|
|
|
1 => 4,
|
|
|
|
|
|
2 => 8,
|
|
|
|
|
|
3 => 16,
|
|
|
|
|
|
4 => 24,
|
|
|
|
|
|
_ => 32
|
2026-05-09 18:54:20 +02:00
|
|
|
|
}
|
|
|
|
|
|
: normalized switch
|
|
|
|
|
|
{
|
|
|
|
|
|
1 => 1,
|
|
|
|
|
|
2 => 2,
|
|
|
|
|
|
3 => 3,
|
|
|
|
|
|
4 => 4,
|
|
|
|
|
|
_ => 5
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-09 19:31:21 +02:00
|
|
|
|
return useGpu ? requestedWorkers : Math.Min(requestedWorkers, maxWorkers);
|
2026-05-09 18:54:20 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-09 15:46:41 +02:00
|
|
|
|
private static int NormalizeFaceMinSize(int value)
|
|
|
|
|
|
{
|
|
|
|
|
|
return value > 0 ? value : 35;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static bool GetRecommendedFaceUpsample(bool useGpu)
|
|
|
|
|
|
{
|
|
|
|
|
|
return !useGpu;
|
2026-05-09 12:09:05 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-09 15:46:41 +02:00
|
|
|
|
private static string BuildFaceEncoderSummary(
|
|
|
|
|
|
int exitCode,
|
|
|
|
|
|
ProcessStartInfo processStartInfo,
|
|
|
|
|
|
string outputFilePath,
|
|
|
|
|
|
string logFilePath,
|
|
|
|
|
|
StringBuilder outputLines,
|
|
|
|
|
|
StringBuilder errorLines)
|
2026-05-09 12:09:05 +02:00
|
|
|
|
{
|
|
|
|
|
|
var summary = new StringBuilder();
|
|
|
|
|
|
summary.AppendLine($"Exit code: {exitCode}");
|
2026-05-09 15:46:41 +02:00
|
|
|
|
summary.AppendLine($"Command: {processStartInfo.FileName} {string.Join(" ", processStartInfo.ArgumentList)}");
|
|
|
|
|
|
summary.AppendLine($"Output file: {outputFilePath}");
|
|
|
|
|
|
summary.AppendLine($"Log file: {logFilePath}");
|
2026-05-09 12:09:05 +02:00
|
|
|
|
|
|
|
|
|
|
lock (outputLines)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (outputLines.Length > 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
summary.AppendLine();
|
|
|
|
|
|
summary.AppendLine("STDOUT:");
|
|
|
|
|
|
summary.Append(outputLines);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
lock (errorLines)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (errorLines.Length > 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
summary.AppendLine();
|
|
|
|
|
|
summary.AppendLine("STDERR:");
|
|
|
|
|
|
summary.Append(errorLines);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return summary.ToString();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static string NormalizeDirectoryPathArgument(string value)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(value))
|
|
|
|
|
|
{
|
|
|
|
|
|
return string.Empty;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var normalized = value.Trim().Trim('"');
|
|
|
|
|
|
var root = Path.GetPathRoot(normalized);
|
|
|
|
|
|
if (!string.IsNullOrEmpty(root) && normalized.Length > root.Length)
|
|
|
|
|
|
{
|
|
|
|
|
|
normalized = normalized.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return normalized;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static string NormalizeFilePathArgument(string value)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(value))
|
|
|
|
|
|
{
|
|
|
|
|
|
return string.Empty;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return value.Trim().Trim('"');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-04 19:48:03 +01:00
|
|
|
|
// Note: These commands will trigger events that the View will handle to show dialogs
|
|
|
|
|
|
// since dialogs require UI context
|
2026-03-12 18:48:13 +01:00
|
|
|
|
public event EventHandler? SelectSourceFolderRequested;
|
|
|
|
|
|
public event EventHandler? SelectDestinationFolderRequested;
|
|
|
|
|
|
public event EventHandler? SelectLogoFileRequested;
|
|
|
|
|
|
public event EventHandler? SelectModelsFolderRequested;
|
|
|
|
|
|
public event EventHandler? SelectCsvOutputRequested;
|
|
|
|
|
|
public event EventHandler<string?>? SaveSettingsRequested;
|
|
|
|
|
|
public event EventHandler<string?>? LoadSettingsRequested;
|
|
|
|
|
|
public event EventHandler? SelectColorRequested;
|
2026-02-14 19:20:25 +01:00
|
|
|
|
// Request that the View shows a message to the user (message, caption, icon)
|
2026-03-12 18:48:13 +01:00
|
|
|
|
public event EventHandler<Tuple<string, string, int>>? ShowMessageRequested;
|
|
|
|
|
|
public event EventHandler? SelectTransparentColorRequested;
|
2026-02-04 19:48:03 +01:00
|
|
|
|
|
|
|
|
|
|
private void SelectSourceFolder(object parameter)
|
|
|
|
|
|
{
|
|
|
|
|
|
SelectSourceFolderRequested?.Invoke(this, EventArgs.Empty);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void SelectDestinationFolder(object parameter)
|
|
|
|
|
|
{
|
|
|
|
|
|
SelectDestinationFolderRequested?.Invoke(this, EventArgs.Empty);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void SelectLogoFile(object parameter)
|
|
|
|
|
|
{
|
|
|
|
|
|
SelectLogoFileRequested?.Invoke(this, EventArgs.Empty);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-16 18:32:04 +01:00
|
|
|
|
private void SelectModelsFolder(object parameter)
|
|
|
|
|
|
{
|
|
|
|
|
|
SelectModelsFolderRequested?.Invoke(this, EventArgs.Empty);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void SelectCsvOutput(object parameter)
|
|
|
|
|
|
{
|
|
|
|
|
|
SelectCsvOutputRequested?.Invoke(this, EventArgs.Empty);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-04 19:48:03 +01:00
|
|
|
|
private void SaveSettings(object parameter)
|
|
|
|
|
|
{
|
2026-03-12 18:48:13 +01:00
|
|
|
|
SaveSettingsRequested?.Invoke(this, string.Empty);
|
2026-02-04 19:48:03 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void LoadSettings(object parameter)
|
|
|
|
|
|
{
|
2026-03-12 18:48:13 +01:00
|
|
|
|
LoadSettingsRequested?.Invoke(this, string.Empty);
|
2026-02-04 19:48:03 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void SelectColor(object parameter)
|
|
|
|
|
|
{
|
|
|
|
|
|
SelectColorRequested?.Invoke(this, EventArgs.Empty);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-15 11:13:23 +01:00
|
|
|
|
private void SelectTransparentColor(object parameter)
|
|
|
|
|
|
{
|
|
|
|
|
|
SelectTransparentColorRequested?.Invoke(this, EventArgs.Empty);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-04 19:48:03 +01:00
|
|
|
|
public async Task SaveSettingsToFileAsync(string filePath)
|
|
|
|
|
|
{
|
|
|
|
|
|
await _settingsService.SaveSettingsAsync(filePath, this);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public async Task LoadSettingsFromFileAsync(string filePath)
|
|
|
|
|
|
{
|
|
|
|
|
|
await _settingsService.LoadSettingsAsync(filePath, this);
|
|
|
|
|
|
}
|
2026-02-14 22:18:56 +01:00
|
|
|
|
|
|
|
|
|
|
private string _appVersion = string.Empty;
|
|
|
|
|
|
public string AppVersion
|
|
|
|
|
|
{
|
|
|
|
|
|
get => _appVersion;
|
|
|
|
|
|
set
|
|
|
|
|
|
{
|
|
|
|
|
|
_appVersion = value;
|
|
|
|
|
|
NotifyPropertyChanged();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2024-10-14 22:55:52 +02:00
|
|
|
|
}
|
2026-02-10 21:18:46 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|