feat: Enhance Face AI upload functionality and UI
Some checks failed
Build Windows Avalonia / build (push) Failing after 1m48s
Release Windows Avalonia / build (push) Failing after 1m41s
Release Windows Avalonia / release (push) Has been skipped

- 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:
Maddo 2026-06-06 11:54:21 +02:00
commit e9142df97c
22 changed files with 1477 additions and 84 deletions

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 KiB

View 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>

View file

@ -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));
}
}

View file

@ -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)

View file

@ -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>

View file

@ -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();

View file

@ -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}"

View file

@ -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
{
}
}
}