using ImageCatalog_2.Commands; using ImageCatalog_2.Services; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Drawing.Text; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; 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; } private readonly ITestService _service; private readonly ILogger _logger; private readonly ISettingsService _settingsService; private readonly ImageCreationStuff _imageCreationService; private readonly PicSettings _picSettings; private readonly IMapper _mapper; // ComboBox collections public List AvailableFonts { get; } public List VerticalPositions { get; } = new() { "Alto", "Centro", "Basso" }; public List HorizontalAlignments { get; } = new() { "Sinistra", "Centro", "Destra" }; public DataModel(ITestService testService, ISettingsService settingsService, ImageCreationStuff imageCreationService, PicSettings picSettings, IMapper mapper, ILogger logger, MaddoShared.IVersionProvider? versionProvider = null) { _service = testService; _logger = logger; _settingsService = settingsService; _imageCreationService = imageCreationService; _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); 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(); } private async Task RunAiExtractionAsync(CancellationToken token) { // Simple stub: scan source folder for supported images and either call AIFotoONLUS.Core // or simulate results. Write CSV output and populate PreviewResults. if (string.IsNullOrWhiteSpace(SourcePath) || !System.IO.Directory.Exists(SourcePath)) { _logger.LogWarning("Source path invalid for AI extraction: {SourcePath}", SourcePath); return; } var imageFiles = System.IO.Directory.EnumerateFiles(SourcePath, "*.*", System.IO.SearchOption.TopDirectoryOnly) .Where(f => f.EndsWith(".jpg", StringComparison.OrdinalIgnoreCase) || f.EndsWith(".jpeg", StringComparison.OrdinalIgnoreCase) || f.EndsWith(".png", StringComparison.OrdinalIgnoreCase) || f.EndsWith(".bmp", StringComparison.OrdinalIgnoreCase) || f.EndsWith(".gif", StringComparison.OrdinalIgnoreCase)) .ToList(); if (imageFiles.Count == 0) { _logger.LogInformation("No image files found for AI extraction in {SourcePath}", SourcePath); return; } // Clear preview await InvokeOnUiThreadAsync(() => { PreviewResults.Clear(); }); // Try to locate AIFotoONLUS.Core types via reflection to avoid hard reference at compile time Type? aiProcessorType = null; object? aiProcessor = null; try { var assembly = AppDomain.CurrentDomain.GetAssemblies() .FirstOrDefault(a => a.GetName().Name?.Equals("AIFotoONLUS.Core", StringComparison.OrdinalIgnoreCase) == true); if (assembly != null) { aiProcessorType = assembly.GetType("AIFotoONLUS.Core.AiProcessor"); if (aiProcessorType != null) { // Create instance assuming parameterless ctor aiProcessor = Activator.CreateInstance(aiProcessorType); } } } catch (Exception ex) { _logger.LogDebug(ex, "AIFotoONLUS.Core not available or failed to load via reflection"); } var results = new List(); foreach (var file in imageFiles) { token.ThrowIfCancellationRequested(); string extracted = string.Empty; if (aiProcessorType is not null && aiProcessor is not null) { try { // Preferred method name: ExtractNumbersFromImage(string imagePath) var method = aiProcessorType.GetMethod("ExtractNumbersFromImage") ?? aiProcessorType.GetMethod("ExtractTextFromImage"); if (method is not null) { var value = method.Invoke(aiProcessor, new object[] { file }); if (value != null) extracted = value.ToString() ?? string.Empty; } else { // No expected method found, fallback to simulated result extracted = SimulateExtraction(file); } } catch (Exception ex) { _logger.LogWarning(ex, "Error invoking AI processor for {File}", file); extracted = SimulateExtraction(file); } } else { // Simulate extraction when library not available extracted = SimulateExtraction(file); } var res = new AiResult { Path = file, Text = extracted }; results.Add(res); await InvokeOnUiThreadAsync(() => PreviewResults.Add(res)); } // Write CSV if requested if (!string.IsNullOrWhiteSpace(CsvOutputPath)) { try { var dir = System.IO.Path.GetDirectoryName(CsvOutputPath) ?? string.Empty; if (!string.IsNullOrWhiteSpace(dir) && !System.IO.Directory.Exists(dir)) { System.IO.Directory.CreateDirectory(dir); } using var sw = new System.IO.StreamWriter(CsvOutputPath, false, System.Text.Encoding.UTF8); sw.WriteLine("Path,Text"); foreach (var r in results) { var safeText = (r.Text ?? string.Empty).Replace("\"", "\"\""); sw.WriteLine($"\"{r.Path}\",\"{safeText}\""); } } catch (Exception ex) { _logger.LogError(ex, "Failed to write CSV to {CsvOutputPath}", CsvOutputPath); } } } private string SimulateExtraction(string file) { // Cheap heuristic: return filename digits var name = System.IO.Path.GetFileNameWithoutExtension(file); var digits = new string(name.Where(char.IsDigit).ToArray()); if (string.IsNullOrEmpty(digits)) return ""; return digits; } private Task InvokeOnUiThreadAsync(Action action) { // Use SynchronizationContext via Task to ensure UI thread update return Task.Run(() => { System.Windows.Application.Current?.Dispatcher.Invoke(action); }); } // AI properties private bool _extractNumbers; public bool ExtractNumbers { get => _extractNumbers; set { _extractNumbers = value; NotifyPropertyChanged(); } } private string _modelsFolderPath = string.Empty; public string ModelsFolderPath { get => _modelsFolderPath; set { _modelsFolderPath = value; NotifyPropertyChanged(); } } private string _csvOutputPath = string.Empty; public string CsvOutputPath { get => _csvOutputPath; set { _csvOutputPath = value; NotifyPropertyChanged(); } } // Preview results for DataGrid private System.Collections.ObjectModel.ObservableCollection _previewResults = new(); public System.Collections.ObjectModel.ObservableCollection PreviewResults => _previewResults; public class AiResult { public string Path { get; set; } = string.Empty; public string Text { get; set; } = string.Empty; } private List LoadAvailableFonts() { var fonts = new List(); using (var installedFonts = new InstalledFontCollection()) { fonts.AddRange(installedFonts.Families.Select(f => f.Name)); } return fonts; } private CancellationTokenSource? _mainToken; public CancellationTokenSource? MainToken { get => _mainToken; set { _mainToken = value; NotifyPropertyChanged(); } } private string _sourcePath; public string SourcePath { get => _sourcePath; set { _sourcePath = value; NotifyPropertyChanged(); } } private string _destinationPath; public string DestinationPath { get => _destinationPath; set { _destinationPath = value; NotifyPropertyChanged(); } } private string _horizontalText; public string HorizontalText { get => _horizontalText; set { _horizontalText = value; NotifyPropertyChanged(); } } private string _verticalText; public string VerticalText { get => _verticalText; set { _verticalText = value; NotifyPropertyChanged(); } } private bool _overwriteImages; public bool OverwriteImages { get => _overwriteImages; set { _overwriteImages = value; NotifyPropertyChanged(); } } private bool _uiEnabled = true; public bool UiEnabled { get => _uiEnabled; set { _uiEnabled = value; NotifyPropertyChanged(); NotifyPropertyChanged(nameof(UiDisabled)); } } public bool UiDisabled => !_uiEnabled; private string _speedCounter = "-"; public string SpeedCounter { get => _speedCounter; set { _speedCounter = value; NotifyPropertyChanged(); } } 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 private string _thumbnailPrefix = "tn_"; public string ThumbnailPrefix { get => _thumbnailPrefix; set { _thumbnailPrefix = value; NotifyPropertyChanged(); } } private int _thumbnailHeight = 350; public int ThumbnailHeight { get => _thumbnailHeight; set { _thumbnailHeight = value; NotifyPropertyChanged(); } } private int _thumbnailWidth = 350; public int ThumbnailWidth { get => _thumbnailWidth; set { _thumbnailWidth = value; NotifyPropertyChanged(); } } // Big photo settings private int _photoBigHeight = 2240; public int PhotoBigHeight { get => _photoBigHeight; set { _photoBigHeight = value; NotifyPropertyChanged(); } } private int _photoBigWidth = 2240; public int PhotoBigWidth { get => _photoBigWidth; set { _photoBigWidth = value; NotifyPropertyChanged(); } } // Font settings private int _fontSize = 20; public int FontSize { get => _fontSize; set { _fontSize = value; NotifyPropertyChanged(); } } private int _fontSizeThumbnail = 50; public int FontSizeThumbnail { get => _fontSizeThumbnail; set { _fontSizeThumbnail = value; NotifyPropertyChanged(); } } private string _fontName = "Arial"; public string FontName { get => _fontName; set { _fontName = value; NotifyPropertyChanged(); } } private bool _fontBold = false; public bool FontBold { get => _fontBold; set { _fontBold = value; NotifyPropertyChanged(); } } // Text settings private int _textTransparency = 0; public int TextTransparency { get => _textTransparency; set { _textTransparency = value; NotifyPropertyChanged(); } } private int _textMargin = 8; public int TextMargin { get => _textMargin; set { _textMargin = value; NotifyPropertyChanged(); } } private string _textColorRGB = "Yellow"; public string TextColorRGB { get => _textColorRGB; set { _textColorRGB = value; NotifyPropertyChanged(); } } private string _transparentColor = "#FFFFFF"; public string TransparentColor { get => _transparentColor; set { _transparentColor = value; NotifyPropertyChanged(); } } private bool _useTransparentColor; public bool UseTransparentColor { get => _useTransparentColor; set { _useTransparentColor = value; NotifyPropertyChanged(); } } // Logo/Watermark settings private string _logoFile = ""; public string LogoFile { get => _logoFile; set { _logoFile = value; NotifyPropertyChanged(); } } private int _logoHeight = 430; public int LogoHeight { get => _logoHeight; set { _logoHeight = value; NotifyPropertyChanged(); } } private int _logoWidth = 430; public int LogoWidth { get => _logoWidth; set { _logoWidth = value; NotifyPropertyChanged(); } } private int _logoMargin = 290; public int LogoMargin { get => _logoMargin; set { _logoMargin = value; NotifyPropertyChanged(); } } private int _logoTransparency = 100; public int LogoTransparency { get => _logoTransparency; set { _logoTransparency = value; NotifyPropertyChanged(); } } // Image library selection (UI radio buttons bind to the boolean helpers) private string _imageLibrary = "System.Graphics"; /// /// 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)); } } 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 private int _verticalTextSize = 20; public int VerticalTextSize { get => _verticalTextSize; set { _verticalTextSize = value; NotifyPropertyChanged(); } } private int _verticalTextMargin = 6; public int VerticalTextMargin { get => _verticalTextMargin; set { _verticalTextMargin = value; NotifyPropertyChanged(); } } // JPEG compression settings private int _jpegQuality = 85; public int JpegQuality { get => _jpegQuality; set { _jpegQuality = value; NotifyPropertyChanged(); } } private int _jpegQualityThumbnail = 30; public int JpegQualityThumbnail { get => _jpegQualityThumbnail; set { _jpegQualityThumbnail = value; NotifyPropertyChanged(); } } // 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(); } } private bool _addLogo; public bool AddLogo { get => _addLogo; set { _addLogo = value; NotifyPropertyChanged(); } } private bool _keepOriginalDimensions; public bool KeepOriginalDimensions { get => _keepOriginalDimensions; set { _keepOriginalDimensions = value; NotifyPropertyChanged(); } } 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 => _addTimeToThumbnails; set { if (_addTimeToThumbnails == value) return; _addTimeToThumbnails = value; if (value) { // ensure mutually exclusive choices _addTextToThumbnails = false; _showPhotoNumber = false; _addNumberAndTimeToThumbnails = false; _addRaceTimeToThumbnails = false; NotifyPropertyChanged(nameof(AddTextToThumbnails)); NotifyPropertyChanged(nameof(ShowPhotoNumber)); NotifyPropertyChanged(nameof(AddNumberAndTimeToThumbnails)); NotifyPropertyChanged(nameof(AddRaceTimeToThumbnails)); } NotifyPropertyChanged(); } } private bool _showFileNameOnThumbnails; public bool ShowFileNameOnThumbnails { get => _showFileNameOnThumbnails; set { _showFileNameOnThumbnails = value; 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 => _addTextToThumbnails; set { if (_addTextToThumbnails == value) return; _addTextToThumbnails = value; if (value) { // ensure mutually exclusive choices _addTimeToThumbnails = false; _showPhotoNumber = false; _addNumberAndTimeToThumbnails = false; _addRaceTimeToThumbnails = false; NotifyPropertyChanged(nameof(AddTimeToThumbnails)); NotifyPropertyChanged(nameof(ShowPhotoNumber)); NotifyPropertyChanged(nameof(AddNumberAndTimeToThumbnails)); NotifyPropertyChanged(nameof(AddRaceTimeToThumbnails)); } NotifyPropertyChanged(); } } private bool _addRaceTimeToThumbnails; public bool AddRaceTimeToThumbnails { get => _addRaceTimeToThumbnails; set { if (_addRaceTimeToThumbnails == value) return; _addRaceTimeToThumbnails = value; if (value) { // ensure mutually exclusive choices _addTimeToThumbnails = false; _addTextToThumbnails = false; _showPhotoNumber = false; _addNumberAndTimeToThumbnails = false; NotifyPropertyChanged(nameof(AddTimeToThumbnails)); NotifyPropertyChanged(nameof(AddTextToThumbnails)); NotifyPropertyChanged(nameof(ShowPhotoNumber)); NotifyPropertyChanged(nameof(AddNumberAndTimeToThumbnails)); } NotifyPropertyChanged(); } } private bool _addNumberAndTimeToThumbnails; public bool AddNumberAndTimeToThumbnails { get => _addNumberAndTimeToThumbnails; set { if (_addNumberAndTimeToThumbnails == value) return; _addNumberAndTimeToThumbnails = value; if (value) { // ensure mutually exclusive choices _addTimeToThumbnails = false; _addTextToThumbnails = false; _showPhotoNumber = false; _addRaceTimeToThumbnails = false; NotifyPropertyChanged(nameof(AddTimeToThumbnails)); NotifyPropertyChanged(nameof(AddTextToThumbnails)); NotifyPropertyChanged(nameof(ShowPhotoNumber)); NotifyPropertyChanged(nameof(AddRaceTimeToThumbnails)); } NotifyPropertyChanged(); } } // Single authoritative thumbnail mode string to avoid conflicting bindings // Possible values: "None", "Text", "Time", "Number", "NumberAndTime", "RaceTime" public string ThumbnailMode { get { if (AddTextToThumbnails) return "Text"; if (AddTimeToThumbnails) return "Time"; if (ShowPhotoNumber) return "Number"; if (AddNumberAndTimeToThumbnails) return "NumberAndTime"; if (AddRaceTimeToThumbnails) return "RaceTime"; return "None"; } set { var current = ThumbnailMode; if (string.Equals(current, value, StringComparison.OrdinalIgnoreCase)) return; // Set the boolean flags via their public setters so mutual-exclusion logic runs switch ((value ?? string.Empty).ToLowerInvariant()) { case "text": AddTextToThumbnails = true; break; case "time": AddTimeToThumbnails = true; break; case "number": ShowPhotoNumber = true; break; case "numberandtime": AddNumberAndTimeToThumbnails = true; break; case "racetime": AddRaceTimeToThumbnails = true; break; default: // clear all _addTimeToThumbnails = false; _addTextToThumbnails = false; _showPhotoNumber = false; _addRaceTimeToThumbnails = false; _addNumberAndTimeToThumbnails = false; NotifyPropertyChanged(nameof(AddTimeToThumbnails)); NotifyPropertyChanged(nameof(AddTextToThumbnails)); NotifyPropertyChanged(nameof(ShowPhotoNumber)); NotifyPropertyChanged(nameof(AddRaceTimeToThumbnails)); NotifyPropertyChanged(nameof(AddNumberAndTimeToThumbnails)); break; } NotifyPropertyChanged(nameof(ThumbnailMode)); } } // Image processing progress and status private string _processingStatus = ""; public string ProcessingStatus { get => _processingStatus; set { _processingStatus = value; NotifyPropertyChanged(); } } private int _processedImagesCount = 0; public int ProcessedImagesCount { get => _processedImagesCount; set { _processedImagesCount = value; NotifyPropertyChanged(); } } private int _totalImagesCount = 0; public int TotalImagesCount { get => _totalImagesCount; set { _totalImagesCount = value; NotifyPropertyChanged(); } } private int _progressBarValue = 0; public int ProgressBarValue { get => _progressBarValue; set { _progressBarValue = value; NotifyPropertyChanged(); } } private int _progressBarMaximum = 100; public int ProgressBarMaximum { get => _progressBarMaximum; set { _progressBarMaximum = value; NotifyPropertyChanged(); } } private ConcurrentBag _results = new(); private int _currentAmount = 0; private int _previousAmount = 0; // Atomic counter for processed images — avoids expensive ConcurrentBag.Count enumerations private int _processedAtomic = 0; private System.Threading.Timer? _speedTimer; // Stopwatch used to compute run-wide averages private Stopwatch? _speedWatch; // Recent diffs queue to smooth short-term fluctuations private readonly Queue _recentDiffs = new(); private int _recentWindowSize = 5; // average over last 5 samples (~5s) 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; // Fix paths FixPaths(); // Reset counters ProcessingStatus = "Elaborazione in corso..."; TotalImagesCount = 0; ProcessedImagesCount = 0; SpeedCounter = "-f/s"; ProgressBarValue = 0; ProgressBarMaximum = 100; // 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 ImageCreationStuff.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 { _results = new ConcurrentBag(); _currentAmount = 0; _previousAmount = 0; _processedAtomic = 0; // Start speed timer (sample every second using lightweight atomic reads) _speedWatch = Stopwatch.StartNew(); _recentDiffs.Clear(); _speedTimer = new System.Threading.Timer(UpdateSpeedCounter, null, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1)); var time = await _imageCreationService.CreaCatalogoParallel( imageCreationOptions, _results, OnImageProcessed, token); // AI integration stub: if ExtractNumbers is enabled, simulate or invoke OCR processing if (ExtractNumbers) { try { await RunAiExtractionAsync(token); } catch (OperationCanceledException) { _logger.LogInformation("AI extraction canceled"); } catch (Exception ex) { _logger.LogError(ex, "AI extraction failed"); } } // Compute final averages and show only averages (do not show raw seconds) var finalProcessed = System.Threading.Volatile.Read(ref _processedAtomic); double overallAvg = 0.0; double overallPerMin = 0.0; if (_speedWatch is not null && _speedWatch.Elapsed.TotalSeconds > 0.0) { overallAvg = finalProcessed / _speedWatch.Elapsed.TotalSeconds; overallPerMin = overallAvg * 60.0; } // Compute elapsed time as h m s and show final averages (no raw seconds parentheses) var finalElapsed = _speedWatch?.Elapsed ?? TimeSpan.Zero; int fh = (int)finalElapsed.TotalHours; int fm = finalElapsed.Minutes; int fs = finalElapsed.Seconds; SpeedCounter = $"{fh}h {fm}m {fs}s{Environment.NewLine}media: {overallAvg:0.00} f/s{Environment.NewLine}media: {overallPerMin:0.00} f/m"; _speedTimer?.Dispose(); _speedTimer = null; _speedWatch?.Stop(); _speedWatch = null; } 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; _speedTimer?.Dispose(); _speedTimer = null; } ProcessingStatus = "Finito"; UiEnabled = true; } private void UpdateSpeedCounter(object? state) { try { _previousAmount = _currentAmount; // Read the atomic counter without enumerating the ConcurrentBag _currentAmount = System.Threading.Volatile.Read(ref _processedAtomic); int diff = _currentAmount - _previousAmount; // Protect against negative or spurious diffs if (diff < 0) diff = 0; // Maintain a small sliding window of recent diffs to smooth the display lock (_recentDiffs) { _recentDiffs.Enqueue(diff); if (_recentDiffs.Count > _recentWindowSize) _recentDiffs.Dequeue(); } double avgRecent; lock (_recentDiffs) { avgRecent = _recentDiffs.Count == 0 ? 0.0 : _recentDiffs.Average(); } // Compute overall average (since start) if we have a stopwatch double overall = 0.0; if (_speedWatch is not null && _speedWatch.Elapsed.TotalSeconds >= 1) { var elapsedSeconds = _speedWatch.Elapsed.TotalSeconds; var total = System.Threading.Volatile.Read(ref _processedAtomic); overall = elapsedSeconds > 0 ? total / elapsedSeconds : 0.0; } // Recent per-minute estimate var recentPerMin = avgRecent * 60.0; var overallPerMin = overall * 60.0; // Build a two-line display plus elapsed time: first line shows f/s with overall media and elapsed time, // second line shows recent photos per minute (media) var elapsed = _speedWatch?.Elapsed ?? TimeSpan.Zero; int hours = (int)elapsed.TotalHours; int minutes = elapsed.Minutes; int seconds = elapsed.Seconds; var elapsedStr = $"{hours}h {minutes}m {seconds}s"; SpeedCounter = $"{avgRecent:0.00} f/s (media: {overall:0.00} f/s) - {elapsedStr}{Environment.NewLine}media: {recentPerMin:0.00} f/m"; } catch { // Swallow unlikely errors from timing/queue operations but keep UI responsive } } private void OnImageProcessed(object? sender, Tuple args) { // Increment atomic processed counter once and use its value for all UI updates var processed = System.Threading.Interlocked.Increment(ref _processedAtomic); ProcessedImagesCount = processed; TotalImagesCount = args.Item2; ProgressBarMaximum = args.Item2; ProgressBarValue = processed; ProcessingStatus = args.Item1; } private void FixPaths() { SourcePath = FixPath(SourcePath); DestinationPath = FixPath(DestinationPath); } private string FixPath(string path) { if (string.IsNullOrWhiteSpace(path)) { return string.Empty; } // Trim leading/trailing quotes path = path.Trim().Trim('"'); // Normalize directory separators path = path.Replace('/', System.IO.Path.DirectorySeparatorChar) .Replace('\\', System.IO.Path.DirectorySeparatorChar); // Remove trailing separators then add one back path = path.TrimEnd(System.IO.Path.DirectorySeparatorChar) + System.IO.Path.DirectorySeparatorChar; return path; } 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"); } } // 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) public event EventHandler> ShowMessageRequested; 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, null); } private void LoadSettings(object parameter) { LoadSettingsRequested?.Invoke(this, null); } 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(); } } } }