feat: Enhance Face AI upload functionality and UI
- Updated MainWindow.axaml to increase height and add new UI elements for SSH upload configuration. - Implemented commands for opening source and destination paths in file explorer. - Added FaceUploadPath and SSH configuration properties to DataModel and AiSettingsViewModel. - Introduced validation for FaceUploadPath format and commands for uploading face encoder output. - Enhanced PickerPreferenceService to manage SSH credentials and upload preferences. - Updated settings persistence to include FaceUploadPath and SSH preferences. - Added tests for FaceUploadPath validation and upload command enabling logic.
This commit is contained in:
parent
e68608312a
commit
e9142df97c
22 changed files with 1477 additions and 84 deletions
11
.vscode/launch.json
vendored
11
.vscode/launch.json
vendored
|
|
@ -11,6 +11,17 @@
|
|||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
13
.vscode/tasks.json
vendored
13
.vscode/tasks.json
vendored
|
|
@ -13,6 +13,19 @@
|
|||
],
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
227
CatalogLite/Assets/Config.xml
Normal file
227
CatalogLite/Assets/Config.xml
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
||||
<NewDataSet>
|
||||
<Setup>
|
||||
<Nome>MiniatureModalita</Nome>
|
||||
<Valore>Text</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>DirSorgente</Nome>
|
||||
<Valore></Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>DirDestinazione</Nome>
|
||||
<Valore></Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>MiniatureCrea</Nome>
|
||||
<Valore>True</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>MiniatureSuffisso</Nome>
|
||||
<Valore>tn_</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>MiniatureAltezza</Nome>
|
||||
<Valore>350</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>MiniatureLarghezza</Nome>
|
||||
<Valore>350</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>FontDimensioneMiniatura</Nome>
|
||||
<Valore>20</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>CompressioneJpegMiniatura</Nome>
|
||||
<Valore>60</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>MiniatureAddOrario</Nome>
|
||||
<Valore>False</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>NomeMiniatura</Nome>
|
||||
<Valore>False</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>MiniatureAddScritta</Nome>
|
||||
<Valore>True</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>TempoSmall</Nome>
|
||||
<Valore>False</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>NumTempoSmall</Nome>
|
||||
<Valore>False</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>FotoCodice</Nome>
|
||||
<Valore></Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>FotoAltezza</Nome>
|
||||
<Valore>2560</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>FotoLarghezza</Nome>
|
||||
<Valore>2560</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>FotoDimOriginali</Nome>
|
||||
<Valore>False</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>CompressioneJpeg</Nome>
|
||||
<Valore>90</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>FontDimensione</Nome>
|
||||
<Valore>50</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>FontNome</Nome>
|
||||
<Valore>Verdana</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>FontBold</Nome>
|
||||
<Valore>True</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>ColoreTestoRGB</Nome>
|
||||
<Valore>#FA7B0A</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>TestoTesto</Nome>
|
||||
<Valore></Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>TestoVerticale</Nome>
|
||||
<Valore></Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>TestoTrasparente</Nome>
|
||||
<Valore>0</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>TestoMargine</Nome>
|
||||
<Valore>8</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>TestoPosizione</Nome>
|
||||
<Valore>Basso</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>TestoAllineamento</Nome>
|
||||
<Valore>Centro</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>GrandezzaVerticale</Nome>
|
||||
<Valore>18</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>MargineVerticale</Nome>
|
||||
<Valore>6</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>MarchioAltezza</Nome>
|
||||
<Valore>250</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>MarchioLarghezza</Nome>
|
||||
<Valore>250</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>MarchioMargine</Nome>
|
||||
<Valore>130</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>MarchioAllOrizzontale</Nome>
|
||||
<Valore>Destra</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>MarchioAllVerticale</Nome>
|
||||
<Valore>Alto</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>MarchioTrasparenza</Nome>
|
||||
<Valore>100</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>MarchioAggiungi</Nome>
|
||||
<Valore>True</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>ColoreTrasparente</Nome>
|
||||
<Valore>#FFFFFF</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>UsaColoreTrasparente</Nome>
|
||||
<Valore>True</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>ImageLibrary</Nome>
|
||||
<Valore>ImageSharp</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>GeneraleForzaJpg</Nome>
|
||||
<Valore>True</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>GeneraleRotazioneAutomatica</Nome>
|
||||
<Valore>True</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>DirSottoDirectory</Nome>
|
||||
<Valore>True</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>TempoGara</Nome>
|
||||
<Valore>False</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>Orario</Nome>
|
||||
<Valore>False</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>EtichettaOrario</Nome>
|
||||
<Valore></Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>DataFoto</Nome>
|
||||
<Valore>False</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>NumeroFoto</Nome>
|
||||
<Valore>False</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>GeneraleSovrascriviFile</Nome>
|
||||
<Valore>False</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>DirDividiNumFile</Nome>
|
||||
<Valore>300</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>DirDividiSuffisso</Nome>
|
||||
<Valore></Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>DirDividiNumCifre</Nome>
|
||||
<Valore>2</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>ChunkSize</Nome>
|
||||
<Valore>0</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>ThreadsCount</Nome>
|
||||
<Valore>0</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>DataPartenza</Nome>
|
||||
<Valore>17/02/2026 09:35:25</Valore>
|
||||
</Setup>
|
||||
</NewDataSet>
|
||||
BIN
CatalogLite/Assets/Logo_RUS_ETS_tricolore_OK.png
Normal file
BIN
CatalogLite/Assets/Logo_RUS_ETS_tricolore_OK.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 290 KiB |
336
CatalogLite/Assets/testConfig.xml
Normal file
336
CatalogLite/Assets/testConfig.xml
Normal file
|
|
@ -0,0 +1,336 @@
|
|||
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
||||
<NewDataSet>
|
||||
<Setup>
|
||||
<Nome>MiniatureModalita</Nome>
|
||||
<Valore>RaceTime</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>MiniatureCrea</Nome>
|
||||
<Valore>True</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>MiniatureSuffisso</Nome>
|
||||
<Valore>tn_</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>MiniatureAltezza</Nome>
|
||||
<Valore>350</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>MiniatureLarghezza</Nome>
|
||||
<Valore>350</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>FontDimensioneMiniatura</Nome>
|
||||
<Valore>48</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>CompressioneJpegMiniatura</Nome>
|
||||
<Valore>25</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>MiniatureAddOrario</Nome>
|
||||
<Valore>False</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>NomeMiniatura</Nome>
|
||||
<Valore>False</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>MiniatureAddScritta</Nome>
|
||||
<Valore>False</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>TempoSmall</Nome>
|
||||
<Valore>True</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>NumTempoSmall</Nome>
|
||||
<Valore>False</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>FotoCodice</Nome>
|
||||
<Valore></Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>FotoAltezza</Nome>
|
||||
<Valore>2560</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>FotoLarghezza</Nome>
|
||||
<Valore>2560</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>FotoDimOriginali</Nome>
|
||||
<Valore>False</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>CompressioneJpeg</Nome>
|
||||
<Valore>90</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>FontDimensione</Nome>
|
||||
<Valore>22</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>FontNome</Nome>
|
||||
<Valore>Verdana</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>FontBold</Nome>
|
||||
<Valore>True</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>ColoreTestoRGB</Nome>
|
||||
<Valore>#FEC005</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>TestoTesto</Nome>
|
||||
<Valore>MARATONINA DI VINCI -1 FEBBRAIO 2026</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>TestoVerticale</Nome>
|
||||
<Valore>MARATONINA DI VINCI
|
||||
1 FEBBRAIO 2026</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>TestoTrasparente</Nome>
|
||||
<Valore>0</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>TestoMargine</Nome>
|
||||
<Valore>8</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>TestoPosizione</Nome>
|
||||
<Valore>Basso</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>TestoAllineamento</Nome>
|
||||
<Valore>Centro</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>GrandezzaVerticale</Nome>
|
||||
<Valore>18</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>MargineVerticale</Nome>
|
||||
<Valore>6</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>MarchioFile</Nome>
|
||||
<Valore>K:\various\catalogtest\Logo.jpg</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>MarchioAltezza</Nome>
|
||||
<Valore>470</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>MarchioLarghezza</Nome>
|
||||
<Valore>470</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>MarchioMargine</Nome>
|
||||
<Valore>350</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>MarchioAllOrizzontale</Nome>
|
||||
<Valore>Destra</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>MarchioAllVerticale</Nome>
|
||||
<Valore>Alto</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>MarchioTrasparenza</Nome>
|
||||
<Valore>100</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>MarchioAggiungi</Nome>
|
||||
<Valore>True</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>ColoreTrasparente</Nome>
|
||||
<Valore>#FFFFFF</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>UsaColoreTrasparente</Nome>
|
||||
<Valore>True</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>ImageLibrary</Nome>
|
||||
<Valore>System.Graphics</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>GeneraleForzaJpg</Nome>
|
||||
<Valore>True</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>GeneraleRotazioneAutomatica</Nome>
|
||||
<Valore>True</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>DirSottoDirectory</Nome>
|
||||
<Valore>True</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>TempoGara</Nome>
|
||||
<Valore>True</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>Orario</Nome>
|
||||
<Valore>False</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>EtichettaOrario</Nome>
|
||||
<Valore> TEMPO :</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>DataFoto</Nome>
|
||||
<Valore>False</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>NumeroFoto</Nome>
|
||||
<Valore>False</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>GeneraleSovrascriviFile</Nome>
|
||||
<Valore>True</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>DirDividiNumFile</Nome>
|
||||
<Valore>300</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>DirDividiSuffisso</Nome>
|
||||
<Valore></Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>DirDividiNumCifre</Nome>
|
||||
<Valore>2</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>ChunkSize</Nome>
|
||||
<Valore>200</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>ThreadsCount</Nome>
|
||||
<Valore>10</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>DataPartenza</Nome>
|
||||
<Valore>01/02/2026 20:30:48</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>AI_EstraiNumeri</Nome>
|
||||
<Valore>True</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>AI_CartellaModelli</Nome>
|
||||
<Valore>K:\vs\AIFotoONLUS\models\\</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>AI_PercorsoCsv</Nome>
|
||||
<Valore>K:\various\catalogtest\aioutput\test2.csv</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>AI_UsaGpuNumeri</Nome>
|
||||
<Valore>True</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>AI_IncludiThumbnailNumeri</Nome>
|
||||
<Valore>False</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>AI_LivelloCaricoNumeri</Nome>
|
||||
<Valore>3</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>AI_FaceExecutablePath</Nome>
|
||||
<Valore>K:\various\regalamiunsorriso\bin\Face_Recognition_Windows\</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>AI_FaceOutputFolderPath</Nome>
|
||||
<Valore>K:\various\catalogtest\aioutput\</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>AI_FaceRecursive</Nome>
|
||||
<Valore>True</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>AI_FaceIncludeThumbnails</Nome>
|
||||
<Valore>False</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>AI_FaceParallelism</Nome>
|
||||
<Valore>3</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>AI_FaceMinSize</Nome>
|
||||
<Valore>35</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>AI_FaceUpsample</Nome>
|
||||
<Valore>False</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>AI_FaceMatcherExecutablePath</Nome>
|
||||
<Valore>K:\various\regalamiunsorriso\bin\Face_Recognition_Windows\face_matcher.exe</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>AI_FaceMatcherTolerance</Nome>
|
||||
<Valore>0,75</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>RaceUpload_Login</Nome>
|
||||
<Valore></Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>RaceUpload_Password</Nome>
|
||||
<Valore></Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>RaceUpload_Description</Nome>
|
||||
<Valore></Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>RaceUpload_TipoGaraId</Nome>
|
||||
<Valore>1</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>RaceUpload_StartDate</Nome>
|
||||
<Valore>12/03/2026 00:00:00</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>RaceUpload_EndDate</Nome>
|
||||
<Valore>12/03/2026 00:00:00</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>RaceUpload_PathBase</Nome>
|
||||
<Valore></Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>RaceUpload_Localita</Nome>
|
||||
<Valore></Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>RaceUpload_EventoInLinea</Nome>
|
||||
<Valore>0</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>RaceUpload_TipoIndex</Nome>
|
||||
<Valore>1</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>RaceUpload_FreeEvent</Nome>
|
||||
<Valore>0</Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>RaceUpload_LastRaceId</Nome>
|
||||
<Valore></Valore>
|
||||
</Setup>
|
||||
<Setup>
|
||||
<Nome>RaceUpload_RemoteProcessedBasePath</Nome>
|
||||
<Valore></Valore>
|
||||
</Setup>
|
||||
</NewDataSet>
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
using Avalonia.Threading;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace CatalogLite;
|
||||
|
|
@ -38,5 +39,14 @@ public sealed class AsyncCommand : ICommand
|
|||
}
|
||||
}
|
||||
|
||||
public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
|
||||
public void RaiseCanExecuteChanged()
|
||||
{
|
||||
if (Dispatcher.UIThread.CheckAccess())
|
||||
{
|
||||
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
|
||||
return;
|
||||
}
|
||||
|
||||
Dispatcher.UIThread.Post(() => CanExecuteChanged?.Invoke(this, EventArgs.Empty));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Globalization;
|
||||
using System.Reflection;
|
||||
using System.Xml.Linq;
|
||||
using MaddoShared;
|
||||
using SixLabors.ImageSharp;
|
||||
|
|
@ -8,6 +9,9 @@ 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))
|
||||
|
|
@ -21,7 +25,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"));
|
||||
|
|
@ -41,6 +45,32 @@ 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
|
||||
|
|
@ -150,6 +180,22 @@ 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))
|
||||
|
|
@ -193,7 +239,13 @@ public sealed class CatalogConfigurationLoader
|
|||
|
||||
public static ConfigurationValues Load(string filePath)
|
||||
{
|
||||
var document = XDocument.Load(filePath);
|
||||
using var stream = File.OpenRead(filePath);
|
||||
return Load(stream);
|
||||
}
|
||||
|
||||
public static ConfigurationValues Load(Stream stream)
|
||||
{
|
||||
var document = XDocument.Load(stream);
|
||||
var values = document
|
||||
.Descendants("Setup")
|
||||
.Where(element => element.Element("Nome") is not null)
|
||||
|
|
|
|||
|
|
@ -25,6 +25,11 @@
|
|||
<Compile Include="$(IntermediateOutputPath)CatalogLiteExpiration.g.cs" Visible="false" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Assets\Config.xml" />
|
||||
<EmbeddedResource Include="Assets\Logo_RUS_ETS_tricolore_OK.png" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MaddoShared\MaddoShared.csproj" />
|
||||
</ItemGroup>
|
||||
|
|
|
|||
|
|
@ -12,10 +12,9 @@ public sealed class LiteCatalogViewModel : ViewModelBase
|
|||
private readonly ILogger<LiteCatalogViewModel> _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 = "Carica una configurazione XML.";
|
||||
private string _processingStatus = "Caricamento configurazione incorporata...";
|
||||
private string _speedCounter = "-";
|
||||
private int _processedImagesCount;
|
||||
private int _totalImagesCount;
|
||||
|
|
@ -36,36 +35,23 @@ 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<LiteMessageEventArgs>? ShowMessageRequested;
|
||||
|
||||
public Action<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;
|
||||
|
|
@ -77,6 +63,26 @@ 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;
|
||||
|
|
@ -159,25 +165,24 @@ public sealed class LiteCatalogViewModel : ViewModelBase
|
|||
}
|
||||
}
|
||||
|
||||
public async Task LoadConfigurationFromFileAsync(string filePath)
|
||||
public void LoadEmbeddedConfiguration()
|
||||
{
|
||||
try
|
||||
{
|
||||
var configuration = await Task.Run(() => _configurationLoader.Load(filePath, _picSettings)).ConfigureAwait(false);
|
||||
var configuration = _configurationLoader.LoadEmbedded(_picSettings);
|
||||
|
||||
RunOnUiThread(() =>
|
||||
{
|
||||
_configuration = configuration;
|
||||
ConfigurationPath = configuration.FilePath;
|
||||
SourcePath = configuration.SourcePath;
|
||||
DestinationPath = configuration.DestinationPath;
|
||||
ResetProgress("Configurazione caricata.");
|
||||
});
|
||||
_configuration = configuration;
|
||||
SourcePath = configuration.SourcePath;
|
||||
DestinationPath = configuration.DestinationPath;
|
||||
NotifyPropertyChanged(nameof(HorizontalText));
|
||||
NotifyPropertyChanged(nameof(VerticalText));
|
||||
ResetProgress("Configurazione incorporata caricata.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Errore durante il caricamento della configurazione");
|
||||
ShowMessage("Configurazione", $"Impossibile caricare la configurazione: {ex.GetBaseException().Message}");
|
||||
_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}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -192,12 +197,6 @@ 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);
|
||||
|
|
@ -222,7 +221,7 @@ public sealed class LiteCatalogViewModel : ViewModelBase
|
|||
{
|
||||
if (_configuration is null)
|
||||
{
|
||||
ShowMessage("Configurazione", "Carica prima una configurazione XML.");
|
||||
ShowMessage("Configurazione", "La configurazione incorporata non e' disponibile.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -340,7 +339,6 @@ public sealed class LiteCatalogViewModel : ViewModelBase
|
|||
|
||||
private void RaiseCommandStates()
|
||||
{
|
||||
LoadConfigurationCommand.RaiseCanExecuteChanged();
|
||||
SelectSourceFolderCommand.RaiseCanExecuteChanged();
|
||||
SelectDestinationFolderCommand.RaiseCanExecuteChanged();
|
||||
StartProcessingCommand.RaiseCanExecuteChanged();
|
||||
|
|
|
|||
|
|
@ -5,24 +5,11 @@
|
|||
x:CompileBindings="False"
|
||||
Title="Catalog Lite"
|
||||
Width="740"
|
||||
Height="380"
|
||||
Height="560"
|
||||
MinWidth="640"
|
||||
MinHeight="340">
|
||||
<Grid Margin="16" RowDefinitions="Auto,Auto,Auto,*,Auto" RowSpacing="12">
|
||||
<Grid ColumnDefinitions="150,*" ColumnSpacing="10">
|
||||
<Button Command="{Binding LoadConfigurationCommand}" ToolTip.Tip="Carica configurazione XML">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="7">
|
||||
<iconPacks:PackIconMaterial Kind="FolderUploadOutline" Width="16" Height="16" />
|
||||
<TextBlock Text="Carica XML" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<TextBox Grid.Column="1"
|
||||
Text="{Binding ConfigurationPath}"
|
||||
IsReadOnly="True"
|
||||
Watermark="Nessuna configurazione caricata" />
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.Row="1" ColumnDefinitions="150,*,116" ColumnSpacing="10">
|
||||
MinHeight="500">
|
||||
<Grid Margin="16" RowDefinitions="Auto,Auto,*,Auto" RowSpacing="12">
|
||||
<Grid ColumnDefinitions="150,*,104,72" ColumnSpacing="10">
|
||||
<TextBlock Text="Sorgente" VerticalAlignment="Center" FontWeight="SemiBold" />
|
||||
<TextBox Grid.Column="1" Text="{Binding SourcePath, Mode=TwoWay}" Watermark="Cartella sorgente" />
|
||||
<Button Grid.Column="2" Command="{Binding SelectSourceFolderCommand}" ToolTip.Tip="Seleziona cartella sorgente">
|
||||
|
|
@ -31,9 +18,15 @@
|
|||
<TextBlock Text="Scegli" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Grid.Column="3" Click="OpenSourcePath_Click" ToolTip.Tip="Apri cartella sorgente in Esplora file">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
|
||||
<iconPacks:PackIconMaterial Kind="Folder" Width="16" Height="16" />
|
||||
<TextBlock Text="Apri" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.Row="2" ColumnDefinitions="150,*,116" ColumnSpacing="10">
|
||||
<Grid Grid.Row="1" ColumnDefinitions="150,*,104,72" ColumnSpacing="10">
|
||||
<TextBlock Text="Destinazione" VerticalAlignment="Center" FontWeight="SemiBold" />
|
||||
<TextBox Grid.Column="1" Text="{Binding DestinationPath, Mode=TwoWay}" Watermark="Cartella destinazione" />
|
||||
<Button Grid.Column="2" Command="{Binding SelectDestinationFolderCommand}" ToolTip.Tip="Seleziona cartella destinazione">
|
||||
|
|
@ -42,8 +35,39 @@
|
|||
<TextBlock Text="Scegli" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Grid.Column="3" Click="OpenDestinationPath_Click" ToolTip.Tip="Apri cartella destinazione in Esplora file">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
|
||||
<iconPacks:PackIconMaterial Kind="Folder" Width="16" Height="16" />
|
||||
<TextBlock Text="Apri" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<Border Grid.Row="2"
|
||||
Background="{DynamicResource PanelBackgroundBrush}"
|
||||
BorderBrush="{DynamicResource BorderMutedBrush}"
|
||||
BorderThickness="1"
|
||||
Padding="14">
|
||||
<Grid RowDefinitions="Auto,Auto" RowSpacing="10">
|
||||
<Grid ColumnDefinitions="150,*" ColumnSpacing="10">
|
||||
<TextBlock Text="Testo orizzontale" VerticalAlignment="Center" FontWeight="SemiBold" />
|
||||
<TextBox Grid.Column="1"
|
||||
Text="{Binding HorizontalText, Mode=TwoWay}"
|
||||
Watermark="Testo applicato alle foto orizzontali" />
|
||||
</Grid>
|
||||
<Grid Grid.Row="1" ColumnDefinitions="150,*" ColumnSpacing="10">
|
||||
<TextBlock Text="Testo verticale" VerticalAlignment="Top" Margin="0,8,0,0" FontWeight="SemiBold" />
|
||||
<TextBox Grid.Column="1"
|
||||
Text="{Binding VerticalText, Mode=TwoWay}"
|
||||
AcceptsReturn="True"
|
||||
TextWrapping="Wrap"
|
||||
MinHeight="96"
|
||||
VerticalContentAlignment="Top"
|
||||
Watermark="Testo multilinea per foto verticali" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<Border Grid.Row="3"
|
||||
Background="{DynamicResource PanelBackgroundBrush}"
|
||||
BorderBrush="{DynamicResource BorderMutedBrush}"
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ using Avalonia.Controls;
|
|||
using Avalonia.Platform.Storage;
|
||||
using Avalonia.Threading;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
|
||||
namespace CatalogLite;
|
||||
|
||||
|
|
@ -21,24 +23,10 @@ public partial class MainWindow : Window
|
|||
_viewModel = viewModel;
|
||||
DataContext = _viewModel;
|
||||
_viewModel.UiInvoker = action => Dispatcher.UIThread.Invoke(action);
|
||||
_viewModel.LoadConfigurationRequested += OnLoadConfigurationRequested;
|
||||
_viewModel.SelectSourceFolderRequested += OnSelectSourceFolderRequested;
|
||||
_viewModel.SelectDestinationFolderRequested += OnSelectDestinationFolderRequested;
|
||||
_viewModel.ShowMessageRequested += OnShowMessageRequested;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
_viewModel.LoadEmbeddedConfiguration();
|
||||
}
|
||||
|
||||
private async void OnSelectSourceFolderRequested(object? sender, EventArgs e)
|
||||
|
|
@ -67,6 +55,16 @@ 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);
|
||||
|
|
@ -110,4 +108,46 @@ 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
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -283,9 +283,120 @@ 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<ILogger<SettingsService>>());
|
||||
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)
|
||||
ITestService? testService = null,
|
||||
PickerPreferenceService? pickerPreferenceService = null)
|
||||
{
|
||||
var mapper = Substitute.For<AutoMapper.IMapper>();
|
||||
var picSettings = new PicSettings();
|
||||
|
|
@ -316,7 +427,8 @@ public class DataModelCharacterizationTests
|
|||
picSettings,
|
||||
mapper,
|
||||
Substitute.For<ILogger<DataModel>>(),
|
||||
versionProvider: null);
|
||||
versionProvider: null,
|
||||
pickerPreferenceService: pickerPreferenceService);
|
||||
}
|
||||
|
||||
private static string CreateFaceEncoderExecutable(string rootPath, string variant)
|
||||
|
|
|
|||
|
|
@ -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 = null;
|
||||
if (picSettings.LogoAggiungi && File.Exists(picSettings.LogoNomeFile))
|
||||
byte[]? logoBytes = picSettings.LogoData;
|
||||
if (logoBytes is null && picSettings.LogoAggiungi && File.Exists(picSettings.LogoNomeFile))
|
||||
{
|
||||
logoBytes = File.ReadAllBytes(picSettings.LogoNomeFile);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ 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; }
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@
|
|||
<views:FaceAiTabView />
|
||||
</TabItem>
|
||||
|
||||
<TabItem>
|
||||
<TabItem IsVisible="False">
|
||||
<TabItem.Header>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<iconPacks:PackIconMaterial Kind="CloudUploadOutline" Width="14" Height="14" Margin="0,0,4,0" Foreground="{DynamicResource ForegroundBrush}" />
|
||||
|
|
|
|||
|
|
@ -273,10 +273,11 @@ public partial class AvaloniaMainWindow : Window
|
|||
var dialog = new Window
|
||||
{
|
||||
Title = title,
|
||||
Width = 480,
|
||||
Width = 960,
|
||||
Height = 560,
|
||||
CanResize = false,
|
||||
WindowStartupLocation = WindowStartupLocation.CenterOwner,
|
||||
SizeToContent = SizeToContent.Height
|
||||
SizeToContent = SizeToContent.Manual
|
||||
};
|
||||
|
||||
dialog.Content = BuildMessageDialogContent(message, () => dialog.Close());
|
||||
|
|
@ -311,11 +312,17 @@ public partial class AvaloniaMainWindow : Window
|
|||
Spacing = 12
|
||||
};
|
||||
|
||||
layout.Children.Add(new TextBlock
|
||||
layout.Children.Add(new ScrollViewer
|
||||
{
|
||||
Text = message,
|
||||
TextWrapping = Avalonia.Media.TextWrapping.Wrap,
|
||||
MaxWidth = 420
|
||||
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")
|
||||
}
|
||||
});
|
||||
|
||||
var closeButton = new Button
|
||||
|
|
|
|||
|
|
@ -79,6 +79,19 @@
|
|||
<TextBlock VerticalAlignment="Center" Text="{Binding FaceStatusMessage}" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
|
||||
<Grid ColumnDefinitions="Auto,320,Auto,*" ColumnSpacing="6" Margin="0,2,0,0">
|
||||
<TextBlock Grid.Column="0" Text="Upload:" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Column="1"
|
||||
Text="{Binding FaceUploadPath, Mode=TwoWay}"
|
||||
Watermark="2026/05.MAGGIO/EMPOLI" />
|
||||
<Button Grid.Column="2" Command="{Binding UploadFaceEncoderOutputCommand}" Width="120">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
|
||||
<iconPacks:PackIconMaterial Kind="CloudUploadOutline" Width="16" Height="16" Foreground="#1565C0" />
|
||||
<TextBlock Text="Upload" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<TextBlock Text="Output comando" FontWeight="Bold" Margin="0,6,0,0" />
|
||||
<TextBox Name="FaceOutputTextBox"
|
||||
Text="{Binding FaceCommandOutput}"
|
||||
|
|
@ -110,6 +123,31 @@
|
|||
PreferenceKey="Picker.FaceExecutableFolder.LastPath"
|
||||
PickerTitle="Seleziona la cartella Face Recognition Windows"
|
||||
PickerMode="Folder" />
|
||||
|
||||
<TextBlock Text="SSH upload" FontWeight="Bold" Margin="0,8,0,0" />
|
||||
<Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto" RowSpacing="6" ColumnSpacing="8">
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="Username:" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Row="0" Grid.Column="1" Text="{Binding FaceSshUsername, Mode=TwoWay}" />
|
||||
|
||||
<TextBlock Grid.Row="1" Grid.Column="0" Text="Password:" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding FaceSshPassword, Mode=TwoWay}" PasswordChar="*" />
|
||||
|
||||
<TextBlock Grid.Row="2" Grid.Column="0" Text="Server:" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Row="2" Grid.Column="1" Text="{Binding FaceSshAddress, Mode=TwoWay}" Watermark="example.org" />
|
||||
|
||||
<TextBlock Grid.Row="3" Grid.Column="0" Text="Porta:" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Row="3" Grid.Column="1" Text="{Binding FaceSshPort, Mode=TwoWay}" Watermark="22" />
|
||||
|
||||
<TextBlock Grid.Row="4" Grid.Column="0" Text="Path A:" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Row="4" Grid.Column="1" Text="{Binding FaceSshPathA, Mode=TwoWay}" Watermark="/mnt/da1/foto/" />
|
||||
|
||||
<TextBlock Grid.Row="5" Grid.Column="0" Text="Path B:" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Row="5" Grid.Column="1" Text="{Binding FaceSshPathB, Mode=TwoWay}" Watermark="/mnt/nas12/foto/" />
|
||||
|
||||
<CheckBox Grid.Row="6" Grid.ColumnSpan="2"
|
||||
Content="Dry run upload Face AI"
|
||||
IsChecked="{Binding FaceUploadDryRun, Mode=TwoWay}" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</TabItem>
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ 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; }
|
||||
|
||||
|
|
@ -50,6 +51,7 @@ 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;
|
||||
|
|
@ -59,6 +61,7 @@ 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();
|
||||
|
|
@ -75,12 +78,15 @@ 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<string> AvailableFonts { get; }
|
||||
|
|
@ -90,7 +96,7 @@ namespace ImageCatalog_2
|
|||
[CLSCompliant(false)]
|
||||
public DataModel(ITestService testService, ISettingsService settingsService,
|
||||
ImageCreationService imageCreationService, IAiExtractionService aiExtractionService, IImageProcessingCoordinator imageProcessingCoordinator, PicSettings picSettings,
|
||||
IMapper mapper, ILogger<DataModel> logger, MaddoShared.IVersionProvider? versionProvider = null)
|
||||
IMapper mapper, ILogger<DataModel> logger, MaddoShared.IVersionProvider? versionProvider = null, PickerPreferenceService? pickerPreferenceService = null)
|
||||
{
|
||||
_service = testService;
|
||||
_logger = logger;
|
||||
|
|
@ -98,6 +104,7 @@ namespace ImageCatalog_2
|
|||
_imageCreationService = imageCreationService;
|
||||
_aiExtractionService = aiExtractionService;
|
||||
_imageProcessingCoordinator = imageProcessingCoordinator;
|
||||
_pickerPreferenceService = pickerPreferenceService;
|
||||
_processing = new ProcessingStateViewModel();
|
||||
_processing.PropertyChanged += OnProcessingPropertyChanged;
|
||||
_paths = new PathSettingsViewModel();
|
||||
|
|
@ -122,10 +129,12 @@ 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;
|
||||
|
||||
|
|
@ -139,6 +148,7 @@ namespace ImageCatalog_2
|
|||
|
||||
// Load available fonts
|
||||
AvailableFonts = LoadAvailableFonts();
|
||||
LoadFaceSshUserPreferences();
|
||||
QueueRefreshNumberAiGpuCapabilities();
|
||||
RefreshFaceExecutableCapabilities();
|
||||
}
|
||||
|
|
@ -330,6 +340,67 @@ 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;
|
||||
|
|
@ -668,7 +739,13 @@ namespace ImageCatalog_2
|
|||
}
|
||||
|
||||
NotifyPropertyChanged(e.PropertyName);
|
||||
if (string.Equals(e.PropertyName, nameof(AiSettingsViewModel.FaceUploadPath), StringComparison.Ordinal))
|
||||
{
|
||||
NotifyPropertyChanged(nameof(IsFaceUploadPathValid));
|
||||
}
|
||||
|
||||
UpdateFaceEncoderCommandStates();
|
||||
UpdateFaceUploadCommandStates();
|
||||
UpdateFaceMatcherCommandStates();
|
||||
}
|
||||
|
||||
|
|
@ -1572,6 +1649,11 @@ namespace ImageCatalog_2
|
|||
_stopFaceEncoderCommand?.RaiseCanExecuteChanged();
|
||||
}
|
||||
|
||||
private void UpdateFaceUploadCommandStates()
|
||||
{
|
||||
_uploadFaceEncoderOutputCommand?.RaiseCanExecuteChanged();
|
||||
}
|
||||
|
||||
private void UpdateFaceMatcherCommandStates()
|
||||
{
|
||||
_startFaceMatcherCommand?.RaiseCanExecuteChanged();
|
||||
|
|
@ -1631,6 +1713,7 @@ 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;
|
||||
|
|
@ -1748,6 +1831,66 @@ 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;
|
||||
|
|
@ -2361,7 +2504,7 @@ namespace ImageCatalog_2
|
|||
&& File.Exists(configuration.RecognitionWeights);
|
||||
}
|
||||
|
||||
private Task ShowErrorMessageAsync(string title, string message)
|
||||
private Task ShowMessageAsync(string title, string message)
|
||||
{
|
||||
return InvokeOnUiThreadAsync(() =>
|
||||
{
|
||||
|
|
@ -2369,6 +2512,11 @@ namespace ImageCatalog_2
|
|||
});
|
||||
}
|
||||
|
||||
private Task ShowErrorMessageAsync(string title, string message)
|
||||
{
|
||||
return ShowMessageAsync(title, message);
|
||||
}
|
||||
|
||||
internal async Task<bool> ConfirmAiCsvOverwriteIfNeededAsync()
|
||||
{
|
||||
var csvOutputPath = NormalizeFilePathArgument(CsvOutputPath);
|
||||
|
|
@ -2897,6 +3045,253 @@ 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<string> setValue, string value)
|
||||
{
|
||||
setValue(value);
|
||||
if (_isLoadingFaceSshUserPreferences)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_pickerPreferenceService?.RememberRawValue(preferenceKey, value);
|
||||
}
|
||||
|
||||
private void SetFaceSshPreferenceBoolValue(string preferenceKey, Action<bool> 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<string> 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<string> RemoteDirectories);
|
||||
|
||||
internal static string BuildSafeFaceEncoderRaceName(string imagesFolderPath)
|
||||
{
|
||||
var raceName = new DirectoryInfo(imagesFolderPath).Name;
|
||||
|
|
|
|||
|
|
@ -289,6 +289,10 @@ 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; }
|
||||
|
|
|
|||
|
|
@ -136,8 +136,9 @@ static class Program
|
|||
var mapper = sp.GetRequiredService<IMapper>();
|
||||
var logger = sp.GetRequiredService<ILogger<DataModel>>();
|
||||
var versionProvider = sp.GetService<MaddoShared.IVersionProvider>();
|
||||
var pickerPreferenceService = sp.GetRequiredService<PickerPreferenceService>();
|
||||
|
||||
return new DataModel(testService, settingsService, imageCreation, aiExtractionService, imageProcessingCoordinator, picSettings, mapper, logger, versionProvider);
|
||||
return new DataModel(testService, settingsService, imageCreation, aiExtractionService, imageProcessingCoordinator, picSettings, mapper, logger, versionProvider, pickerPreferenceService);
|
||||
});
|
||||
|
||||
services.AddTransient<IAiExtractionService, AiExtractionService>();
|
||||
|
|
|
|||
|
|
@ -22,6 +22,13 @@ 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
|
||||
|
|
@ -71,6 +78,13 @@ 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))
|
||||
|
|
@ -82,6 +96,12 @@ 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))
|
||||
|
|
|
|||
|
|
@ -115,6 +115,105 @@ 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
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue