From 3c722a66dfd55c317c82938acd80d4c39baee2b0 Mon Sep 17 00:00:00 2001 From: MaddoScientisto Date: Thu, 12 Mar 2026 18:48:13 +0100 Subject: [PATCH] feat: Add AI extraction service and related view models - Introduced `IAiExtractionService` and its implementation `AiExtractionService` for processing images and extracting text. - Created `AiResultItem` model to hold results from AI extraction. - Added `ImageProcessingCoordinator` to manage image processing tasks and provide progress updates. - Implemented view models for AI settings, path settings, processing state, race upload settings, and visual settings to support UI binding. - Updated `Program.cs` to register new services and dependencies. - Modified project file to skip MinVer execution during local builds. --- .../DataModelCharacterizationTests.cs | 174 ++++ MaddoShared.Tests/MaddoShared.Tests.csproj | 1 + imagecatalog/AvaloniaViews/AiTabView.axaml | 78 +- imagecatalog/DataModel.cs | 854 ++++++------------ imagecatalog/ImageCatalog 2.csproj | 2 + imagecatalog/Models/AiResultItem.cs | 7 + imagecatalog/Program.cs | 6 +- imagecatalog/Services/AiExtractionService.cs | 132 +++ imagecatalog/Services/IAiExtractionService.cs | 22 + .../Services/IImageProcessingCoordinator.cs | 28 + .../Services/ImageProcessingCoordinator.cs | 127 +++ .../ViewModels/AiSettingsViewModel.cs | 75 ++ .../ViewModels/PathSettingsViewModel.cs | 49 + .../ViewModels/ProcessingStateViewModel.cs | 82 ++ .../ViewModels/RaceUploadSettingsViewModel.cs | 149 +++ .../ViewModels/VisualSettingsViewModel.cs | 312 +++++++ 16 files changed, 1466 insertions(+), 632 deletions(-) create mode 100644 MaddoShared.Tests/DataModelCharacterizationTests.cs create mode 100644 imagecatalog/Models/AiResultItem.cs create mode 100644 imagecatalog/Services/AiExtractionService.cs create mode 100644 imagecatalog/Services/IAiExtractionService.cs create mode 100644 imagecatalog/Services/IImageProcessingCoordinator.cs create mode 100644 imagecatalog/Services/ImageProcessingCoordinator.cs create mode 100644 imagecatalog/ViewModels/AiSettingsViewModel.cs create mode 100644 imagecatalog/ViewModels/PathSettingsViewModel.cs create mode 100644 imagecatalog/ViewModels/ProcessingStateViewModel.cs create mode 100644 imagecatalog/ViewModels/RaceUploadSettingsViewModel.cs create mode 100644 imagecatalog/ViewModels/VisualSettingsViewModel.cs diff --git a/MaddoShared.Tests/DataModelCharacterizationTests.cs b/MaddoShared.Tests/DataModelCharacterizationTests.cs new file mode 100644 index 0000000..468e389 --- /dev/null +++ b/MaddoShared.Tests/DataModelCharacterizationTests.cs @@ -0,0 +1,174 @@ +using System; +using System.Threading.Tasks; +using FluentAssertions; +using ImageCatalog_2; +using ImageCatalog_2.Services; +using MaddoShared; +using Microsoft.Extensions.Logging; +using Moq; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace MaddoShared.Tests; + +[TestClass] +public class DataModelCharacterizationTests +{ + [TestMethod] + public void SelectSourceFolderCommand_RaisesEvent() + { + var model = CreateModel(); + var raised = false; + model.SelectSourceFolderRequested += (_, _) => raised = true; + + model.SelectSourceFolderCommand.Execute(null); + + raised.Should().BeTrue(); + } + + [TestMethod] + public async Task SaveSettingsToFileAsync_DelegatesToSettingsService() + { + var settingsService = new Mock(); + settingsService + .Setup(s => s.SaveSettingsAsync(It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + + var model = CreateModel(settingsService: settingsService.Object); + + await model.SaveSettingsToFileAsync("settings.xml"); + + settingsService.Verify( + s => s.SaveSettingsAsync("settings.xml", model), + Times.Once); + } + + [TestMethod] + public async Task LoadSettingsFromFileAsync_DelegatesToSettingsService() + { + var settingsService = new Mock(); + settingsService + .Setup(s => s.LoadSettingsAsync(It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + + var model = CreateModel(settingsService: settingsService.Object); + + await model.LoadSettingsFromFileAsync("settings.xml"); + + settingsService.Verify( + s => s.LoadSettingsAsync("settings.xml", model), + Times.Once); + } + + [TestMethod] + public void ThumbnailOptionIndex_UpdatesAuthoritativeThumbnailState() + { + var model = CreateModel(); + + model.ThumbnailOptionIndex = (int)DataModel.ThumbnailOptionEnum.RaceTime; + + model.ThumbnailOption.Should().Be(DataModel.ThumbnailOptionEnum.RaceTime); + model.AddRaceTimeToThumbnails.Should().BeTrue(); + model.ThumbnailMode.Should().Be("RaceTime"); + } + + [TestMethod] + public void ProcessingChildChange_RaisesDataModelPropertyChanged() + { + var model = CreateModel(); + string? changed = null; + model.PropertyChanged += (_, args) => changed = args.PropertyName; + + model.Processing.SpeedCounter = "12.00 f/s"; + + changed.Should().Be(nameof(DataModel.SpeedCounter)); + model.SpeedCounter.Should().Be("12.00 f/s"); + } + + [TestMethod] + public void PathsNormalize_UpdatesFlattenedSourceAndDestination() + { + var model = CreateModel(); + model.SourcePath = "\"C:/input\""; + model.DestinationPath = "C:/output"; + + model.Paths.NormalizePaths(); + + model.SourcePath.Should().Be($"C:{System.IO.Path.DirectorySeparatorChar}input{System.IO.Path.DirectorySeparatorChar}"); + model.DestinationPath.Should().Be($"C:{System.IO.Path.DirectorySeparatorChar}output{System.IO.Path.DirectorySeparatorChar}"); + } + + [TestMethod] + public void AiChildChange_RaisesDataModelPropertyChanged() + { + var model = CreateModel(); + string? changed = null; + model.PropertyChanged += (_, args) => changed = args.PropertyName; + + model.Ai.ModelsFolderPath = "K:/models"; + + changed.Should().Be(nameof(DataModel.ModelsFolderPath)); + model.ModelsFolderPath.Should().Be("K:/models"); + } + + [TestMethod] + public void RaceUploadChildChange_RaisesDataModelPropertyChanged() + { + var model = CreateModel(); + string? changed = null; + model.PropertyChanged += (_, args) => changed = args.PropertyName; + + model.RaceUpload.ApiLogin = "admin"; + + changed.Should().Be(nameof(DataModel.ApiLogin)); + model.ApiLogin.Should().Be("admin"); + } + + [TestMethod] + public void VisualChildChange_RaisesDataModelPropertyChanged() + { + var model = CreateModel(); + string? changed = null; + model.PropertyChanged += (_, args) => changed = args.PropertyName; + + model.Visual.FontSize = 42; + + changed.Should().Be(nameof(DataModel.FontSize)); + model.FontSize.Should().Be(42); + } + + private static DataModel CreateModel( + ISettingsService? settingsService = null, + ITestService? testService = null) + { + var mapper = new Mock().Object; + var picSettings = new PicSettings(); + + var imageCreator = new Mock(); + imageCreator + .Setup(x => x.CreateImageAsync(It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + + var imageCreationService = new ImageCreationService( + new Mock>().Object, + picSettings, + imageCreator.Object); + + var imageProcessingCoordinator = new ImageProcessingCoordinator( + imageCreationService, + new Mock>().Object); + + var aiExtractionService = new AiExtractionService( + new Mock>().Object); + + return new DataModel( + testService ?? new Mock().Object, + settingsService ?? new Mock().Object, + imageCreationService, + aiExtractionService, + imageProcessingCoordinator, + picSettings, + mapper, + new Mock>().Object, + versionProvider: null); + } +} diff --git a/MaddoShared.Tests/MaddoShared.Tests.csproj b/MaddoShared.Tests/MaddoShared.Tests.csproj index b2f98b7..911c340 100644 --- a/MaddoShared.Tests/MaddoShared.Tests.csproj +++ b/MaddoShared.Tests/MaddoShared.Tests.csproj @@ -22,6 +22,7 @@ + diff --git a/imagecatalog/AvaloniaViews/AiTabView.axaml b/imagecatalog/AvaloniaViews/AiTabView.axaml index a5f6971..8b125d3 100644 --- a/imagecatalog/AvaloniaViews/AiTabView.axaml +++ b/imagecatalog/AvaloniaViews/AiTabView.axaml @@ -2,39 +2,53 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:avaloniaDataGrid="clr-namespace:Avalonia.Controls;assembly=Avalonia.Controls.DataGrid" x:Class="ImageCatalog_2.AvaloniaViews.AiTabView"> - - - - + + + + + - - - - -