using ImageCatalog_2.Commands; using ImageCatalog_2.Models; using ImageCatalog_2.Services; using ImageCatalog_2.ViewModels; using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; #if WINDOWS using System.Drawing.Text; #endif using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Text; using System.Threading; using System.Threading.Tasks; #if WINDOWS using System.Windows.Forms; #endif using System.Windows.Input; using AutoMapper; using MaddoShared; using Microsoft.Extensions.Logging; namespace ImageCatalog_2 { public class DataModel : ViewModelBase { public ICommand TestCommand { get; } public ICommand AsyncTestCommand { get; } public ICommand AsyncCancelOperationCommand { get; } public ICommand ProcessImagesCommand { get; } public ICommand SelectSourceFolderCommand { get; } public ICommand SelectDestinationFolderCommand { get; } public ICommand SelectLogoFileCommand { get; } public ICommand SaveSettingsCommand { get; } public ICommand LoadSettingsCommand { get; } public ICommand SelectColorCommand { get; } public ICommand SelectTransparentColorCommand { get; } public ICommand SelectModelsFolderCommand { get; } public ICommand SelectCsvOutputCommand { get; } public ICommand StartAiCommand { get; } public ICommand StartFaceEncoderCommand { get; } public ICommand StopFaceEncoderCommand { get; } private readonly ITestService _service; private readonly ILogger _logger; private readonly ISettingsService _settingsService; private readonly ImageCreationService _imageCreationService; 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; private readonly PicSettings _picSettings; private readonly IMapper _mapper; private readonly AsyncCommand _startFaceEncoderCommand; private readonly AsyncCommand _stopFaceEncoderCommand; private readonly object _faceEncoderProcessLock = new(); private Process? _faceEncoderProcess; private CancellationTokenSource? _faceEncoderWatcherTokenSource; private Task? _faceEncoderWatcherTask; private bool _hasStartedFaceEncoderInSession; private bool _isUpdatingFaceExecutableSelection; // ComboBox collections public List AvailableFonts { get; } public List VerticalPositions { get; } = new() { "Alto", "Centro", "Basso" }; public List HorizontalAlignments { get; } = new() { "Sinistra", "Centro", "Destra" }; [CLSCompliant(false)] public DataModel(ITestService testService, ISettingsService settingsService, ImageCreationService imageCreationService, IAiExtractionService aiExtractionService, IImageProcessingCoordinator imageProcessingCoordinator, PicSettings picSettings, IMapper mapper, ILogger logger, MaddoShared.IVersionProvider? versionProvider = null) { _service = testService; _logger = logger; _settingsService = settingsService; _imageCreationService = imageCreationService; _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; _picSettings = picSettings; _mapper = mapper; // Populate AppVersion from version provider when available AppVersion = versionProvider?.GetVersionString() ?? string.Empty; TestCommand = new RelayCommand(Test); AsyncTestCommand = new AsyncCommand(TestAsync); AsyncCancelOperationCommand = new AsyncCommand(CancelOperation); ProcessImagesCommand = new AsyncCommand(ProcessImages); SelectModelsFolderCommand = new RelayCommand(SelectModelsFolder); SelectCsvOutputCommand = new RelayCommand(SelectCsvOutput); StartAiCommand = new AsyncCommand(StartAiAsync); _startFaceEncoderCommand = new AsyncCommand(RunFaceEncoderAsync, CanRunFaceEncoder); _stopFaceEncoderCommand = new AsyncCommand(() => StopFaceEncoderAsync("Arresto richiesto dall'utente."), CanStopFaceEncoder); StartFaceEncoderCommand = _startFaceEncoderCommand; StopFaceEncoderCommand = _stopFaceEncoderCommand; SelectSourceFolderCommand = new RelayCommand(SelectSourceFolder); SelectDestinationFolderCommand = new RelayCommand(SelectDestinationFolder); SelectLogoFileCommand = new RelayCommand(SelectLogoFile); SaveSettingsCommand = new RelayCommand(SaveSettings); LoadSettingsCommand = new RelayCommand(LoadSettings); SelectColorCommand = new RelayCommand(SelectColor); SelectTransparentColorCommand = new RelayCommand(SelectTransparentColor); // Load available fonts AvailableFonts = LoadAvailableFonts(); RefreshFaceExecutableCapabilities(); } private async Task StartAiAsync() { MainToken = new CancellationTokenSource(); try { await RunAiExtractionCoreAsync(MainToken.Token, useDestination: true, recursive: true).ConfigureAwait(false); } catch (OperationCanceledException) { // user cancelled } finally { MainToken = null; } } private async Task RunAiExtractionCoreAsync(CancellationToken token, bool useDestination = false, bool recursive = false) { var searchRoot = useDestination ? DestinationPath : SourcePath; if (string.IsNullOrWhiteSpace(searchRoot) || !System.IO.Directory.Exists(searchRoot)) { _logger.LogWarning("AI extraction path invalid: {Path}", searchRoot); return; } await InvokeOnUiThreadAsync(() => { PreviewResults.Clear(); AiProgress = 0; }).ConfigureAwait(false); await _aiExtractionService.RunAsync( new AiExtractionRequest { SearchRoot = searchRoot, Recursive = recursive, CsvOutputPath = CsvOutputPath }, token, result => InvokeOnUiThreadAsync(() => PreviewResults.Add(result)), progress => InvokeOnUiThreadAsync(() => AiProgress = progress)).ConfigureAwait(false); } /// /// Optional UI-thread invoker set by the active UI layer (WPF, Avalonia, etc.). /// When set, uses this delegate instead of the WPF dispatcher. /// public Action? UiInvoker { get; set; } private Task InvokeOnUiThreadAsync(Action action) { return Task.Run(() => { if (UiInvoker != null) UiInvoker(action); else System.Windows.Application.Current?.Dispatcher.Invoke(action); }); } public AiSettingsViewModel Ai => _ai; public RaceUploadSettingsViewModel RaceUpload => _raceUpload; // AI properties public bool ExtractNumbers { get => _ai.ExtractNumbers; set => _ai.ExtractNumbers = value; } public string ModelsFolderPath { get => _ai.ModelsFolderPath; set => _ai.ModelsFolderPath = value; } public string CsvOutputPath { get => _ai.CsvOutputPath; set => _ai.CsvOutputPath = value; } public string FaceExecutablePath { get => _ai.FaceExecutablePath; set { var normalizedValue = value ?? string.Empty; if (string.Equals(_ai.FaceExecutablePath, normalizedValue, StringComparison.Ordinal)) { RefreshFaceExecutableCapabilities(); return; } _ai.FaceExecutablePath = normalizedValue; RefreshFaceExecutableCapabilities(); } } public string FaceOutputFolderPath { get => _ai.FaceOutputFolderPath; set => _ai.FaceOutputFolderPath = value; } public bool FaceRecursive { get => _ai.FaceRecursive; set => _ai.FaceRecursive = value; } 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; } // Race upload settings public string ApiLogin { get => _raceUpload.ApiLogin; set => _raceUpload.ApiLogin = value; } public string ApiPassword { get => _raceUpload.ApiPassword; set => _raceUpload.ApiPassword = value; } public string ApiRaceDescription { get => _raceUpload.ApiRaceDescription; set => _raceUpload.ApiRaceDescription = value; } public string ApiRaceTypeId { get => _raceUpload.ApiRaceTypeId; set => _raceUpload.ApiRaceTypeId = value; } public DateTime ApiRaceStartDate { get => _raceUpload.ApiRaceStartDate; set => _raceUpload.ApiRaceStartDate = value; } public DateTime ApiRaceEndDate { get => _raceUpload.ApiRaceEndDate; set => _raceUpload.ApiRaceEndDate = value; } public string ApiPathBase { get => _raceUpload.ApiPathBase; set => _raceUpload.ApiPathBase = value; } public string ApiLocalita { get => _raceUpload.ApiLocalita; set => _raceUpload.ApiLocalita = value; } public int ApiEventoInLineaIndex { get => _raceUpload.ApiEventoInLineaIndex; set => _raceUpload.ApiEventoInLineaIndex = value; } public int ApiTipoIndexValue { get => _raceUpload.ApiTipoIndexValue; set => _raceUpload.ApiTipoIndexValue = value; } public int ApiFreeEventIndex { get => _raceUpload.ApiFreeEventIndex; set => _raceUpload.ApiFreeEventIndex = value; } public string ApiRaceId { get => _raceUpload.ApiRaceId; set => _raceUpload.ApiRaceId = value; } public string ApiRemoteProcessedBasePath { get => _raceUpload.ApiRemoteProcessedBasePath; set => _raceUpload.ApiRemoteProcessedBasePath = value; } // Preview results for DataGrid public System.Collections.ObjectModel.ObservableCollection PreviewResults => _ai.PreviewResults; public double AiProgress { get => _ai.AiProgress; set => _ai.AiProgress = value; } private List LoadAvailableFonts() { #if WINDOWS var fonts = new List(); using (var installedFonts = new InstalledFontCollection()) { fonts.AddRange(installedFonts.Families.Select(f => f.Name)); } return fonts; #else return new List(); #endif } private CancellationTokenSource? _mainToken; public CancellationTokenSource? MainToken { get => _mainToken; set { _mainToken = value; NotifyPropertyChanged(); } } public string SourcePath { get => _paths.SourcePath; set => _paths.SourcePath = value; } public string DestinationPath { get => _paths.DestinationPath; set => _paths.DestinationPath = value; } public string HorizontalText { get => _visual.HorizontalText; set => _visual.HorizontalText = value; } public string VerticalText { get => _visual.VerticalText; set => _visual.VerticalText = value; } public bool OverwriteImages { get => _visual.OverwriteImages; set => _visual.OverwriteImages = value; } private bool _uiEnabled = true; public bool UiEnabled { get => _uiEnabled; set { _uiEnabled = value; NotifyPropertyChanged(); NotifyPropertyChanged(nameof(UiDisabled)); } } public bool UiDisabled => !_uiEnabled; public ProcessingStateViewModel Processing => _processing; public PathSettingsViewModel Paths => _paths; public VisualSettingsViewModel Visual => _visual; private void OnProcessingPropertyChanged(object? sender, PropertyChangedEventArgs e) { if (string.IsNullOrWhiteSpace(e.PropertyName)) { return; } // 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); UpdateFaceEncoderCommandStates(); } 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; } private int _chunkSize; public int ChunkSize { get => _chunkSize; set { _chunkSize = value; NotifyPropertyChanged(); } } private int _threadsCount; public int ThreadsCount { get => _threadsCount; set { _threadsCount = value; NotifyPropertyChanged(); } } // Thumbnail settings public string ThumbnailPrefix { get => _visual.ThumbnailPrefix; set => _visual.ThumbnailPrefix = value; } public int ThumbnailHeight { get => _visual.ThumbnailHeight; set => _visual.ThumbnailHeight = value; } public int ThumbnailWidth { get => _visual.ThumbnailWidth; set => _visual.ThumbnailWidth = value; } // Big photo settings public int PhotoBigHeight { get => _visual.PhotoBigHeight; set => _visual.PhotoBigHeight = value; } public int PhotoBigWidth { get => _visual.PhotoBigWidth; set => _visual.PhotoBigWidth = value; } // Font settings public int FontSize { get => _visual.FontSize; set => _visual.FontSize = value; } public int FontSizeThumbnail { get => _visual.FontSizeThumbnail; set => _visual.FontSizeThumbnail = value; } public string FontName { get => _visual.FontName; set => _visual.FontName = value; } public bool FontBold { get => _visual.FontBold; set => _visual.FontBold = value; } // Text settings public int TextTransparency { get => _visual.TextTransparency; set => _visual.TextTransparency = value; } public int TextMargin { get => _visual.TextMargin; set => _visual.TextMargin = value; } public string TextColorRGB { get => _visual.TextColorRGB; set => _visual.TextColorRGB = value; } public string TransparentColor { get => _visual.TransparentColor; set => _visual.TransparentColor = value; } public bool UseTransparentColor { get => _visual.UseTransparentColor; set => _visual.UseTransparentColor = value; } // Logo/Watermark settings public string LogoFile { get => _visual.LogoFile; set => _visual.LogoFile = value; } public int LogoHeight { get => _visual.LogoHeight; set => _visual.LogoHeight = value; } public int LogoWidth { get => _visual.LogoWidth; set => _visual.LogoWidth = value; } public int LogoMargin { get => _visual.LogoMargin; set => _visual.LogoMargin = value; } public int LogoTransparency { get => _visual.LogoTransparency; set => _visual.LogoTransparency = value; } // Image library selection (UI radio buttons bind to the boolean helpers) private string _imageLibrary = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "System.Graphics" : "ImageSharp"; /// /// Whether the application is running on Windows. Used by cross-platform UIs to show/hide Windows-only options. /// public bool IsRunningOnWindows => RuntimeInformation.IsOSPlatform(OSPlatform.Windows); /// /// 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. /// 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)); NotifyPropertyChanged(nameof(IsRunningOnWindows)); } } 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(); } } // 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 { get => _visual.VerticalTextSize; set => _visual.VerticalTextSize = value; } public int VerticalTextMargin { get => _visual.VerticalTextMargin; set => _visual.VerticalTextMargin = value; } // JPEG compression settings public int JpegQuality { get => _visual.JpegQuality; set => _visual.JpegQuality = value; } public int JpegQualityThumbnail { get => _visual.JpegQualityThumbnail; set => _visual.JpegQualityThumbnail = value; } // 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 { get => _visual.AddLogo; set => _visual.AddLogo = value; } public bool KeepOriginalDimensions { get => _visual.KeepOriginalDimensions; set => _visual.KeepOriginalDimensions = value; } private bool _showDate; public bool ShowDate { get => _showDate; set { _showDate = value; NotifyPropertyChanged(); } } private bool _showPhotoNumber; public bool ShowPhotoNumber { get => _showPhotoNumber; set { if (_showPhotoNumber == value) return; _showPhotoNumber = value; 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)); } NotifyPropertyChanged(); NotifyPropertyChanged(nameof(ThumbnailMode)); } } 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 { get => _thumbnailOption == ThumbnailOptionEnum.Time; set { if (value) { ThumbnailOption = ThumbnailOptionEnum.Time; } else if (_thumbnailOption == ThumbnailOptionEnum.Time) { ThumbnailOption = ThumbnailOptionEnum.None; } NotifyPropertyChanged(); } } private bool _showFileNameOnThumbnails; public bool ShowFileNameOnThumbnails { get => _thumbnailOption == ThumbnailOptionEnum.FileName; set { if (value) ThumbnailOption = ThumbnailOptionEnum.FileName; else if (_thumbnailOption == ThumbnailOptionEnum.FileName) ThumbnailOption = ThumbnailOptionEnum.None; 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(); } } private string _bigPhotoSuffix = ""; public string BigPhotoSuffix { get => _bigPhotoSuffix; set { _bigPhotoSuffix = value; NotifyPropertyChanged(); } } private bool _addTextToThumbnails; public bool AddTextToThumbnails { get => _thumbnailOption == ThumbnailOptionEnum.Text; set { if (value) ThumbnailOption = ThumbnailOptionEnum.Text; else if (_thumbnailOption == ThumbnailOptionEnum.Text) ThumbnailOption = ThumbnailOptionEnum.None; NotifyPropertyChanged(); } } private bool _addRaceTimeToThumbnails; public bool AddRaceTimeToThumbnails { get => _thumbnailOption == ThumbnailOptionEnum.RaceTime; set { if (value) ThumbnailOption = ThumbnailOptionEnum.RaceTime; else if (_thumbnailOption == ThumbnailOptionEnum.RaceTime) ThumbnailOption = ThumbnailOptionEnum.None; NotifyPropertyChanged(); } } private bool _addNumberAndTimeToThumbnails; public bool AddNumberAndTimeToThumbnails { get => _thumbnailOption == ThumbnailOptionEnum.FileNameAndTime; set { if (value) 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; NotifyPropertyChanged(); } } // Single authoritative thumbnail mode string to avoid conflicting bindings // Possible values: "None", "Text", "Time", "Number", "NumberAndTime", "RaceTime" public string ThumbnailMode { get { return _thumbnailOption switch { ThumbnailOptionEnum.Text => "Text", ThumbnailOptionEnum.Time => "Time", ThumbnailOptionEnum.FileName => "Number", ThumbnailOptionEnum.FileNameAndTime => "NumberAndTime", ThumbnailOptionEnum.RaceTime => "RaceTime", _ => "None", }; } set { // Map incoming string to enum and set the authoritative property switch ((value ?? string.Empty).ToLowerInvariant()) { case "text": ThumbnailOption = ThumbnailOptionEnum.Text; break; case "time": ThumbnailOption = ThumbnailOptionEnum.Time; break; case "number": ThumbnailOption = ThumbnailOptionEnum.FileName; break; case "numberandtime": ThumbnailOption = ThumbnailOptionEnum.FileNameAndTime; break; case "racetime": ThumbnailOption = ThumbnailOptionEnum.RaceTime; break; default: ThumbnailOption = ThumbnailOptionEnum.None; break; } } } // Image processing progress and status public string ProcessingStatus { get => _processing.ProcessingStatus; set => _processing.ProcessingStatus = value; } public int ProcessedImagesCount { get => _processing.ProcessedImagesCount; set => _processing.ProcessedImagesCount = value; } public int TotalImagesCount { get => _processing.TotalImagesCount; set => _processing.TotalImagesCount = value; } public int ProgressBarValue { get => _processing.ProgressBarValue; set => _processing.ProgressBarValue = value; } public int ProgressBarMaximum { get => _processing.ProgressBarMaximum; set => _processing.ProgressBarMaximum = value; } private void Test(object parameter) { Debug.WriteLine("Yep"); this.UiEnabled = !this.UiEnabled; } private async Task TestAsync() { Debug.WriteLine("Yep c"); } private async Task ProcessImages() { _logger.LogInformation("Avvio elaborazione..."); UiEnabled = false; MainToken?.Dispose(); MainToken = new CancellationTokenSource(); var token = MainToken.Token; // Normalize paths _paths.NormalizePaths(); // Reset counters _processing.ResetForRun(); // Update PicSettings from DataModel using AutoMapper _mapper.Map(this, _picSettings); // 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 } var imageCreationOptions = new ImageCreationService.Options { 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 { 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); // AI integration stub: if ExtractNumbers is enabled, simulate or invoke OCR processing if (ExtractNumbers) { try { await RunAiExtractionCoreAsync(token); } catch (OperationCanceledException) { _logger.LogInformation("AI extraction canceled"); } catch (Exception ex) { _logger.LogError(ex, "AI extraction failed"); } } SpeedCounter = runResult.FinalSpeedCounter; } 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; } private async Task CancelOperation() { try { 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"); } } UiEnabled = true; } catch (Exception e) { _logger.LogError(e, "Error canceling the token"); _logger.LogInformation("Ignora questo errore"); } } 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; } var executablePath = NormalizeFilePathArgument(FaceExecutablePath); var outputFilePath = NormalizeFilePathArgument(FaceOutputFolderPath); var imagesFolder = NormalizeDirectoryPathArgument(DestinationPath); if (string.IsNullOrWhiteSpace(executablePath) || !File.Exists(executablePath)) { FaceStatusMessage = "Percorso eseguibile non valido."; return; } if (string.IsNullOrWhiteSpace(imagesFolder) || !Directory.Exists(imagesFolder)) { FaceStatusMessage = "Cartella Destinazione non valida."; return; } if (string.IsNullOrWhiteSpace(outputFilePath)) { FaceStatusMessage = "Inserisci il file di output .pkl."; return; } if (!string.Equals(Path.GetExtension(outputFilePath), ".pkl", StringComparison.OrdinalIgnoreCase)) { FaceStatusMessage = "Il file di output deve avere estensione .pkl."; return; } try { var outputDirectory = Path.GetDirectoryName(outputFilePath); if (!string.IsNullOrWhiteSpace(outputDirectory)) { Directory.CreateDirectory(outputDirectory); } } catch (Exception ex) { _logger.LogError(ex, "Unable to create face output directory for file: {OutputFilePath}", outputFilePath); FaceStatusMessage = "Impossibile creare la cartella del file di output."; return; } FaceExecutablePath = executablePath; FaceOutputFolderPath = outputFilePath; FaceCommandOutput = string.Empty; FaceStatusMessage = "Esecuzione face encoder in corso..."; 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, RedirectStandardInput = true, CreateNoWindow = false, }; processStartInfo.ArgumentList.Add("--images"); processStartInfo.ArgumentList.Add(imagesFolder); processStartInfo.ArgumentList.Add("--out"); processStartInfo.ArgumentList.Add(outputFilePath); if (FaceRecursive) { processStartInfo.ArgumentList.Add("--recursive"); } using var process = new Process { StartInfo = processStartInfo, EnableRaisingEvents = true }; process.OutputDataReceived += (_, args) => AppendFaceProcessOutput(outputLines, args.Data); process.ErrorDataReceived += (_, args) => AppendFaceProcessOutput(errorLines, args.Data); if (!process.Start()) { throw new InvalidOperationException("Avvio face encoder fallito."); } _hasStartedFaceEncoderInSession = true; EnsureFaceEncoderWatcherStarted(); TrackFaceEncoderProcess(process); await InvokeOnUiThreadAsync(() => IsFaceEncoderRunning = true).ConfigureAwait(false); process.BeginOutputReadLine(); process.BeginErrorReadLine(); await process.WaitForExitAsync().ConfigureAwait(false); var summary = BuildFaceEncoderSummary(process.ExitCode, outputLines, errorLines); await InvokeOnUiThreadAsync(() => { FaceCommandOutput = summary; FaceStatusMessage = process.ExitCode == 0 ? "Face encoder completato." : $"Face encoder terminato con errore (code {process.ExitCode})."; }).ConfigureAwait(false); } catch (Exception ex) { _logger.LogError(ex, "Face encoder execution failed."); await InvokeOnUiThreadAsync(() => { FaceCommandOutput = ex.ToString(); FaceStatusMessage = "Errore durante esecuzione face encoder."; }).ConfigureAwait(false); } finally { 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); } private void RefreshFaceExecutableCapabilities() { if (_isUpdatingFaceExecutableSelection) { return; } var executableName = Path.GetFileNameWithoutExtension(NormalizeFilePathArgument(_ai.FaceExecutablePath)); var supportsGpu = executableName.EndsWith("_cpu", StringComparison.OrdinalIgnoreCase) || executableName.EndsWith("_gpu", StringComparison.OrdinalIgnoreCase); var useGpu = supportsGpu && executableName.EndsWith("_gpu", StringComparison.OrdinalIgnoreCase); _ai.FaceGpuOptionEnabled = supportsGpu; _ai.UseFaceGpu = supportsGpu && useGpu; } private void SetUseFaceGpu(bool value) { if (!FaceGpuOptionEnabled) { if (_ai.UseFaceGpu) { _ai.UseFaceGpu = false; } return; } if (_ai.UseFaceGpu == value) { return; } _ai.UseFaceGpu = value; var currentPath = NormalizeFilePathArgument(_ai.FaceExecutablePath); if (string.IsNullOrWhiteSpace(currentPath)) { return; } var extension = Path.GetExtension(currentPath); var fileName = Path.GetFileNameWithoutExtension(currentPath); var baseName = fileName.EndsWith("_cpu", StringComparison.OrdinalIgnoreCase) ? fileName[..^4] : fileName.EndsWith("_gpu", StringComparison.OrdinalIgnoreCase) ? fileName[..^4] : fileName; if (string.Equals(baseName, fileName, StringComparison.Ordinal)) { _ai.UseFaceGpu = false; _ai.FaceGpuOptionEnabled = false; return; } var updatedFileName = $"{baseName}{(value ? "_gpu" : "_cpu")}{extension}"; var directory = Path.GetDirectoryName(currentPath); var updatedPath = string.IsNullOrWhiteSpace(directory) ? updatedFileName : Path.Combine(directory, updatedFileName); if (string.Equals(updatedPath, currentPath, StringComparison.OrdinalIgnoreCase)) { return; } _isUpdatingFaceExecutableSelection = true; try { _ai.FaceExecutablePath = updatedPath; } finally { _isUpdatingFaceExecutableSelection = false; } } 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() { var configuredExecutablePath = NormalizeFilePathArgument(FaceExecutablePath); 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 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); } } private static void AppendFaceProcessOutput(StringBuilder builder, string? line) { if (string.IsNullOrWhiteSpace(line)) { return; } lock (builder) { builder.AppendLine(line); } } private static string BuildFaceEncoderSummary(int exitCode, StringBuilder outputLines, StringBuilder errorLines) { var summary = new StringBuilder(); summary.AppendLine($"Exit code: {exitCode}"); 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('"'); } // Note: These commands will trigger events that the View will handle to show dialogs // since dialogs require UI context public event EventHandler? SelectSourceFolderRequested; public event EventHandler? SelectDestinationFolderRequested; public event EventHandler? SelectLogoFileRequested; public event EventHandler? SelectModelsFolderRequested; public event EventHandler? SelectCsvOutputRequested; public event EventHandler? SaveSettingsRequested; public event EventHandler? LoadSettingsRequested; public event EventHandler? SelectColorRequested; // Request that the View shows a message to the user (message, caption, icon) #if WINDOWS public event EventHandler>? ShowMessageRequested; #else public event EventHandler>? ShowMessageRequested; #endif public event EventHandler? SelectTransparentColorRequested; 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); } private void SelectModelsFolder(object parameter) { SelectModelsFolderRequested?.Invoke(this, EventArgs.Empty); } private void SelectCsvOutput(object parameter) { SelectCsvOutputRequested?.Invoke(this, EventArgs.Empty); } private void SaveSettings(object parameter) { SaveSettingsRequested?.Invoke(this, string.Empty); } private void LoadSettings(object parameter) { LoadSettingsRequested?.Invoke(this, string.Empty); } private void SelectColor(object parameter) { SelectColorRequested?.Invoke(this, EventArgs.Empty); } private void SelectTransparentColor(object parameter) { SelectTransparentColorRequested?.Invoke(this, EventArgs.Empty); } public async Task SaveSettingsToFileAsync(string filePath) { await _settingsService.SaveSettingsAsync(filePath, this); } public async Task LoadSettingsFromFileAsync(string filePath) { await _settingsService.LoadSettingsAsync(filePath, this); } private string _appVersion = string.Empty; public string AppVersion { get => _appVersion; set { _appVersion = value; NotifyPropertyChanged(); } } } }