diff --git a/.vscode/launch.json b/.vscode/launch.json index afbb38c..0af4af3 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -11,17 +11,6 @@ "cwd": "${workspaceFolder:Catalog}/imagecatalog", "stopAtEntry": false, "console": "internalConsole" - }, - { - "name": "CatalogLite Avalonia", - "type": "coreclr", - "request": "launch", - "preLaunchTask": "build CatalogLite Avalonia", - "program": "${workspaceFolder:Catalog}/CatalogLite/bin/Debug/net10.0/CatalogLite.exe", - "args": [], - "cwd": "${workspaceFolder:Catalog}/CatalogLite", - "stopAtEntry": false, - "console": "internalConsole" } ] } \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 791bec6..22c598b 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -13,19 +13,6 @@ ], "problemMatcher": "$msCompile", "group": "build" - }, - { - "label": "build CatalogLite Avalonia", - "type": "process", - "command": "dotnet", - "args": [ - "build", - "${workspaceFolder:Catalog}/CatalogLite/CatalogLite.csproj", - "--configuration", - "Debug" - ], - "problemMatcher": "$msCompile", - "group": "build" } ] } \ No newline at end of file diff --git a/CatalogLite/Assets/Config.xml b/CatalogLite/Assets/Config.xml deleted file mode 100644 index 9b0fa7d..0000000 --- a/CatalogLite/Assets/Config.xml +++ /dev/null @@ -1,227 +0,0 @@ - - - - MiniatureModalita - Text - - - DirSorgente - - - - DirDestinazione - - - - MiniatureCrea - True - - - MiniatureSuffisso - tn_ - - - MiniatureAltezza - 350 - - - MiniatureLarghezza - 350 - - - FontDimensioneMiniatura - 20 - - - CompressioneJpegMiniatura - 60 - - - MiniatureAddOrario - False - - - NomeMiniatura - False - - - MiniatureAddScritta - True - - - TempoSmall - False - - - NumTempoSmall - False - - - FotoCodice - - - - FotoAltezza - 2560 - - - FotoLarghezza - 2560 - - - FotoDimOriginali - False - - - CompressioneJpeg - 90 - - - FontDimensione - 50 - - - FontNome - Verdana - - - FontBold - True - - - ColoreTestoRGB - #FA7B0A - - - TestoTesto - - - - TestoVerticale - - - - TestoTrasparente - 0 - - - TestoMargine - 8 - - - TestoPosizione - Basso - - - TestoAllineamento - Centro - - - GrandezzaVerticale - 18 - - - MargineVerticale - 6 - - - MarchioAltezza - 250 - - - MarchioLarghezza - 250 - - - MarchioMargine - 130 - - - MarchioAllOrizzontale - Destra - - - MarchioAllVerticale - Alto - - - MarchioTrasparenza - 100 - - - MarchioAggiungi - True - - - ColoreTrasparente - #FFFFFF - - - UsaColoreTrasparente - True - - - ImageLibrary - ImageSharp - - - GeneraleForzaJpg - True - - - GeneraleRotazioneAutomatica - True - - - DirSottoDirectory - True - - - TempoGara - False - - - Orario - False - - - EtichettaOrario - - - - DataFoto - False - - - NumeroFoto - False - - - GeneraleSovrascriviFile - False - - - DirDividiNumFile - 300 - - - DirDividiSuffisso - - - - DirDividiNumCifre - 2 - - - ChunkSize - 0 - - - ThreadsCount - 0 - - - DataPartenza - 17/02/2026 09:35:25 - - \ No newline at end of file diff --git a/CatalogLite/Assets/Logo_RUS_ETS_tricolore_OK.png b/CatalogLite/Assets/Logo_RUS_ETS_tricolore_OK.png deleted file mode 100644 index 1d4d8bf..0000000 Binary files a/CatalogLite/Assets/Logo_RUS_ETS_tricolore_OK.png and /dev/null differ diff --git a/CatalogLite/Assets/testConfig.xml b/CatalogLite/Assets/testConfig.xml deleted file mode 100644 index cf735d0..0000000 --- a/CatalogLite/Assets/testConfig.xml +++ /dev/null @@ -1,336 +0,0 @@ - - - - MiniatureModalita - RaceTime - - - MiniatureCrea - True - - - MiniatureSuffisso - tn_ - - - MiniatureAltezza - 350 - - - MiniatureLarghezza - 350 - - - FontDimensioneMiniatura - 48 - - - CompressioneJpegMiniatura - 25 - - - MiniatureAddOrario - False - - - NomeMiniatura - False - - - MiniatureAddScritta - False - - - TempoSmall - True - - - NumTempoSmall - False - - - FotoCodice - - - - FotoAltezza - 2560 - - - FotoLarghezza - 2560 - - - FotoDimOriginali - False - - - CompressioneJpeg - 90 - - - FontDimensione - 22 - - - FontNome - Verdana - - - FontBold - True - - - ColoreTestoRGB - #FEC005 - - - TestoTesto - MARATONINA DI VINCI -1 FEBBRAIO 2026 - - - TestoVerticale - MARATONINA DI VINCI -1 FEBBRAIO 2026 - - - TestoTrasparente - 0 - - - TestoMargine - 8 - - - TestoPosizione - Basso - - - TestoAllineamento - Centro - - - GrandezzaVerticale - 18 - - - MargineVerticale - 6 - - - MarchioFile - K:\various\catalogtest\Logo.jpg - - - MarchioAltezza - 470 - - - MarchioLarghezza - 470 - - - MarchioMargine - 350 - - - MarchioAllOrizzontale - Destra - - - MarchioAllVerticale - Alto - - - MarchioTrasparenza - 100 - - - MarchioAggiungi - True - - - ColoreTrasparente - #FFFFFF - - - UsaColoreTrasparente - True - - - ImageLibrary - System.Graphics - - - GeneraleForzaJpg - True - - - GeneraleRotazioneAutomatica - True - - - DirSottoDirectory - True - - - TempoGara - True - - - Orario - False - - - EtichettaOrario - TEMPO : - - - DataFoto - False - - - NumeroFoto - False - - - GeneraleSovrascriviFile - True - - - DirDividiNumFile - 300 - - - DirDividiSuffisso - - - - DirDividiNumCifre - 2 - - - ChunkSize - 200 - - - ThreadsCount - 10 - - - DataPartenza - 01/02/2026 20:30:48 - - - AI_EstraiNumeri - True - - - AI_CartellaModelli - K:\vs\AIFotoONLUS\models\\ - - - AI_PercorsoCsv - K:\various\catalogtest\aioutput\test2.csv - - - AI_UsaGpuNumeri - True - - - AI_IncludiThumbnailNumeri - False - - - AI_LivelloCaricoNumeri - 3 - - - AI_FaceExecutablePath - K:\various\regalamiunsorriso\bin\Face_Recognition_Windows\ - - - AI_FaceOutputFolderPath - K:\various\catalogtest\aioutput\ - - - AI_FaceRecursive - True - - - AI_FaceIncludeThumbnails - False - - - AI_FaceParallelism - 3 - - - AI_FaceMinSize - 35 - - - AI_FaceUpsample - False - - - AI_FaceMatcherExecutablePath - K:\various\regalamiunsorriso\bin\Face_Recognition_Windows\face_matcher.exe - - - AI_FaceMatcherTolerance - 0,75 - - - RaceUpload_Login - - - - RaceUpload_Password - - - - RaceUpload_Description - - - - RaceUpload_TipoGaraId - 1 - - - RaceUpload_StartDate - 12/03/2026 00:00:00 - - - RaceUpload_EndDate - 12/03/2026 00:00:00 - - - RaceUpload_PathBase - - - - RaceUpload_Localita - - - - RaceUpload_EventoInLinea - 0 - - - RaceUpload_TipoIndex - 1 - - - RaceUpload_FreeEvent - 0 - - - RaceUpload_LastRaceId - - - - RaceUpload_RemoteProcessedBasePath - - - \ No newline at end of file diff --git a/CatalogLite/AsyncCommand.cs b/CatalogLite/AsyncCommand.cs index d030d67..fcc9fe7 100644 --- a/CatalogLite/AsyncCommand.cs +++ b/CatalogLite/AsyncCommand.cs @@ -1,4 +1,3 @@ -using Avalonia.Threading; using System.Windows.Input; namespace CatalogLite; @@ -39,14 +38,5 @@ public sealed class AsyncCommand : ICommand } } - public void RaiseCanExecuteChanged() - { - if (Dispatcher.UIThread.CheckAccess()) - { - CanExecuteChanged?.Invoke(this, EventArgs.Empty); - return; - } - - Dispatcher.UIThread.Post(() => CanExecuteChanged?.Invoke(this, EventArgs.Empty)); - } + public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty); } \ No newline at end of file diff --git a/CatalogLite/CatalogConfigurationLoader.cs b/CatalogLite/CatalogConfigurationLoader.cs index a4e4a02..f7652c9 100644 --- a/CatalogLite/CatalogConfigurationLoader.cs +++ b/CatalogLite/CatalogConfigurationLoader.cs @@ -1,5 +1,4 @@ using System.Globalization; -using System.Reflection; using System.Xml.Linq; using MaddoShared; using SixLabors.ImageSharp; @@ -9,9 +8,6 @@ namespace CatalogLite; public sealed class CatalogConfigurationLoader { - private const string EmbeddedConfigResourceName = "CatalogLite.Assets.Config.xml"; - private const string EmbeddedLogoResourceName = "CatalogLite.Assets.Logo_RUS_ETS_tricolore_OK.png"; - public CatalogLiteConfiguration Load(string filePath, PicSettings picSettings) { if (string.IsNullOrWhiteSpace(filePath)) @@ -25,7 +21,7 @@ public sealed class CatalogConfigurationLoader } var values = ConfigurationValues.Load(filePath); - ApplyPicSettings(values, picSettings); + ApplyPicSettings(values, picSettings); var sourcePath = LiteCatalogViewModel.NormalizeDirectoryPath(values.GetString("DirSorgente")); var destinationPath = LiteCatalogViewModel.NormalizeDirectoryPath(values.GetString("DirDestinazione")); @@ -45,32 +41,6 @@ public sealed class CatalogConfigurationLoader }; } - public CatalogLiteConfiguration LoadEmbedded(PicSettings picSettings) - { - using var configStream = OpenEmbeddedResource(EmbeddedConfigResourceName); - var values = ConfigurationValues.Load(configStream); - ApplyPicSettings(values, picSettings); - picSettings.LogoData = ReadAllBytes(OpenEmbeddedResource(EmbeddedLogoResourceName)); - picSettings.LogoNomeFile = string.Empty; - - var sourcePath = LiteCatalogViewModel.NormalizeDirectoryPath(values.GetString("DirSorgente")); - var destinationPath = LiteCatalogViewModel.NormalizeDirectoryPath(values.GetString("DirDestinazione")); - - picSettings.DirectorySorgente = sourcePath; - picSettings.DirectoryDestinazione = destinationPath; - picSettings.DestDir = string.IsNullOrWhiteSpace(destinationPath) - ? new DirectoryInfo(Environment.CurrentDirectory) - : new DirectoryInfo(destinationPath); - - return new CatalogLiteConfiguration - { - FilePath = "Configurazione incorporata", - SourcePath = sourcePath, - DestinationPath = destinationPath, - Options = BuildOptions(values, sourcePath, destinationPath) - }; - } - public static ImageCreationService.Options CloneOptions(ImageCreationService.Options options, string sourcePath, string destinationPath) { return new ImageCreationService.Options @@ -180,22 +150,6 @@ public sealed class CatalogConfigurationLoader return string.Equals(values.GetString("MiniatureModalita"), mode, StringComparison.OrdinalIgnoreCase); } - private static Stream OpenEmbeddedResource(string resourceName) - { - var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName); - return stream ?? throw new InvalidOperationException($"Risorsa incorporata non trovata: {resourceName}"); - } - - private static byte[] ReadAllBytes(Stream stream) - { - using (stream) - { - using var memoryStream = new MemoryStream(); - stream.CopyTo(memoryStream); - return memoryStream.ToArray(); - } - } - private static Rgba32 ParseColor(string value, Rgba32 fallback) { if (string.IsNullOrWhiteSpace(value)) @@ -239,13 +193,7 @@ public sealed class CatalogConfigurationLoader public static ConfigurationValues Load(string filePath) { - using var stream = File.OpenRead(filePath); - return Load(stream); - } - - public static ConfigurationValues Load(Stream stream) - { - var document = XDocument.Load(stream); + var document = XDocument.Load(filePath); var values = document .Descendants("Setup") .Where(element => element.Element("Nome") is not null) diff --git a/CatalogLite/CatalogLite.csproj b/CatalogLite/CatalogLite.csproj index be2b0bc..54121b7 100644 --- a/CatalogLite/CatalogLite.csproj +++ b/CatalogLite/CatalogLite.csproj @@ -25,11 +25,6 @@ - - - - - diff --git a/CatalogLite/LiteCatalogViewModel.cs b/CatalogLite/LiteCatalogViewModel.cs index 656e4d2..0a4c8c6 100644 --- a/CatalogLite/LiteCatalogViewModel.cs +++ b/CatalogLite/LiteCatalogViewModel.cs @@ -12,9 +12,10 @@ public sealed class LiteCatalogViewModel : ViewModelBase private readonly ILogger _logger; private CatalogLiteConfiguration? _configuration; private CancellationTokenSource? _processingTokenSource; + private string _configurationPath = string.Empty; private string _sourcePath = string.Empty; private string _destinationPath = string.Empty; - private string _processingStatus = "Caricamento configurazione incorporata..."; + private string _processingStatus = "Carica una configurazione XML."; private string _speedCounter = "-"; private int _processedImagesCount; private int _totalImagesCount; @@ -35,23 +36,36 @@ public sealed class LiteCatalogViewModel : ViewModelBase _imageProcessingCoordinator = imageProcessingCoordinator; _logger = logger; + LoadConfigurationCommand = new AsyncCommand(RequestLoadConfigurationAsync, () => !IsProcessing); SelectSourceFolderCommand = new AsyncCommand(RequestSourceFolderAsync, () => !IsProcessing); SelectDestinationFolderCommand = new AsyncCommand(RequestDestinationFolderAsync, () => !IsProcessing); StartProcessingCommand = new AsyncCommand(StartProcessingAsync, CanStartProcessing); StopProcessingCommand = new AsyncCommand(StopProcessingAsync, () => IsProcessing); } + public event EventHandler? LoadConfigurationRequested; public event EventHandler? SelectSourceFolderRequested; public event EventHandler? SelectDestinationFolderRequested; public event EventHandler? ShowMessageRequested; public Action? UiInvoker { get; set; } + public AsyncCommand LoadConfigurationCommand { get; } public AsyncCommand SelectSourceFolderCommand { get; } public AsyncCommand SelectDestinationFolderCommand { get; } public AsyncCommand StartProcessingCommand { get; } public AsyncCommand StopProcessingCommand { get; } + public string ConfigurationPath + { + get => _configurationPath; + private set + { + _configurationPath = value; + NotifyPropertyChanged(); + } + } + public string SourcePath { get => _sourcePath; @@ -63,26 +77,6 @@ public sealed class LiteCatalogViewModel : ViewModelBase } } - public string HorizontalText - { - get => _picSettings.TestoFirmaStart ?? string.Empty; - set - { - _picSettings.TestoFirmaStart = value ?? string.Empty; - NotifyPropertyChanged(); - } - } - - public string VerticalText - { - get => _picSettings.TestoFirmaStartV ?? string.Empty; - set - { - _picSettings.TestoFirmaStartV = value ?? string.Empty; - NotifyPropertyChanged(); - } - } - public string DestinationPath { get => _destinationPath; @@ -165,24 +159,25 @@ public sealed class LiteCatalogViewModel : ViewModelBase } } - public void LoadEmbeddedConfiguration() + public async Task LoadConfigurationFromFileAsync(string filePath) { try { - var configuration = _configurationLoader.LoadEmbedded(_picSettings); + var configuration = await Task.Run(() => _configurationLoader.Load(filePath, _picSettings)).ConfigureAwait(false); - _configuration = configuration; - SourcePath = configuration.SourcePath; - DestinationPath = configuration.DestinationPath; - NotifyPropertyChanged(nameof(HorizontalText)); - NotifyPropertyChanged(nameof(VerticalText)); - ResetProgress("Configurazione incorporata caricata."); + RunOnUiThread(() => + { + _configuration = configuration; + ConfigurationPath = configuration.FilePath; + SourcePath = configuration.SourcePath; + DestinationPath = configuration.DestinationPath; + ResetProgress("Configurazione caricata."); + }); } catch (Exception ex) { - _logger.LogError(ex, "Errore durante il caricamento della configurazione incorporata"); - ProcessingStatus = "Errore caricamento configurazione incorporata."; - ShowMessage("Configurazione", $"Impossibile caricare la configurazione incorporata: {ex.GetBaseException().Message}"); + _logger.LogError(ex, "Errore durante il caricamento della configurazione"); + ShowMessage("Configurazione", $"Impossibile caricare la configurazione: {ex.GetBaseException().Message}"); } } @@ -197,6 +192,12 @@ public sealed class LiteCatalogViewModel : ViewModelBase return trimmed + Path.DirectorySeparatorChar; } + private Task RequestLoadConfigurationAsync() + { + LoadConfigurationRequested?.Invoke(this, EventArgs.Empty); + return Task.CompletedTask; + } + private Task RequestSourceFolderAsync() { SelectSourceFolderRequested?.Invoke(this, EventArgs.Empty); @@ -221,7 +222,7 @@ public sealed class LiteCatalogViewModel : ViewModelBase { if (_configuration is null) { - ShowMessage("Configurazione", "La configurazione incorporata non e' disponibile."); + ShowMessage("Configurazione", "Carica prima una configurazione XML."); return; } @@ -339,6 +340,7 @@ public sealed class LiteCatalogViewModel : ViewModelBase private void RaiseCommandStates() { + LoadConfigurationCommand.RaiseCanExecuteChanged(); SelectSourceFolderCommand.RaiseCanExecuteChanged(); SelectDestinationFolderCommand.RaiseCanExecuteChanged(); StartProcessingCommand.RaiseCanExecuteChanged(); diff --git a/CatalogLite/MainWindow.axaml b/CatalogLite/MainWindow.axaml index 3067595..48118bd 100644 --- a/CatalogLite/MainWindow.axaml +++ b/CatalogLite/MainWindow.axaml @@ -5,11 +5,24 @@ x:CompileBindings="False" Title="Catalog Lite" Width="740" - Height="560" + Height="380" MinWidth="640" - MinHeight="500"> - - + MinHeight="340"> + + + + + + + - - + - - - - - - - - - - - - - - Dispatcher.UIThread.Invoke(action); + _viewModel.LoadConfigurationRequested += OnLoadConfigurationRequested; _viewModel.SelectSourceFolderRequested += OnSelectSourceFolderRequested; _viewModel.SelectDestinationFolderRequested += OnSelectDestinationFolderRequested; _viewModel.ShowMessageRequested += OnShowMessageRequested; - _viewModel.LoadEmbeddedConfiguration(); + } + + private async void OnLoadConfigurationRequested(object? sender, EventArgs e) + { + var files = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions + { + Title = "Carica configurazione XML", + FileTypeFilter = [new FilePickerFileType("XML") { Patterns = ["*.xml"] }] + }); + + if (files.Count > 0) + { + await _viewModel.LoadConfigurationFromFileAsync(files[0].Path.LocalPath); + } } private async void OnSelectSourceFolderRequested(object? sender, EventArgs e) @@ -55,16 +67,6 @@ public partial class MainWindow : Window } } - private void OpenSourcePath_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) - { - OpenInExplorer(_viewModel.SourcePath); - } - - private void OpenDestinationPath_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) - { - OpenInExplorer(_viewModel.DestinationPath); - } - private async void OnShowMessageRequested(object? sender, LiteMessageEventArgs e) { await ShowMessageDialogAsync(e.Title, e.Message); @@ -108,46 +110,4 @@ public partial class MainWindow : Window closeButton.Click += (_, _) => dialog.Close(); await dialog.ShowDialog(this); } - - private static void OpenInExplorer(string? path) - { - if (string.IsNullOrWhiteSpace(path)) - { - return; - } - - var normalizedPath = path.Trim().Trim('"'); - - try - { - if (File.Exists(normalizedPath)) - { - Process.Start("explorer.exe", $"/select,\"{normalizedPath}\""); - return; - } - - if (Directory.Exists(normalizedPath)) - { - Process.Start(new ProcessStartInfo - { - FileName = normalizedPath, - UseShellExecute = true - }); - return; - } - - var containingDirectory = Path.GetDirectoryName(normalizedPath); - if (!string.IsNullOrWhiteSpace(containingDirectory) && Directory.Exists(containingDirectory)) - { - Process.Start(new ProcessStartInfo - { - FileName = containingDirectory, - UseShellExecute = true - }); - } - } - catch - { - } - } } \ No newline at end of file diff --git a/MaddoShared.Tests/DataModelCharacterizationTests.cs b/MaddoShared.Tests/DataModelCharacterizationTests.cs index 6e8c82c..ac09cb8 100644 --- a/MaddoShared.Tests/DataModelCharacterizationTests.cs +++ b/MaddoShared.Tests/DataModelCharacterizationTests.cs @@ -283,120 +283,9 @@ public class DataModelCharacterizationTests output.LogFilePath.ShouldBe(@"C:\out\encoder_log_20260509_143045_04_APRILE_gara.txt"); } - [TestMethod] - public void FaceUploadPath_ValidatesExpectedRelativePathShape() - { - DataModel.IsValidFaceUploadPath("2026/05.MAGGIO/EMPOLI").ShouldBeTrue(); - DataModel.IsValidFaceUploadPath("2026/5.MAGGIO/EMPOLI").ShouldBeFalse(); - DataModel.IsValidFaceUploadPath("2026/00.MAGGIO/EMPOLI").ShouldBeFalse(); - DataModel.IsValidFaceUploadPath("2026/13.MAGGIO/EMPOLI").ShouldBeFalse(); - DataModel.IsValidFaceUploadPath("2026/05.MAGGIO").ShouldBeFalse(); - - DataModel.CombineRemoteUploadPath("/mnt/da1/foto/", "2026/05.MAGGIO/EMPOLI") - .ShouldBe("/mnt/da1/foto/2026/05.MAGGIO/EMPOLI"); - } - - [TestMethod] - public void FaceUploadCommand_IsEnabledOnlyForValidUploadPath() - { - var model = CreateModel(); - - model.UploadFaceEncoderOutputCommand.CanExecute(null).ShouldBeFalse(); - - model.FaceUploadPath = "2026/05.MAGGIO/EMPOLI"; - model.UploadFaceEncoderOutputCommand.CanExecute(null).ShouldBeTrue(); - - model.FaceUploadPath = "2026/5.MAGGIO/EMPOLI"; - model.UploadFaceEncoderOutputCommand.CanExecute(null).ShouldBeFalse(); - } - - [TestMethod] - public void FaceSshPreferences_AreStoredInUserPreferences() - { - using var tempDirectory = new TemporaryDirectory(); - var preferencesFile = Path.Combine(tempDirectory.Path, "userprefs.xml"); - var preferenceService = new PickerPreferenceService(new ImageCatalog.ParametriSetup(preferencesFile)); - - var model = CreateModel(pickerPreferenceService: preferenceService); - model.FaceSshUsername = "ssh-user"; - model.FaceSshPassword = "ssh-password"; - model.FaceSshAddress = "upload.example.org"; - model.FaceSshPort = "2222"; - model.FaceSshPathA = "/mnt/da1/foto/"; - model.FaceSshPathB = "/mnt/nas12/foto/"; - model.FaceUploadDryRun = true; - - var reloadedPreferenceService = new PickerPreferenceService(new ImageCatalog.ParametriSetup(preferencesFile)); - var reloaded = CreateModel(pickerPreferenceService: reloadedPreferenceService); - - reloaded.FaceSshUsername.ShouldBe("ssh-user"); - reloaded.FaceSshPassword.ShouldBe("ssh-password"); - reloaded.FaceSshAddress.ShouldBe("upload.example.org"); - reloaded.FaceSshPort.ShouldBe("2222"); - reloaded.FaceSshPathA.ShouldBe("/mnt/da1/foto/"); - reloaded.FaceSshPathB.ShouldBe("/mnt/nas12/foto/"); - reloaded.FaceUploadDryRun.ShouldBeTrue(); - } - - [TestMethod] - public void ResolveLatestFaceUploadSourceFile_UsesLatestPklForCurrentRace() - { - using var tempDirectory = new TemporaryDirectory(); - var outputFolder = Path.Combine(tempDirectory.Path, "out"); - var currentRaceFolder = Path.Combine(tempDirectory.Path, "04 APRILE gara"); - Directory.CreateDirectory(outputFolder); - Directory.CreateDirectory(currentRaceFolder); - - var olderCurrentRace = Path.Combine(outputFolder, "face_encodings_20260509_143045_04_APRILE_gara.pkl"); - var newerCurrentRace = Path.Combine(outputFolder, "face_encodings_20260509_153045_04_APRILE_gara.pkl"); - var otherRace = Path.Combine(outputFolder, "face_encodings_20260509_163045_05_MAGGIO_gara.pkl"); - - File.WriteAllText(olderCurrentRace, "old"); - File.WriteAllText(newerCurrentRace, "new"); - File.WriteAllText(otherRace, "other"); - - File.SetLastWriteTimeUtc(olderCurrentRace, new DateTime(2026, 5, 9, 14, 30, 45, DateTimeKind.Utc)); - File.SetLastWriteTimeUtc(newerCurrentRace, new DateTime(2026, 5, 9, 15, 30, 45, DateTimeKind.Utc)); - File.SetLastWriteTimeUtc(otherRace, new DateTime(2026, 5, 9, 16, 30, 45, DateTimeKind.Utc)); - - var selected = DataModel.ResolveLatestFaceUploadSourceFile(outputFolder, currentRaceFolder); - - selected.ShouldBe(newerCurrentRace); - } - - [TestMethod] - public async Task SettingsService_PersistsFaceUploadPathButNotSshPreferences() - { - using var tempDirectory = new TemporaryDirectory(); - var settingsFile = Path.Combine(tempDirectory.Path, "settings.xml"); - var userPreferencesFile = Path.Combine(tempDirectory.Path, "userprefs.xml"); - var preferenceService = new PickerPreferenceService(new ImageCatalog.ParametriSetup(userPreferencesFile)); - var settingsService = new SettingsService( - new ImageCatalog.ParametriSetup(Path.Combine(tempDirectory.Path, "unused.xml")), - Substitute.For>()); - var model = CreateModel(settingsService: settingsService, pickerPreferenceService: preferenceService); - - model.FaceUploadPath = "2026/05.MAGGIO/EMPOLI"; - model.FaceSshUsername = "ssh-user"; - - await settingsService.SaveSettingsAsync(settingsFile, model); - - var xml = File.ReadAllText(settingsFile); - xml.ShouldContain("AI_FaceUploadPath"); - xml.ShouldContain("2026/05.MAGGIO/EMPOLI"); - xml.ShouldNotContain("AI_FaceUploadDryRun"); - xml.ShouldNotContain("FaceAI.Ssh"); - xml.ShouldNotContain("ssh-user"); - - var loaded = CreateModel(settingsService: settingsService); - await settingsService.LoadSettingsAsync(settingsFile, loaded); - loaded.FaceUploadPath.ShouldBe("2026/05.MAGGIO/EMPOLI"); - } - private static DataModel CreateModel( ISettingsService? settingsService = null, - ITestService? testService = null, - PickerPreferenceService? pickerPreferenceService = null) + ITestService? testService = null) { var mapper = Substitute.For(); var picSettings = new PicSettings(); @@ -427,8 +316,7 @@ public class DataModelCharacterizationTests picSettings, mapper, Substitute.For>(), - versionProvider: null, - pickerPreferenceService: pickerPreferenceService); + versionProvider: null); } private static string CreateFaceEncoderExecutable(string rootPath, string variant) diff --git a/MaddoShared.Tests/Test1.cs b/MaddoShared.Tests/Test1.cs index d8dc33b..da8e098 100644 --- a/MaddoShared.Tests/Test1.cs +++ b/MaddoShared.Tests/Test1.cs @@ -1,7 +1,6 @@ using ImageCatalog_2.Services; using ImageCatalog_2.Models; using Shouldly; -using System.Text; namespace MaddoShared.Tests; @@ -9,7 +8,7 @@ namespace MaddoShared.Tests; public sealed class AiExtractionServiceCsvTests { [TestMethod] - public void WriteCsvOutput_UsesLegacyCompatibleQuotingAndSkipsEmptyResults() + public void WriteCsvOutput_UsesLegacyCompatibleHeaderAndFilenameColumn() { using var tempDir = new TempDirectory(); var csvPath = Path.Combine(tempDir.Path, "ocr.csv"); @@ -17,35 +16,15 @@ public sealed class AiExtractionServiceCsvTests AiExtractionService.WriteCsvOutput( csvPath, [ - new AiResultItem { Path = @"C:\images\IMG_7146.JPG", Text = "43" }, - new AiResultItem { Path = @"C:\images\IMG_7207.JPG", Text = "43,84,61" }, - new AiResultItem { Path = @"C:\images\IMG_7208.JPG", Text = string.Empty }, - new AiResultItem { Path = @"C:\images\IMG_7209.JPG", Text = " " }, - new AiResultItem { Path = @"C:\images\IMG_7210.JPG", Text = "a\"b,c" } + new AiResultItem { Path = @"C:\images\IMG_7146.JPG", Text = "43,84,61" }, + new AiResultItem { Path = @"C:\images\IMG_7207.JPG", Text = "a\"b" } ]); - var csv = File.ReadAllText(csvPath, Encoding.UTF8); + var lines = File.ReadAllLines(csvPath); - csv.ShouldBe("filename,text\r" - + "IMG_7146.JPG,43\r" - + "IMG_7207.JPG,\"43,84,61\"\r" - + "IMG_7210.JPG,\"a\"\"b,c\"\r"); - } - - [TestMethod] - public void WriteCsvOutput_WritesUtf8WithoutBom() - { - using var tempDir = new TempDirectory(); - var csvPath = Path.Combine(tempDir.Path, "ocr.csv"); - - AiExtractionService.WriteCsvOutput( - csvPath, - [new AiResultItem { Path = @"C:\images\IMG_7146.JPG", Text = "43" }]); - - var bytes = File.ReadAllBytes(csvPath); - - bytes[.."filename,text\r".Length].ShouldBe(Encoding.ASCII.GetBytes("filename,text\r")); - bytes[..3].ShouldNotBe(new byte[] { 0xEF, 0xBB, 0xBF }); + lines[0].ShouldBe("filename,text"); + lines[1].ShouldBe("\"IMG_7146.JPG\",\"43,84,61\""); + lines[2].ShouldBe("\"IMG_7207.JPG\",\"a\"\"b\""); } private sealed class TempDirectory : IDisposable diff --git a/MaddoShared/ImageCreationStuff.cs b/MaddoShared/ImageCreationStuff.cs index 9622b7d..7d22457 100644 --- a/MaddoShared/ImageCreationStuff.cs +++ b/MaddoShared/ImageCreationStuff.cs @@ -71,8 +71,8 @@ namespace MaddoShared int threads = options.MaxThreads; // Load logo once as raw bytes (cross-platform). byte[] is safe to share across threads. - byte[]? logoBytes = picSettings.LogoData; - if (logoBytes is null && picSettings.LogoAggiungi && File.Exists(picSettings.LogoNomeFile)) + byte[]? logoBytes = null; + if (picSettings.LogoAggiungi && File.Exists(picSettings.LogoNomeFile)) { logoBytes = File.ReadAllBytes(picSettings.LogoNomeFile); } diff --git a/MaddoShared/PicSettings.cs b/MaddoShared/PicSettings.cs index 62acc9d..0623a7f 100644 --- a/MaddoShared/PicSettings.cs +++ b/MaddoShared/PicSettings.cs @@ -10,7 +10,6 @@ public class PicSettings public string DirectoryDestinazione { get; set; } public string TestoFirmaStart { get; set; } public string TestoFirmaStartV { get; set; } - public byte[]? LogoData { get; set; } public DateTime DataPartenza { get; set; } public string TestoOrario { get; set; } public int DimStandard { get; set; } diff --git a/imagecatalog/AvaloniaMainWindow.axaml b/imagecatalog/AvaloniaMainWindow.axaml index 3b058a0..c01f37f 100644 --- a/imagecatalog/AvaloniaMainWindow.axaml +++ b/imagecatalog/AvaloniaMainWindow.axaml @@ -88,7 +88,7 @@ - + diff --git a/imagecatalog/AvaloniaMainWindow.axaml.cs b/imagecatalog/AvaloniaMainWindow.axaml.cs index 20e5fb2..6740d6f 100644 --- a/imagecatalog/AvaloniaMainWindow.axaml.cs +++ b/imagecatalog/AvaloniaMainWindow.axaml.cs @@ -273,11 +273,10 @@ public partial class AvaloniaMainWindow : Window var dialog = new Window { Title = title, - Width = 960, - Height = 560, + Width = 480, CanResize = false, WindowStartupLocation = WindowStartupLocation.CenterOwner, - SizeToContent = SizeToContent.Manual + SizeToContent = SizeToContent.Height }; dialog.Content = BuildMessageDialogContent(message, () => dialog.Close()); @@ -312,17 +311,11 @@ public partial class AvaloniaMainWindow : Window Spacing = 12 }; - layout.Children.Add(new ScrollViewer + layout.Children.Add(new TextBlock { - Height = 460, - HorizontalScrollBarVisibility = Avalonia.Controls.Primitives.ScrollBarVisibility.Auto, - VerticalScrollBarVisibility = Avalonia.Controls.Primitives.ScrollBarVisibility.Auto, - Content = new TextBlock - { - Text = message, - TextWrapping = Avalonia.Media.TextWrapping.Wrap, - FontFamily = new Avalonia.Media.FontFamily("Cascadia Mono, Consolas, monospace") - } + Text = message, + TextWrapping = Avalonia.Media.TextWrapping.Wrap, + MaxWidth = 420 }); var closeButton = new Button diff --git a/imagecatalog/AvaloniaViews/FaceAiTabView.axaml b/imagecatalog/AvaloniaViews/FaceAiTabView.axaml index 4564e7d..dc1aed0 100644 --- a/imagecatalog/AvaloniaViews/FaceAiTabView.axaml +++ b/imagecatalog/AvaloniaViews/FaceAiTabView.axaml @@ -79,19 +79,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/imagecatalog/DataModel.cs b/imagecatalog/DataModel.cs index b5ea1d6..4ed641a 100644 --- a/imagecatalog/DataModel.cs +++ b/imagecatalog/DataModel.cs @@ -41,7 +41,6 @@ namespace ImageCatalog_2 public ICommand StartAiCommand { get; } public ICommand StartFaceEncoderCommand { get; } public ICommand StopFaceEncoderCommand { get; } - public ICommand UploadFaceEncoderOutputCommand { get; } public ICommand StartFaceMatcherCommand { get; } public ICommand StopFaceMatcherCommand { get; } @@ -51,7 +50,6 @@ namespace ImageCatalog_2 private readonly ImageCreationService _imageCreationService; private readonly IAiExtractionService _aiExtractionService; private readonly IImageProcessingCoordinator _imageProcessingCoordinator; - private readonly PickerPreferenceService? _pickerPreferenceService; private readonly ProcessingStateViewModel _processing; private readonly PathSettingsViewModel _paths; private readonly AiSettingsViewModel _ai; @@ -61,7 +59,6 @@ namespace ImageCatalog_2 private readonly IMapper _mapper; private readonly AsyncCommand _startFaceEncoderCommand; private readonly AsyncCommand _stopFaceEncoderCommand; - private readonly AsyncCommand _uploadFaceEncoderOutputCommand; private readonly AsyncCommand _startFaceMatcherCommand; private readonly AsyncCommand _stopFaceMatcherCommand; private readonly object _faceEncoderProcessLock = new(); @@ -78,15 +75,12 @@ namespace ImageCatalog_2 private Task? _faceMatcherLogWatcherTask; private bool _hasStartedFaceEncoderInSession; private bool _hasStartedFaceMatcherInSession; - private bool _isLoadingFaceSshUserPreferences; - private string _lastFaceEncoderOutputFilePath = string.Empty; private int _numberAiGpuRefreshVersion; private volatile bool _numberAiGpuValidationPending; private sealed record ParsedFaceMatcherRow(string PhotoId, double? Score, string RawRow, string DebugSummary); private const string AiCsvOverwriteDialogTitle = "File CSV gia esistente"; - private static readonly Regex FaceUploadPathRegex = new(@"^\d{4}/(?:0[1-9]|1[0-2])\.[^/\\]+/[^/\\]+$", RegexOptions.Compiled | RegexOptions.CultureInvariant); // ComboBox collections public List AvailableFonts { get; } @@ -96,7 +90,7 @@ namespace ImageCatalog_2 [CLSCompliant(false)] public DataModel(ITestService testService, ISettingsService settingsService, ImageCreationService imageCreationService, IAiExtractionService aiExtractionService, IImageProcessingCoordinator imageProcessingCoordinator, PicSettings picSettings, - IMapper mapper, ILogger logger, MaddoShared.IVersionProvider? versionProvider = null, PickerPreferenceService? pickerPreferenceService = null) + IMapper mapper, ILogger logger, MaddoShared.IVersionProvider? versionProvider = null) { _service = testService; _logger = logger; @@ -104,7 +98,6 @@ namespace ImageCatalog_2 _imageCreationService = imageCreationService; _aiExtractionService = aiExtractionService; _imageProcessingCoordinator = imageProcessingCoordinator; - _pickerPreferenceService = pickerPreferenceService; _processing = new ProcessingStateViewModel(); _processing.PropertyChanged += OnProcessingPropertyChanged; _paths = new PathSettingsViewModel(); @@ -129,12 +122,10 @@ namespace ImageCatalog_2 StartAiCommand = new AsyncCommand(StartAiAsync); _startFaceEncoderCommand = new AsyncCommand(RunFaceEncoderAsync, CanRunFaceEncoder); _stopFaceEncoderCommand = new AsyncCommand(() => StopFaceEncoderAsync("Arresto richiesto dall'utente."), CanStopFaceEncoder); - _uploadFaceEncoderOutputCommand = new AsyncCommand(UploadFaceEncoderOutputAsync, CanUploadFaceEncoderOutput); _startFaceMatcherCommand = new AsyncCommand(RunFaceMatcherAsync, CanRunFaceMatcher); _stopFaceMatcherCommand = new AsyncCommand(() => StopFaceMatcherAsync("Arresto richiesto dall'utente."), CanStopFaceMatcher); StartFaceEncoderCommand = _startFaceEncoderCommand; StopFaceEncoderCommand = _stopFaceEncoderCommand; - UploadFaceEncoderOutputCommand = _uploadFaceEncoderOutputCommand; StartFaceMatcherCommand = _startFaceMatcherCommand; StopFaceMatcherCommand = _stopFaceMatcherCommand; @@ -148,7 +139,6 @@ namespace ImageCatalog_2 // Load available fonts AvailableFonts = LoadAvailableFonts(); - LoadFaceSshUserPreferences(); QueueRefreshNumberAiGpuCapabilities(); RefreshFaceExecutableCapabilities(); } @@ -340,67 +330,6 @@ namespace ImageCatalog_2 set => _ai.FaceOutputFolderPath = value; } - public string FaceUploadPath - { - get => _ai.FaceUploadPath; - set - { - _ai.FaceUploadPath = value?.Trim() ?? string.Empty; - NotifyPropertyChanged(nameof(IsFaceUploadPathValid)); - UpdateFaceUploadCommandStates(); - } - } - - public bool FaceUploadDryRun - { - get => _ai.FaceUploadDryRun; - set => SetFaceSshPreferenceBoolValue(PickerPreferenceKeys.FaceUploadDryRun, normalizedValue => _ai.FaceUploadDryRun = normalizedValue, value); - } - - public bool IsFaceUploadPathValid => IsValidFaceUploadPath(FaceUploadPath); - - public string FaceSshUsername - { - get => _ai.FaceSshUsername; - set => SetFaceSshPreferenceValue(PickerPreferenceKeys.FaceSshUsername, normalizedValue => _ai.FaceSshUsername = normalizedValue, value?.Trim() ?? string.Empty); - } - - public string FaceSshPassword - { - get => _ai.FaceSshPassword; - set => SetFaceSshPreferenceValue(PickerPreferenceKeys.FaceSshPassword, normalizedValue => _ai.FaceSshPassword = normalizedValue, value ?? string.Empty); - } - - public string FaceSshAddress - { - get => _ai.FaceSshAddress; - set => SetFaceSshPreferenceValue(PickerPreferenceKeys.FaceSshAddress, normalizedValue => _ai.FaceSshAddress = normalizedValue, value?.Trim() ?? string.Empty); - } - - public string FaceSshPort - { - get => _ai.FaceSshPort; - set => SetFaceSshPreferenceValue(PickerPreferenceKeys.FaceSshPort, normalizedValue => _ai.FaceSshPort = normalizedValue, value?.Trim() ?? string.Empty); - } - - public string FaceSshPathA - { - get => _ai.FaceSshPathA; - set => SetFaceSshPreferenceValue(PickerPreferenceKeys.FaceSshPathA, normalizedValue => _ai.FaceSshPathA = normalizedValue, value?.Trim() ?? string.Empty); - } - - public string FaceSshPathB - { - get => _ai.FaceSshPathB; - set => SetFaceSshPreferenceValue(PickerPreferenceKeys.FaceSshPathB, normalizedValue => _ai.FaceSshPathB = normalizedValue, value?.Trim() ?? string.Empty); - } - - public bool IsFaceUploadRunning - { - get => _ai.IsFaceUploadRunning; - private set => _ai.IsFaceUploadRunning = value; - } - public bool FaceRecursive { get => _ai.FaceRecursive; @@ -739,13 +668,7 @@ namespace ImageCatalog_2 } NotifyPropertyChanged(e.PropertyName); - if (string.Equals(e.PropertyName, nameof(AiSettingsViewModel.FaceUploadPath), StringComparison.Ordinal)) - { - NotifyPropertyChanged(nameof(IsFaceUploadPathValid)); - } - UpdateFaceEncoderCommandStates(); - UpdateFaceUploadCommandStates(); UpdateFaceMatcherCommandStates(); } @@ -1649,11 +1572,6 @@ namespace ImageCatalog_2 _stopFaceEncoderCommand?.RaiseCanExecuteChanged(); } - private void UpdateFaceUploadCommandStates() - { - _uploadFaceEncoderOutputCommand?.RaiseCanExecuteChanged(); - } - private void UpdateFaceMatcherCommandStates() { _startFaceMatcherCommand?.RaiseCanExecuteChanged(); @@ -1713,7 +1631,6 @@ namespace ImageCatalog_2 var parallelism = NormalizeFaceParallelism(FaceParallelism); var minSize = NormalizeFaceMinSize(FaceMinSize); var outputFiles = BuildFaceEncoderOutputPaths(outputFolderPath, imagesFolder, DateTime.Now); - _lastFaceEncoderOutputFilePath = outputFiles.OutputFilePath; FaceExecutablePath = executableRootPath; FaceOutputFolderPath = outputFolderPath; @@ -1831,66 +1748,6 @@ namespace ImageCatalog_2 return !IsFaceMatcherRunning; } - private bool CanUploadFaceEncoderOutput() - { - return IsFaceUploadPathValid && !IsFaceUploadRunning; - } - - private async Task UploadFaceEncoderOutputAsync() - { - if (!IsValidFaceUploadPath(FaceUploadPath)) - { - FaceStatusMessage = "Percorso upload non valido."; - return; - } - - if (!TryBuildFaceUploadRequest(out var request, out var validationMessage)) - { - FaceStatusMessage = validationMessage; - await ShowErrorMessageAsync("Upload Face AI", validationMessage).ConfigureAwait(false); - return; - } - - IsFaceUploadRunning = true; - UpdateFaceUploadCommandStates(); - FaceStatusMessage = "Upload face encoder in corso..."; - - try - { - if (FaceUploadDryRun) - { - var preview = BuildFaceUploadDryRunPreview(request); - await ShowMessageAsync("Upload Face AI (dry run)", preview).ConfigureAwait(false); - await InvokeOnUiThreadAsync(() => FaceStatusMessage = "Dry run: comando mostrato nel popup.").ConfigureAwait(false); - return; - } - - var result = await RunFaceUploadPowerShellAsync(request).ConfigureAwait(false); - await InvokeOnUiThreadAsync(() => - { - FaceCommandOutput = string.IsNullOrWhiteSpace(FaceCommandOutput) - ? result - : $"{FaceCommandOutput.TrimEnd()}\n\n{result}"; - FaceStatusMessage = "Upload completato."; - }).ConfigureAwait(false); - } - catch (Exception ex) - { - var message = ex.GetBaseException().Message; - _logger.LogError(ex, "Face encoder upload failed."); - await InvokeOnUiThreadAsync(() => FaceStatusMessage = "Errore durante upload face encoder.").ConfigureAwait(false); - await ShowErrorMessageAsync("Upload Face AI", message).ConfigureAwait(false); - } - finally - { - await InvokeOnUiThreadAsync(() => - { - IsFaceUploadRunning = false; - UpdateFaceUploadCommandStates(); - }).ConfigureAwait(false); - } - } - private bool CanStopFaceMatcher() { return IsFaceMatcherRunning; @@ -2504,7 +2361,7 @@ namespace ImageCatalog_2 && File.Exists(configuration.RecognitionWeights); } - private Task ShowMessageAsync(string title, string message) + private Task ShowErrorMessageAsync(string title, string message) { return InvokeOnUiThreadAsync(() => { @@ -2512,11 +2369,6 @@ namespace ImageCatalog_2 }); } - private Task ShowErrorMessageAsync(string title, string message) - { - return ShowMessageAsync(title, message); - } - internal async Task ConfirmAiCsvOverwriteIfNeededAsync() { var csvOutputPath = NormalizeFilePathArgument(CsvOutputPath); @@ -3045,253 +2897,6 @@ namespace ImageCatalog_2 Path.Combine(outputFolderPath, $"encoder_log_{timestampToken}_{safeRaceName}.txt")); } - internal static bool IsValidFaceUploadPath(string value) - { - return FaceUploadPathRegex.IsMatch((value ?? string.Empty).Trim()); - } - - internal static string CombineRemoteUploadPath(string basePath, string relativePath) - { - var normalizedBasePath = (basePath ?? string.Empty).Trim().Replace('\\', '/').TrimEnd('/'); - var normalizedRelativePath = (relativePath ?? string.Empty).Trim().Replace('\\', '/').Trim('/'); - - return string.IsNullOrWhiteSpace(normalizedBasePath) - ? normalizedRelativePath - : string.IsNullOrWhiteSpace(normalizedRelativePath) - ? normalizedBasePath - : $"{normalizedBasePath}/{normalizedRelativePath}"; - } - - private void LoadFaceSshUserPreferences() - { - if (_pickerPreferenceService is null) - { - return; - } - - _isLoadingFaceSshUserPreferences = true; - try - { - _ai.FaceSshUsername = _pickerPreferenceService.GetRememberedValue(PickerPreferenceKeys.FaceSshUsername) ?? string.Empty; - _ai.FaceSshPassword = _pickerPreferenceService.GetRememberedRawValue(PickerPreferenceKeys.FaceSshPassword) ?? string.Empty; - _ai.FaceSshAddress = _pickerPreferenceService.GetRememberedValue(PickerPreferenceKeys.FaceSshAddress) ?? string.Empty; - _ai.FaceSshPort = _pickerPreferenceService.GetRememberedValue(PickerPreferenceKeys.FaceSshPort) ?? "22"; - _ai.FaceSshPathA = _pickerPreferenceService.GetRememberedValue(PickerPreferenceKeys.FaceSshPathA) ?? string.Empty; - _ai.FaceSshPathB = _pickerPreferenceService.GetRememberedValue(PickerPreferenceKeys.FaceSshPathB) ?? string.Empty; - _ai.FaceUploadDryRun = TryGetRememberedBoolPreference(PickerPreferenceKeys.FaceUploadDryRun); - } - finally - { - _isLoadingFaceSshUserPreferences = false; - } - } - - private void SetFaceSshPreferenceValue(string preferenceKey, Action setValue, string value) - { - setValue(value); - if (_isLoadingFaceSshUserPreferences) - { - return; - } - - _pickerPreferenceService?.RememberRawValue(preferenceKey, value); - } - - private void SetFaceSshPreferenceBoolValue(string preferenceKey, Action setValue, bool value) - { - setValue(value); - if (_isLoadingFaceSshUserPreferences) - { - return; - } - - _pickerPreferenceService?.RememberRawValue(preferenceKey, value ? bool.TrueString : bool.FalseString); - } - - private bool TryGetRememberedBoolPreference(string preferenceKey) - { - var rawValue = _pickerPreferenceService?.GetRememberedRawValue(preferenceKey); - return bool.TryParse(rawValue, out var parsed) && parsed; - } - - private bool TryBuildFaceUploadRequest(out FaceUploadRequest request, out string validationMessage) - { - request = null!; - validationMessage = string.Empty; - - var sourceFilePath = ResolveFaceEncoderUploadSourceFile(); - if (string.IsNullOrWhiteSpace(sourceFilePath) || !File.Exists(sourceFilePath)) - { - validationMessage = "File .pkl face encoder non trovato nella cartella output."; - return false; - } - - var username = FaceSshUsername.Trim(); - var password = FaceSshPassword; - var serverAddress = FaceSshAddress.Trim(); - if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(serverAddress)) - { - validationMessage = "Inserisci username e indirizzo SSH."; - return false; - } - - if (!int.TryParse(FaceSshPort.Trim(), NumberStyles.Integer, CultureInfo.InvariantCulture, out var port) || port is < 1 or > 65535) - { - validationMessage = "Porta SSH non valida."; - return false; - } - - var remoteBasePaths = new[] { FaceSshPathA, FaceSshPathB } - .Select(path => path.Trim()) - .Where(path => !string.IsNullOrWhiteSpace(path)) - .ToArray(); - if (remoteBasePaths.Length != 2) - { - validationMessage = "Inserisci path SSH A e path SSH B."; - return false; - } - - var relativeUploadPath = FaceUploadPath.Trim(); - var remoteDirectories = remoteBasePaths - .Select(path => CombineRemoteUploadPath(path, relativeUploadPath)) - .ToArray(); - - request = new FaceUploadRequest(sourceFilePath, username, password, serverAddress, port, remoteDirectories); - return true; - } - - private string? ResolveFaceEncoderUploadSourceFile() - { - var outputFolderPath = NormalizeDirectoryPathArgument(FaceOutputFolderPath); - if (!Directory.Exists(outputFolderPath)) - { - return null; - } - - return ResolveLatestFaceUploadSourceFile(outputFolderPath, NormalizeDirectoryPathArgument(DestinationPath)); - } - - internal static string? ResolveLatestFaceUploadSourceFile(string outputFolderPath, string imagesFolderPath) - { - var normalizedOutputFolderPath = NormalizeDirectoryPathArgument(outputFolderPath); - if (!Directory.Exists(normalizedOutputFolderPath)) - { - return null; - } - - var safeRaceName = BuildSafeFaceEncoderRaceName(NormalizeDirectoryPathArgument(imagesFolderPath)); - var racePattern = $"face_encodings_*_{safeRaceName}.pkl"; - - return new DirectoryInfo(normalizedOutputFolderPath) - .EnumerateFiles(racePattern, SearchOption.TopDirectoryOnly) - .OrderByDescending(file => file.LastWriteTimeUtc) - .ThenByDescending(file => file.Name, StringComparer.OrdinalIgnoreCase) - .FirstOrDefault() - ?.FullName; - } - - private async Task RunFaceUploadPowerShellAsync(FaceUploadRequest request) - { - var commandLine = BuildFaceUploadPowerShellCommand(request); - - var processStartInfo = CreateFaceUploadProcessStartInfo(commandLine); - - using var process = new Process { StartInfo = processStartInfo }; - - if (!process.Start()) - { - throw new InvalidOperationException("Avvio PowerShell per upload fallito."); - } - - await process.WaitForExitAsync().ConfigureAwait(false); - - if (process.ExitCode != 0) - { - throw new InvalidOperationException($"Upload fallito (exit code {process.ExitCode}). Verifica autenticazione SSH o inserisci la password nel terminale se richiesta."); - } - - var summary = new StringBuilder(); - summary.AppendLine("Upload Face AI completato."); - summary.AppendLine($"Command: {commandLine}"); - summary.AppendLine($"File: {request.LocalPath}"); - foreach (var remoteDirectory in request.RemoteDirectories) - { - summary.AppendLine($"Destinazione: {remoteDirectory}"); - } - - return summary.ToString(); - } - - private static ProcessStartInfo CreateFaceUploadProcessStartInfo(string commandLine) - { - var processStartInfo = new ProcessStartInfo - { - FileName = ResolvePowerShellExecutable(), - UseShellExecute = false, - RedirectStandardOutput = false, - RedirectStandardError = false, - RedirectStandardInput = false, - CreateNoWindow = false - }; - - processStartInfo.ArgumentList.Add("-NoProfile"); - processStartInfo.ArgumentList.Add("-ExecutionPolicy"); - processStartInfo.ArgumentList.Add("Bypass"); - processStartInfo.ArgumentList.Add("-Command"); - processStartInfo.ArgumentList.Add(commandLine); - - return processStartInfo; - } - - private static string BuildFaceUploadDryRunPreview(FaceUploadRequest request) - { - return BuildFaceUploadPowerShellCommand(request); - } - - private static string BuildFaceUploadPowerShellCommand(FaceUploadRequest request) - { - var fileName = Path.GetFileName(request.LocalPath); - var copyCommands = request.RemoteDirectories - .Select(remoteDirectory => BuildFaceUploadCopyCommand(request, CombineRemoteUploadPath(remoteDirectory, fileName))) - .ToArray(); - - return string.Join("; ", copyCommands); - } - - private static string ResolvePowerShellExecutable() - { - return OperatingSystem.IsWindows() ? "powershell.exe" : "pwsh"; - } - - private static string BuildFaceUploadCopyCommand(FaceUploadRequest request, string remoteFilePath) - { - return BuildScpCommand(request, remoteFilePath); - } - - private static string BuildScpCommand(FaceUploadRequest request, string remoteFilePath) - { - var remoteTarget = $"{request.UserName}@{request.ServerAddress}:{QuoteScpRemotePath(remoteFilePath)}"; - return $"scp -P {request.Port.ToString(CultureInfo.InvariantCulture)} {QuotePowerShellString(request.LocalPath)} {QuotePowerShellString(remoteTarget)}"; - } - - private static string QuotePowerShellString(string value) - { - return $"'{(value ?? string.Empty).Replace("'", "''", StringComparison.Ordinal)}'"; - } - - private static string QuoteScpRemotePath(string value) - { - return "'" + (value ?? string.Empty).Replace("'", "'\\''", StringComparison.Ordinal) + "'"; - } - - private sealed record FaceUploadRequest( - string LocalPath, - string UserName, - string Password, - string ServerAddress, - int Port, - IReadOnlyList RemoteDirectories); - internal static string BuildSafeFaceEncoderRaceName(string imagesFolderPath) { var raceName = new DirectoryInfo(imagesFolderPath).Name; diff --git a/imagecatalog/Models/SettingsDto.cs b/imagecatalog/Models/SettingsDto.cs index 91f81cd..41c7dd5 100644 --- a/imagecatalog/Models/SettingsDto.cs +++ b/imagecatalog/Models/SettingsDto.cs @@ -289,10 +289,6 @@ namespace ImageCatalog_2.Models [XmlElement("AI_FaceOutputFolderPath")] public string FaceOutputFolderPath { get; set; } = string.Empty; - [JsonPropertyName("FaceUploadPath")] - [XmlElement("AI_FaceUploadPath")] - public string FaceUploadPath { get; set; } = string.Empty; - [JsonPropertyName("FaceRecursive")] [XmlElement("AI_FaceRecursive")] public bool FaceRecursive { get; set; } diff --git a/imagecatalog/Program.cs b/imagecatalog/Program.cs index 997a47c..aa02e40 100644 --- a/imagecatalog/Program.cs +++ b/imagecatalog/Program.cs @@ -136,9 +136,8 @@ static class Program var mapper = sp.GetRequiredService(); var logger = sp.GetRequiredService>(); var versionProvider = sp.GetService(); - var pickerPreferenceService = sp.GetRequiredService(); - return new DataModel(testService, settingsService, imageCreation, aiExtractionService, imageProcessingCoordinator, picSettings, mapper, logger, versionProvider, pickerPreferenceService); + return new DataModel(testService, settingsService, imageCreation, aiExtractionService, imageProcessingCoordinator, picSettings, mapper, logger, versionProvider); }); services.AddTransient(); diff --git a/imagecatalog/Services/AiExtractionService.cs b/imagecatalog/Services/AiExtractionService.cs index b8f413a..685a43f 100644 --- a/imagecatalog/Services/AiExtractionService.cs +++ b/imagecatalog/Services/AiExtractionService.cs @@ -217,31 +217,16 @@ public class AiExtractionService : IAiExtractionService Directory.CreateDirectory(dir); } - using var sw = new StreamWriter(csvOutputPath, false, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); - sw.NewLine = "\r"; + using var sw = new StreamWriter(csvOutputPath, false, Encoding.UTF8); sw.WriteLine("filename,text"); foreach (var result in extractedResults) { - var text = result.Text ?? string.Empty; - if (string.IsNullOrWhiteSpace(text)) - { - continue; - } - var csvFileName = Path.GetFileName(result.Path ?? string.Empty); - sw.Write(csvFileName); - sw.Write(','); - sw.WriteLine(FormatLegacyTextField(text)); + var safeText = (result.Text ?? string.Empty).Replace("\"", "\"\""); + sw.WriteLine($"\"{csvFileName}\",\"{safeText}\""); } } - private static string FormatLegacyTextField(string text) - { - return text.Contains(',', StringComparison.Ordinal) - ? $"\"{text.Replace("\"", "\"\"")}\"" - : text; - } - private static double CalculateAverageImagesPerSecond(int processed, TimeSpan elapsed) { return elapsed.TotalSeconds > 0 ? processed / elapsed.TotalSeconds : 0; diff --git a/imagecatalog/Services/PickerPreferenceService.cs b/imagecatalog/Services/PickerPreferenceService.cs index 785ee4f..49bdaea 100644 --- a/imagecatalog/Services/PickerPreferenceService.cs +++ b/imagecatalog/Services/PickerPreferenceService.cs @@ -22,13 +22,6 @@ public static class PickerPreferenceKeys public const string FaceMatcherEncodings = "Picker.FaceMatcherEncodings.LastPath"; public const string FaceMatcherOutput = "Picker.FaceMatcherOutput.LastPath"; public const string FaceMatcherLog = "Picker.FaceMatcherLog.LastPath"; - public const string FaceSshUsername = "FaceAI.Ssh.Username"; - public const string FaceSshPassword = "FaceAI.Ssh.Password"; - public const string FaceSshAddress = "FaceAI.Ssh.Address"; - public const string FaceSshPort = "FaceAI.Ssh.Port"; - public const string FaceSshPathA = "FaceAI.Ssh.PathA"; - public const string FaceSshPathB = "FaceAI.Ssh.PathB"; - public const string FaceUploadDryRun = "FaceAI.Upload.DryRun"; } public sealed class PickerPreferenceService @@ -78,13 +71,6 @@ public sealed class PickerPreferenceService : value.Trim().Trim('"'); } - public string? GetRememberedRawValue(string preferenceKey) - { - return _userPreferences.ParametroExists(preferenceKey) - ? _userPreferences.LeggiParametroString(preferenceKey) - : null; - } - public void RememberValue(string preferenceKey, string? value) { if (string.IsNullOrWhiteSpace(value)) @@ -96,12 +82,6 @@ public sealed class PickerPreferenceService _userPreferences.SalvaParametriSetup(); } - public void RememberRawValue(string preferenceKey, string? value) - { - _userPreferences.AggiornaParametro(preferenceKey, value ?? string.Empty); - _userPreferences.SalvaParametriSetup(); - } - public void ForgetValue(string preferenceKey) { if (_userPreferences.RimuoviParametro(preferenceKey)) diff --git a/imagecatalog/ViewModels/AiSettingsViewModel.cs b/imagecatalog/ViewModels/AiSettingsViewModel.cs index 118d577..e38213c 100644 --- a/imagecatalog/ViewModels/AiSettingsViewModel.cs +++ b/imagecatalog/ViewModels/AiSettingsViewModel.cs @@ -115,105 +115,6 @@ public class AiSettingsViewModel : ViewModelBase } } - private string _faceUploadPath = string.Empty; - public string FaceUploadPath - { - get => _faceUploadPath; - set - { - _faceUploadPath = value; - NotifyPropertyChanged(); - } - } - - private bool _faceUploadDryRun; - public bool FaceUploadDryRun - { - get => _faceUploadDryRun; - set - { - _faceUploadDryRun = value; - NotifyPropertyChanged(); - } - } - - private string _faceSshUsername = string.Empty; - public string FaceSshUsername - { - get => _faceSshUsername; - set - { - _faceSshUsername = value; - NotifyPropertyChanged(); - } - } - - private string _faceSshPassword = string.Empty; - public string FaceSshPassword - { - get => _faceSshPassword; - set - { - _faceSshPassword = value; - NotifyPropertyChanged(); - } - } - - private string _faceSshAddress = string.Empty; - public string FaceSshAddress - { - get => _faceSshAddress; - set - { - _faceSshAddress = value; - NotifyPropertyChanged(); - } - } - - private string _faceSshPort = "22"; - public string FaceSshPort - { - get => _faceSshPort; - set - { - _faceSshPort = value; - NotifyPropertyChanged(); - } - } - - private string _faceSshPathA = string.Empty; - public string FaceSshPathA - { - get => _faceSshPathA; - set - { - _faceSshPathA = value; - NotifyPropertyChanged(); - } - } - - private string _faceSshPathB = string.Empty; - public string FaceSshPathB - { - get => _faceSshPathB; - set - { - _faceSshPathB = value; - NotifyPropertyChanged(); - } - } - - private bool _isFaceUploadRunning; - public bool IsFaceUploadRunning - { - get => _isFaceUploadRunning; - set - { - _isFaceUploadRunning = value; - NotifyPropertyChanged(); - } - } - private bool _faceRecursive; public bool FaceRecursive {