diff --git a/imagecatalog/AvaloniaMainWindow.axaml b/imagecatalog/AvaloniaMainWindow.axaml
index 6165691..4517b63 100644
--- a/imagecatalog/AvaloniaMainWindow.axaml
+++ b/imagecatalog/AvaloniaMainWindow.axaml
@@ -2,6 +2,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:views="clr-namespace:ImageCatalog_2.AvaloniaViews"
x:Class="ImageCatalog_2.AvaloniaMainWindow"
mc:Ignorable="d"
Title="Image Catalog - Avalonia" Height="540" Width="800">
@@ -14,389 +15,43 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Nessuna
- Aggiungi scritta
- Nome file
- Aggiungi orario
- Nome+Orario
- Tempo gara
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
@@ -425,14 +80,13 @@
-
+
-
diff --git a/imagecatalog/AvaloniaMainWindow.axaml.cs b/imagecatalog/AvaloniaMainWindow.axaml.cs
index 53bdba0..96dcfc9 100644
--- a/imagecatalog/AvaloniaMainWindow.axaml.cs
+++ b/imagecatalog/AvaloniaMainWindow.axaml.cs
@@ -1,60 +1,53 @@
-using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
-using Avalonia.Media.Imaging;
using Avalonia.Platform.Storage;
using Avalonia.Styling;
using Avalonia.Threading;
-using Catalog.Communication.Abstractions;
-using Catalog.Communication.Models;
-using ImageCatalog;
-using Microsoft.Extensions.Logging;
-using System;
-using System.Diagnostics;
using System.IO;
-using System.Linq;
-using System.Net;
-using System.Text;
-using System.Text.RegularExpressions;
-using System.Threading;
-using System.Threading.Tasks;
namespace ImageCatalog_2;
public partial class AvaloniaMainWindow : Window
{
private readonly DataModel _model;
- private readonly IRaceUploadCommunicationClient _apiClient;
- private readonly ILogger _logger;
- private bool _isDarkTheme = false;
+ private bool _isDarkTheme;
- public AvaloniaMainWindow(
- DataModel model,
- IRaceUploadCommunicationClient apiClient,
- ILogger logger)
+ public AvaloniaMainWindow(DataModel model)
{
InitializeComponent();
+
_model = model;
- _apiClient = apiClient;
- _logger = logger;
DataContext = _model;
Opened += (_, _) => SyncThemeStateFromCurrentTheme();
- // Provide Avalonia dispatcher so DataModel can marshal UI updates
+ // Let DataModel marshal callbacks onto Avalonia UI thread.
_model.UiInvoker = action => Dispatcher.UIThread.Invoke(action);
- // Wire dialog events
_model.SelectSourceFolderRequested += async (_, _) =>
{
- var folders = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions { Title = "Seleziona cartella sorgente" });
- if (folders.Count > 0) _model.SourcePath = folders[0].Path.LocalPath + Path.DirectorySeparatorChar;
+ var folders = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
+ {
+ Title = "Seleziona cartella sorgente"
+ });
+
+ if (folders.Count > 0)
+ {
+ _model.SourcePath = folders[0].Path.LocalPath + Path.DirectorySeparatorChar;
+ }
};
_model.SelectDestinationFolderRequested += async (_, _) =>
{
- var folders = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions { Title = "Seleziona cartella destinazione" });
- if (folders.Count > 0) _model.DestinationPath = folders[0].Path.LocalPath + Path.DirectorySeparatorChar;
+ var folders = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
+ {
+ Title = "Seleziona cartella destinazione"
+ });
+
+ if (folders.Count > 0)
+ {
+ _model.DestinationPath = folders[0].Path.LocalPath + Path.DirectorySeparatorChar;
+ }
};
_model.SelectLogoFileRequested += async (_, _) =>
@@ -62,19 +55,29 @@ public partial class AvaloniaMainWindow : Window
var files = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
{
Title = "Seleziona logo",
- FileTypeFilter = new[] { new FilePickerFileType("Immagini") { Patterns = new[] { "*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif" } } }
+ FileTypeFilter =
+ [
+ new FilePickerFileType("Immagini") { Patterns = ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif"] }
+ ]
});
+
if (files.Count > 0)
{
_model.LogoFile = files[0].Path.LocalPath;
- UpdateLogoPreview(_model.LogoFile);
}
};
_model.SelectModelsFolderRequested += async (_, _) =>
{
- var folders = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions { Title = "Seleziona cartella modelli" });
- if (folders.Count > 0) _model.ModelsFolderPath = folders[0].Path.LocalPath + Path.DirectorySeparatorChar;
+ var folders = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
+ {
+ Title = "Seleziona cartella modelli"
+ });
+
+ if (folders.Count > 0)
+ {
+ _model.ModelsFolderPath = folders[0].Path.LocalPath + Path.DirectorySeparatorChar;
+ }
};
_model.SelectCsvOutputRequested += async (_, _) =>
@@ -83,9 +86,13 @@ public partial class AvaloniaMainWindow : Window
{
Title = "Salva CSV",
DefaultExtension = "csv",
- FileTypeChoices = new[] { new FilePickerFileType("CSV") { Patterns = new[] { "*.csv" } } }
+ FileTypeChoices = [new FilePickerFileType("CSV") { Patterns = ["*.csv"] }]
});
- if (file != null) _model.CsvOutputPath = file.Path.LocalPath;
+
+ if (file is not null)
+ {
+ _model.CsvOutputPath = file.Path.LocalPath;
+ }
};
_model.SaveSettingsRequested += async (_, _) =>
@@ -94,9 +101,13 @@ public partial class AvaloniaMainWindow : Window
{
Title = "Salva impostazioni",
DefaultExtension = "xml",
- FileTypeChoices = new[] { new FilePickerFileType("Setup") { Patterns = new[] { "*.xml" } } }
+ FileTypeChoices = [new FilePickerFileType("Setup") { Patterns = ["*.xml"] }]
});
- if (file != null) await _model.SaveSettingsToFileAsync(file.Path.LocalPath);
+
+ if (file is not null)
+ {
+ await _model.SaveSettingsToFileAsync(file.Path.LocalPath);
+ }
};
_model.LoadSettingsRequested += async (_, _) =>
@@ -104,620 +115,41 @@ public partial class AvaloniaMainWindow : Window
var files = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
{
Title = "Carica impostazioni",
- FileTypeFilter = new[] { new FilePickerFileType("Setup") { Patterns = new[] { "*.xml" } } }
+ FileTypeFilter = [new FilePickerFileType("Setup") { Patterns = ["*.xml"] }]
});
- if (files.Count > 0) await _model.LoadSettingsFromFileAsync(files[0].Path.LocalPath);
+
+ if (files.Count > 0)
+ {
+ await _model.LoadSettingsFromFileAsync(files[0].Path.LocalPath);
+ }
};
_model.SelectColorRequested += (_, _) =>
{
- // Color is set by typing hex directly in the TextBox
+ // Color is set by typing hex directly in the TextBox.
};
_model.SelectTransparentColorRequested += (_, _) =>
{
- // Color is set by typing hex directly in the TextBox
- };
-
- _model.PropertyChanged += (_, e) =>
- {
- if (e.PropertyName == nameof(_model.LogoFile))
- UpdateLogoPreview(_model.LogoFile);
+ // Color is set by typing hex directly in the TextBox.
};
}
private void ToggleTheme_Click(object? sender, RoutedEventArgs e)
{
_isDarkTheme = !_isDarkTheme;
- if (Avalonia.Application.Current != null)
+
+ if (Avalonia.Application.Current is not null)
+ {
Avalonia.Application.Current.RequestedThemeVariant = _isDarkTheme ? ThemeVariant.Dark : ThemeVariant.Light;
+ }
UpdateThemeToggleButtonContent();
}
- private void OpenSourceFolder_Click(object? sender, RoutedEventArgs e) => OpenInExplorer(_model.SourcePath);
- private void OpenDestinationFolder_Click(object? sender, RoutedEventArgs e) => OpenInExplorer(_model.DestinationPath);
- private void OpenModelsFolder_Click(object? sender, RoutedEventArgs e) => OpenInExplorer(_model.ModelsFolderPath);
- private void OpenCsvOutputFolder_Click(object? sender, RoutedEventArgs e)
- {
- var dir = Path.GetDirectoryName(_model.CsvOutputPath);
- OpenInExplorer(string.IsNullOrWhiteSpace(dir) ? _model.CsvOutputPath : dir);
- }
-
- private static void OpenInExplorer(string? path)
- {
- if (string.IsNullOrWhiteSpace(path)) return;
- path = path.Trim().Trim('"');
- try
- {
- if (File.Exists(path))
- System.Diagnostics.Process.Start("explorer.exe", $"/select,\"{path}\"");
- else if (Directory.Exists(path))
- System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = path, UseShellExecute = true });
- }
- catch { }
- }
-
- private void UpdateLogoPreview(string? path)
- {
- var preview = this.FindControl("LogoPreview");
- if (preview == null) return;
- if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
- {
- preview.Source = null;
- return;
- }
- try { preview.Source = new Avalonia.Media.Imaging.Bitmap(path); }
- catch { preview.Source = null; }
- }
-
- private async void CreateRace_Click(object? sender, RoutedEventArgs e)
- {
- var outputBox = this.FindControl("ApiOutputTextBox");
- var statusBlock = this.FindControl("ApiStatusTextBlock");
- var createButton = this.FindControl("ApiCreateRaceButton");
- var uploadButton = this.FindControl("ApiUploadButton");
-
- if (outputBox is null || statusBlock is null || createButton is null || uploadButton is null)
- {
- return;
- }
-
- var login = _model.ApiLogin?.Trim() ?? string.Empty;
- var password = _model.ApiPassword ?? string.Empty;
- var descriptionRaw = _model.ApiRaceDescription?.Trim() ?? string.Empty;
-
- if (string.IsNullOrWhiteSpace(login) || string.IsNullOrWhiteSpace(password) || string.IsNullOrWhiteSpace(descriptionRaw))
- {
- statusBlock.Text = "Inserisci login, password e descrizione gara.";
- return;
- }
-
- if (!long.TryParse(_model.ApiRaceTypeId?.Trim(), out var tipoGaraId) || tipoGaraId <= 0)
- {
- statusBlock.Text = "Tipo gara non valido.";
- return;
- }
-
- createButton.IsEnabled = false;
- uploadButton.IsEnabled = false;
- statusBlock.Text = "Creazione gara in corso...";
- outputBox.Text = string.Empty;
-
- try
- {
- var startDate = DateOnly.FromDateTime(_model.ApiRaceStartDate.Date);
- var endDate = DateOnly.FromDateTime((_model.ApiRaceEndDate == default ? _model.ApiRaceStartDate : _model.ApiRaceEndDate).Date);
- var sanitizedDescription = SanitizeRaceDescription(descriptionRaw);
-
- var loginResponse = await LoginAsync(login, password).ConfigureAwait(true);
-
- var saveResponse = await _apiClient.SaveRaceAsync(
- new RaceSaveRequest
- {
- IdGara = 0,
- Description = sanitizedDescription,
- StartDate = startDate,
- EndDate = endDate,
- TipoGaraId = tipoGaraId,
- EventoInLinea = _model.ApiEventoInLineaIndex,
- TipoIndicizzazione = _model.ApiTipoIndexValue,
- FreeEvent = _model.ApiFreeEventIndex,
- PathBase = _model.ApiPathBase?.Trim(),
- Localita = _model.ApiLocalita?.Trim(),
- },
- CancellationToken.None);
-
- var raceId = ExtractRaceId(saveResponse.Body);
- if (raceId <= 0)
- {
- throw new InvalidOperationException("Impossibile ricavare id_gara dalla risposta di salvataggio.");
- }
-
- _model.ApiRaceId = raceId.ToString();
-
- var createPointsResponse = await _apiClient.CreateRacePointsAsync(raceId, CancellationToken.None);
-
- var sb = new StringBuilder();
- sb.AppendLine($"Login HTTP: {(int)loginResponse.StatusCode} {loginResponse.StatusCode}");
- sb.AppendLine($"Save Gara HTTP: {(int)saveResponse.StatusCode} {saveResponse.StatusCode}");
- sb.AppendLine($"Crea Punti HTTP: {(int)createPointsResponse.StatusCode} {createPointsResponse.StatusCode}");
- sb.AppendLine($"id_gara: {raceId}");
- sb.AppendLine();
- sb.AppendLine("Gara creata e avvio creazione punti richiesto.");
-
- outputBox.Text = sb.ToString();
- statusBlock.Text = "Gara creata.";
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Race creation failed in Avalonia tab.");
- outputBox.Text = ex.ToString();
- statusBlock.Text = "Errore durante la creazione gara.";
- }
- finally
- {
- createButton.IsEnabled = true;
- uploadButton.IsEnabled = true;
- }
- }
-
- private async void UploadProcessed_Click(object? sender, RoutedEventArgs e)
- {
- var outputBox = this.FindControl("ApiOutputTextBox");
- var statusBlock = this.FindControl("ApiStatusTextBlock");
- var createButton = this.FindControl("ApiCreateRaceButton");
- var uploadButton = this.FindControl("ApiUploadButton");
-
- if (outputBox is null || statusBlock is null || createButton is null || uploadButton is null)
- {
- return;
- }
-
- var login = _model.ApiLogin?.Trim() ?? string.Empty;
- var password = _model.ApiPassword ?? string.Empty;
- var racePathBase = _model.ApiPathBase?.Trim() ?? string.Empty;
- var remoteProcessedBase = _model.ApiRemoteProcessedBasePath?.Trim() ?? string.Empty;
-
- if (!long.TryParse(_model.ApiRaceId?.Trim(), out var raceId) || raceId <= 0)
- {
- statusBlock.Text = "id_gara non valido.";
- return;
- }
-
- if (string.IsNullOrWhiteSpace(login) || string.IsNullOrWhiteSpace(password))
- {
- statusBlock.Text = "Inserisci login e password.";
- return;
- }
-
- if (string.IsNullOrWhiteSpace(_model.DestinationPath) || !Directory.Exists(_model.DestinationPath))
- {
- statusBlock.Text = "Cartella destinazione locale non valida.";
- return;
- }
-
- if (string.IsNullOrWhiteSpace(remoteProcessedBase))
- {
- statusBlock.Text = "Inserisci il path base remoto per le foto processate.";
- return;
- }
-
- createButton.IsEnabled = false;
- uploadButton.IsEnabled = false;
- statusBlock.Text = "Upload foto processate in corso...";
-
- try
- {
- await LoginAsync(login, password).ConfigureAwait(true);
-
- var files = Directory
- .EnumerateFiles(_model.DestinationPath, "*.*", SearchOption.AllDirectories)
- .Where(IsSupportedImage)
- .ToList();
-
- if (files.Count == 0)
- {
- statusBlock.Text = "Nessuna immagine trovata in destinazione.";
- outputBox.Text = "Nessun file processato da inviare.";
- return;
- }
-
- var uploaded = 0;
- var sb = new StringBuilder();
- sb.AppendLine($"File da inviare: {files.Count}");
-
- foreach (var file in files)
- {
- var relativePath = Path.GetRelativePath(_model.DestinationPath, file);
- var relativeDir = Path.GetDirectoryName(relativePath) ?? string.Empty;
- var remotePath = CombineRemotePath(remoteProcessedBase, racePathBase, relativeDir);
-
- await using var stream = File.OpenRead(file);
- await _apiClient.UploadFileToReceiverAsync(
- new ReceiveFileUploadRequest
- {
- FileName = Path.GetFileName(file),
- FileStream = stream,
- DestinationPath = remotePath,
- OverwriteRemoteFile = true,
- },
- CancellationToken.None).ConfigureAwait(true);
-
- uploaded++;
- if (uploaded % 20 == 0 || uploaded == files.Count)
- {
- statusBlock.Text = $"Upload foto: {uploaded}/{files.Count}";
- }
- }
-
- sb.AppendLine($"Upload completato: {uploaded}/{files.Count}");
- statusBlock.Text = "Creazione punti foto e indicizzazione in corso...";
-
- await _apiClient.CreateRacePointsAsync(raceId, CancellationToken.None).ConfigureAwait(true);
- var pointIds = await LoadPointIdsWithRetryAsync(raceId, CancellationToken.None).ConfigureAwait(true);
-
- foreach (var pointId in pointIds)
- {
- await _apiClient.IndexRacePointAsync(pointId, CancellationToken.None).ConfigureAwait(true);
- }
-
- sb.AppendLine($"Punti foto indicizzati: {pointIds.Count}");
- outputBox.Text = sb.ToString();
- statusBlock.Text = "Upload e indicizzazione completati.";
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Upload flow failed in Avalonia tab.");
- outputBox.Text = ex.ToString();
- statusBlock.Text = "Errore durante upload/indicizzazione.";
- }
- finally
- {
- createButton.IsEnabled = true;
- uploadButton.IsEnabled = true;
- }
- }
-
- private async Task LoginAsync(string login, string password)
- {
- return await _apiClient.LoginAdminAsync(
- new AdminLoginRequest
- {
- Login = login,
- Password = password,
- Command = "check",
- },
- CancellationToken.None).ConfigureAwait(false);
- }
-
- private async Task> LoadPointIdsWithRetryAsync(long raceId, CancellationToken cancellationToken)
- {
- const int maxAttempts = 10;
-
- for (var attempt = 1; attempt <= maxAttempts; attempt++)
- {
- var response = await _apiClient.GetRaceDetailAsync(raceId, cancellationToken).ConfigureAwait(false);
- var ids = ExtractPointIds(response.Body);
-
- if (ids.Count > 0)
- {
- return ids;
- }
-
- await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken).ConfigureAwait(false);
- }
-
- return new List();
- }
-
- private static List ExtractPointIds(string html)
- {
- var ids = Regex
- .Matches(html ?? string.Empty, @"indexFoto\((\d+)\)", RegexOptions.IgnoreCase)
- .Select(m => long.TryParse(m.Groups[1].Value, out var value) ? value : 0L)
- .Where(v => v > 0)
- .Distinct()
- .ToList();
-
- return ids;
- }
-
- private static string SanitizeRaceDescription(string value)
- {
- if (string.IsNullOrWhiteSpace(value))
- {
- return string.Empty;
- }
-
- var cleaned = Regex.Replace(value, "[^A-Za-z0-9 _-]", " ");
- return Regex.Replace(cleaned, "\\s+", " ").Trim();
- }
-
- private static string CombineRemotePath(string remoteBase, string racePathBase, string relativeDir)
- {
- var segments = new[] { remoteBase, racePathBase, relativeDir }
- .Where(s => !string.IsNullOrWhiteSpace(s))
- .Select(s => s!.Replace('\\', '/').Trim('/'));
-
- var joined = string.Join('/', segments);
- return string.IsNullOrWhiteSpace(joined) ? "/" : joined + "/";
- }
-
- private static bool IsSupportedImage(string filePath)
- {
- var extension = Path.GetExtension(filePath);
- if (string.IsNullOrWhiteSpace(extension))
- {
- return false;
- }
-
- return extension.Equals(".jpg", StringComparison.OrdinalIgnoreCase)
- || extension.Equals(".jpeg", StringComparison.OrdinalIgnoreCase)
- || extension.Equals(".png", StringComparison.OrdinalIgnoreCase)
- || extension.Equals(".bmp", StringComparison.OrdinalIgnoreCase)
- || extension.Equals(".gif", StringComparison.OrdinalIgnoreCase);
- }
-
- private async void SelectFaceExecutable_Click(object? sender, RoutedEventArgs e)
- {
- var executableBox = this.FindControl("FaceExecutablePathTextBox");
- if (executableBox is null)
- {
- return;
- }
-
- var files = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
- {
- Title = "Seleziona face_encoder.exe",
- FileTypeFilter = new[]
- {
- new FilePickerFileType("Eseguibile") { Patterns = new[] { "*.exe" } },
- new FilePickerFileType("Tutti i file") { Patterns = new[] { "*.*" } }
- }
- });
-
- if (files.Count > 0)
- {
- executableBox.Text = files[0].Path.LocalPath;
- _model.FaceExecutablePath = executableBox.Text;
- }
- }
-
- private async void SelectFaceOutputFolder_Click(object? sender, RoutedEventArgs e)
- {
- var outputBox = this.FindControl("FaceOutputFolderTextBox");
- if (outputBox is null)
- {
- return;
- }
-
- var folders = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
- {
- Title = "Seleziona cartella output encodings"
- });
-
- if (folders.Count > 0)
- {
- outputBox.Text = folders[0].Path.LocalPath;
- _model.FaceOutputFolderPath = outputBox.Text;
- }
- }
-
- private void OpenFaceExecutableFolder_Click(object? sender, RoutedEventArgs e)
- {
- var executableBox = this.FindControl("FaceExecutablePathTextBox");
- if (executableBox is null)
- {
- return;
- }
-
- var path = executableBox.Text?.Trim();
- if (string.IsNullOrWhiteSpace(path))
- {
- return;
- }
-
- if (File.Exists(path))
- {
- OpenInExplorer(path);
- return;
- }
-
- var dir = Path.GetDirectoryName(path);
- OpenInExplorer(string.IsNullOrWhiteSpace(dir) ? path : dir);
- }
-
- private void OpenFaceOutputFolder_Click(object? sender, RoutedEventArgs e)
- {
- var outputBox = this.FindControl("FaceOutputFolderTextBox");
- if (outputBox is null)
- {
- return;
- }
-
- OpenInExplorer(outputBox.Text);
- }
-
- private async void RunFaceEncoder_Click(object? sender, RoutedEventArgs e)
- {
- var executableBox = this.FindControl("FaceExecutablePathTextBox");
- var outputFolderBox = this.FindControl("FaceOutputFolderTextBox");
- var outputLogBox = this.FindControl("FaceOutputTextBox");
- var statusBlock = this.FindControl("FaceStatusTextBlock");
- var runButton = this.FindControl("FaceRunButton");
-
- if (executableBox is null || outputFolderBox is null || outputLogBox is null || statusBlock is null || runButton is null)
- {
- return;
- }
-
- var executablePath = executableBox.Text?.Trim().Trim('"') ?? string.Empty;
- var outputFolder = outputFolderBox.Text?.Trim().Trim('"') ?? string.Empty;
- var imagesFolder = (_model.DestinationPath ?? string.Empty).Trim().Trim('"');
-
- _model.FaceExecutablePath = executablePath;
- _model.FaceOutputFolderPath = outputFolder;
-
- if (string.IsNullOrWhiteSpace(executablePath) || !File.Exists(executablePath))
- {
- statusBlock.Text = "Percorso eseguibile non valido.";
- return;
- }
-
- if (string.IsNullOrWhiteSpace(imagesFolder) || !Directory.Exists(imagesFolder))
- {
- statusBlock.Text = "Cartella Destinazione non valida.";
- return;
- }
-
- if (string.IsNullOrWhiteSpace(outputFolder))
- {
- statusBlock.Text = "Inserisci la cartella di output.";
- return;
- }
-
- try
- {
- Directory.CreateDirectory(outputFolder);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Unable to create face output folder: {OutputFolder}", outputFolder);
- statusBlock.Text = "Impossibile creare la cartella di output.";
- return;
- }
-
- runButton.IsEnabled = false;
- statusBlock.Text = "Esecuzione face encoder in corso...";
- outputLogBox.Text = string.Empty;
-
- var outputLines = new StringBuilder();
- var errorLines = new StringBuilder();
-
- try
- {
- var imagesFolderArg = NormalizeDirectoryPathArgument(imagesFolder);
- var outputFolderArg = NormalizeDirectoryPathArgument(outputFolder);
- Console.WriteLine($"[FaceAI] Command: \"{executablePath}\" --images \"{imagesFolderArg}\" --out \"{outputFolderArg}\"");
-
- var processStartInfo = new ProcessStartInfo
- {
- FileName = executablePath,
- WorkingDirectory = Path.GetDirectoryName(executablePath) ?? Environment.CurrentDirectory,
- UseShellExecute = false,
- RedirectStandardOutput = true,
- RedirectStandardError = true,
- CreateNoWindow = true,
- };
- processStartInfo.ArgumentList.Add("--images");
- processStartInfo.ArgumentList.Add(imagesFolderArg);
- processStartInfo.ArgumentList.Add("--out");
- processStartInfo.ArgumentList.Add(outputFolderArg);
-
- using var process = new Process { StartInfo = processStartInfo, EnableRaisingEvents = true };
- process.OutputDataReceived += (_, args) =>
- {
- if (string.IsNullOrWhiteSpace(args.Data))
- {
- return;
- }
-
- lock (outputLines)
- {
- outputLines.AppendLine(args.Data);
- }
-
- Console.WriteLine(args.Data);
- };
-
- process.ErrorDataReceived += (_, args) =>
- {
- if (string.IsNullOrWhiteSpace(args.Data))
- {
- return;
- }
-
- lock (errorLines)
- {
- errorLines.AppendLine(args.Data);
- }
-
- Console.Error.WriteLine(args.Data);
- };
-
- if (!process.Start())
- {
- throw new InvalidOperationException("Avvio face_encoder.exe fallito.");
- }
-
- process.BeginOutputReadLine();
- process.BeginErrorReadLine();
-
- await process.WaitForExitAsync().ConfigureAwait(true);
-
- var summary = new StringBuilder();
- summary.AppendLine($"Exit code: {process.ExitCode}");
-
- if (outputLines.Length > 0)
- {
- summary.AppendLine();
- summary.AppendLine("STDOUT:");
- summary.Append(outputLines);
- }
-
- if (errorLines.Length > 0)
- {
- summary.AppendLine();
- summary.AppendLine("STDERR:");
- summary.Append(errorLines);
- }
-
- outputLogBox.Text = summary.ToString();
-
- if (process.ExitCode == 0)
- {
- statusBlock.Text = "Face encoder completato.";
- }
- else
- {
- statusBlock.Text = $"Face encoder terminato con errore (code {process.ExitCode}).";
- }
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Face encoder execution failed.");
- Console.Error.WriteLine(ex);
- outputLogBox.Text = ex.ToString();
- statusBlock.Text = "Errore durante esecuzione face encoder.";
- }
- finally
- {
- runButton.IsEnabled = true;
- }
- }
-
- private static string NormalizeDirectoryPathArgument(string value)
- {
- if (string.IsNullOrWhiteSpace(value))
- {
- return string.Empty;
- }
-
- var normalized = value.Trim().Trim('"');
- var root = Path.GetPathRoot(normalized);
- if (!string.IsNullOrEmpty(root) && normalized.Length > root.Length)
- {
- normalized = normalized.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
- }
-
- return normalized;
- }
-
private void SyncThemeStateFromCurrentTheme()
{
- var actualVariant = ActualThemeVariant;
- _isDarkTheme = actualVariant == ThemeVariant.Dark;
+ _isDarkTheme = ActualThemeVariant == ThemeVariant.Dark;
UpdateThemeToggleButtonContent();
}
@@ -731,27 +163,4 @@ public partial class AvaloniaMainWindow : Window
toggleButton.Content = _isDarkTheme ? "☀" : "🌙";
}
-
- private static long ExtractRaceId(string html)
- {
- if (string.IsNullOrWhiteSpace(html))
- {
- return 0;
- }
-
- var inputMatch = Regex.Match(
- html,
- "id=\\\"id_gara\\\"[^>]*value=\\\"(?\\d+)\\\"",
- RegexOptions.IgnoreCase);
-
- if (inputMatch.Success && long.TryParse(inputMatch.Groups["id"].Value, out var idFromInput))
- {
- return idFromInput;
- }
-
- var labelMatch = Regex.Match(html, "Descrizione \\(id: (?\\d+)\\)", RegexOptions.IgnoreCase);
- return labelMatch.Success && long.TryParse(labelMatch.Groups["id"].Value, out var idFromLabel)
- ? idFromLabel
- : 0;
- }
}
diff --git a/imagecatalog/AvaloniaViews/AiTabView.axaml b/imagecatalog/AvaloniaViews/AiTabView.axaml
new file mode 100644
index 0000000..e9652d4
--- /dev/null
+++ b/imagecatalog/AvaloniaViews/AiTabView.axaml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/imagecatalog/AvaloniaViews/AiTabView.axaml.cs b/imagecatalog/AvaloniaViews/AiTabView.axaml.cs
new file mode 100644
index 0000000..1873691
--- /dev/null
+++ b/imagecatalog/AvaloniaViews/AiTabView.axaml.cs
@@ -0,0 +1,58 @@
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using System.Diagnostics;
+using System.IO;
+
+namespace ImageCatalog_2.AvaloniaViews;
+
+public partial class AiTabView : Avalonia.Controls.UserControl
+{
+ public AiTabView()
+ {
+ InitializeComponent();
+ }
+
+ private void OpenModelsFolder_Click(object? sender, RoutedEventArgs e)
+ {
+ if (DataContext is DataModel model)
+ {
+ OpenInExplorer(model.ModelsFolderPath);
+ }
+ }
+
+ private void OpenCsvOutputFolder_Click(object? sender, RoutedEventArgs e)
+ {
+ if (DataContext is not DataModel model)
+ {
+ return;
+ }
+
+ var directory = Path.GetDirectoryName(model.CsvOutputPath);
+ OpenInExplorer(string.IsNullOrWhiteSpace(directory) ? model.CsvOutputPath : directory);
+ }
+
+ 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}\"");
+ }
+ else if (Directory.Exists(normalizedPath))
+ {
+ Process.Start(new ProcessStartInfo { FileName = normalizedPath, UseShellExecute = true });
+ }
+ }
+ catch
+ {
+ // Ignore failures when opening Explorer.
+ }
+ }
+}
diff --git a/imagecatalog/AvaloniaViews/FaceAiTabView.axaml b/imagecatalog/AvaloniaViews/FaceAiTabView.axaml
new file mode 100644
index 0000000..e2e22db
--- /dev/null
+++ b/imagecatalog/AvaloniaViews/FaceAiTabView.axaml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/imagecatalog/AvaloniaViews/FaceAiTabView.axaml.cs b/imagecatalog/AvaloniaViews/FaceAiTabView.axaml.cs
new file mode 100644
index 0000000..209c01e
--- /dev/null
+++ b/imagecatalog/AvaloniaViews/FaceAiTabView.axaml.cs
@@ -0,0 +1,317 @@
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Avalonia.Platform.Storage;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace ImageCatalog_2.AvaloniaViews;
+
+public partial class FaceAiTabView : Avalonia.Controls.UserControl
+{
+ private readonly ILogger _logger;
+
+ public FaceAiTabView()
+ {
+ InitializeComponent();
+ _logger = Program.ServiceProvider.GetService(typeof(ILogger)) as ILogger
+ ?? NullLogger.Instance;
+ }
+
+ private async void SelectFaceExecutable_Click(object? sender, RoutedEventArgs e)
+ {
+ var executableBox = this.FindControl("FaceExecutablePathTextBox");
+ if (executableBox is null)
+ {
+ return;
+ }
+
+ var topLevel = TopLevel.GetTopLevel(this);
+ var storageProvider = topLevel?.StorageProvider;
+ if (storageProvider is null)
+ {
+ return;
+ }
+
+ var files = await storageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
+ {
+ Title = "Seleziona face_encoder.exe",
+ FileTypeFilter =
+ [
+ new FilePickerFileType("Eseguibile") { Patterns = ["*.exe"] },
+ new FilePickerFileType("Tutti i file") { Patterns = ["*.*"] }
+ ]
+ });
+
+ if (files.Count > 0)
+ {
+ executableBox.Text = files[0].Path.LocalPath;
+ if (DataContext is DataModel model)
+ {
+ model.FaceExecutablePath = executableBox.Text;
+ }
+ }
+ }
+
+ private async void SelectFaceOutputFolder_Click(object? sender, RoutedEventArgs e)
+ {
+ var outputBox = this.FindControl("FaceOutputFolderTextBox");
+ if (outputBox is null)
+ {
+ return;
+ }
+
+ var topLevel = TopLevel.GetTopLevel(this);
+ var storageProvider = topLevel?.StorageProvider;
+ if (storageProvider is null)
+ {
+ return;
+ }
+
+ var folders = await storageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
+ {
+ Title = "Seleziona cartella output encodings"
+ });
+
+ if (folders.Count > 0)
+ {
+ outputBox.Text = folders[0].Path.LocalPath;
+ if (DataContext is DataModel model)
+ {
+ model.FaceOutputFolderPath = outputBox.Text;
+ }
+ }
+ }
+
+ private void OpenFaceExecutableFolder_Click(object? sender, RoutedEventArgs e)
+ {
+ var executableBox = this.FindControl("FaceExecutablePathTextBox");
+ if (executableBox is null)
+ {
+ return;
+ }
+
+ var path = executableBox.Text?.Trim();
+ if (string.IsNullOrWhiteSpace(path))
+ {
+ return;
+ }
+
+ if (File.Exists(path))
+ {
+ OpenInExplorer(path);
+ return;
+ }
+
+ var directory = Path.GetDirectoryName(path);
+ OpenInExplorer(string.IsNullOrWhiteSpace(directory) ? path : directory);
+ }
+
+ private void OpenFaceOutputFolder_Click(object? sender, RoutedEventArgs e)
+ {
+ var outputBox = this.FindControl("FaceOutputFolderTextBox");
+ if (outputBox is null)
+ {
+ return;
+ }
+
+ OpenInExplorer(outputBox.Text);
+ }
+
+ private async void RunFaceEncoder_Click(object? sender, RoutedEventArgs e)
+ {
+ var executableBox = this.FindControl("FaceExecutablePathTextBox");
+ var outputFolderBox = this.FindControl("FaceOutputFolderTextBox");
+ var outputLogBox = this.FindControl("FaceOutputTextBox");
+ var statusBlock = this.FindControl("FaceStatusTextBlock");
+ var runButton = this.FindControl("FaceRunButton");
+
+ if (executableBox is null || outputFolderBox is null || outputLogBox is null || statusBlock is null || runButton is null)
+ {
+ return;
+ }
+
+ if (DataContext is not DataModel model)
+ {
+ statusBlock.Text = "DataContext non valido.";
+ return;
+ }
+
+ var executablePath = executableBox.Text?.Trim().Trim('"') ?? string.Empty;
+ var outputFolder = outputFolderBox.Text?.Trim().Trim('"') ?? string.Empty;
+ var imagesFolder = (model.DestinationPath ?? string.Empty).Trim().Trim('"');
+
+ model.FaceExecutablePath = executablePath;
+ model.FaceOutputFolderPath = outputFolder;
+
+ if (string.IsNullOrWhiteSpace(executablePath) || !File.Exists(executablePath))
+ {
+ statusBlock.Text = "Percorso eseguibile non valido.";
+ return;
+ }
+
+ if (string.IsNullOrWhiteSpace(imagesFolder) || !Directory.Exists(imagesFolder))
+ {
+ statusBlock.Text = "Cartella Destinazione non valida.";
+ return;
+ }
+
+ if (string.IsNullOrWhiteSpace(outputFolder))
+ {
+ statusBlock.Text = "Inserisci la cartella di output.";
+ return;
+ }
+
+ try
+ {
+ Directory.CreateDirectory(outputFolder);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Unable to create face output folder: {OutputFolder}", outputFolder);
+ statusBlock.Text = "Impossibile creare la cartella di output.";
+ return;
+ }
+
+ runButton.IsEnabled = false;
+ statusBlock.Text = "Esecuzione face encoder in corso...";
+ outputLogBox.Text = string.Empty;
+
+ var outputLines = new StringBuilder();
+ var errorLines = new StringBuilder();
+
+ try
+ {
+ var imagesFolderArg = NormalizeDirectoryPathArgument(imagesFolder);
+ var outputFolderArg = NormalizeDirectoryPathArgument(outputFolder);
+
+ var processStartInfo = new ProcessStartInfo
+ {
+ FileName = executablePath,
+ WorkingDirectory = Path.GetDirectoryName(executablePath) ?? Environment.CurrentDirectory,
+ UseShellExecute = false,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ CreateNoWindow = true,
+ };
+ processStartInfo.ArgumentList.Add("--images");
+ processStartInfo.ArgumentList.Add(imagesFolderArg);
+ processStartInfo.ArgumentList.Add("--out");
+ processStartInfo.ArgumentList.Add(outputFolderArg);
+
+ using var process = new Process { StartInfo = processStartInfo, EnableRaisingEvents = true };
+ process.OutputDataReceived += (_, args) =>
+ {
+ if (string.IsNullOrWhiteSpace(args.Data))
+ {
+ return;
+ }
+
+ lock (outputLines)
+ {
+ outputLines.AppendLine(args.Data);
+ }
+ };
+
+ process.ErrorDataReceived += (_, args) =>
+ {
+ if (string.IsNullOrWhiteSpace(args.Data))
+ {
+ return;
+ }
+
+ lock (errorLines)
+ {
+ errorLines.AppendLine(args.Data);
+ }
+ };
+
+ if (!process.Start())
+ {
+ throw new InvalidOperationException("Avvio face_encoder.exe fallito.");
+ }
+
+ process.BeginOutputReadLine();
+ process.BeginErrorReadLine();
+ await process.WaitForExitAsync().ConfigureAwait(true);
+
+ var summary = new StringBuilder();
+ summary.AppendLine($"Exit code: {process.ExitCode}");
+
+ if (outputLines.Length > 0)
+ {
+ summary.AppendLine();
+ summary.AppendLine("STDOUT:");
+ summary.Append(outputLines);
+ }
+
+ if (errorLines.Length > 0)
+ {
+ summary.AppendLine();
+ summary.AppendLine("STDERR:");
+ summary.Append(errorLines);
+ }
+
+ outputLogBox.Text = summary.ToString();
+ statusBlock.Text = process.ExitCode == 0
+ ? "Face encoder completato."
+ : $"Face encoder terminato con errore (code {process.ExitCode}).";
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Face encoder execution failed.");
+ outputLogBox.Text = ex.ToString();
+ statusBlock.Text = "Errore durante esecuzione face encoder.";
+ }
+ finally
+ {
+ runButton.IsEnabled = true;
+ }
+ }
+
+ 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}\"");
+ }
+ else if (Directory.Exists(normalizedPath))
+ {
+ Process.Start(new ProcessStartInfo { FileName = normalizedPath, UseShellExecute = true });
+ }
+ }
+ catch
+ {
+ // Ignore failures when opening Explorer.
+ }
+ }
+
+ private static string NormalizeDirectoryPathArgument(string value)
+ {
+ if (string.IsNullOrWhiteSpace(value))
+ {
+ return string.Empty;
+ }
+
+ var normalized = value.Trim().Trim('"');
+ var root = Path.GetPathRoot(normalized);
+ if (!string.IsNullOrEmpty(root) && normalized.Length > root.Length)
+ {
+ normalized = normalized.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
+ }
+
+ return normalized;
+ }
+}
diff --git a/imagecatalog/AvaloniaViews/GeneralTabView.axaml b/imagecatalog/AvaloniaViews/GeneralTabView.axaml
new file mode 100644
index 0000000..580f8a5
--- /dev/null
+++ b/imagecatalog/AvaloniaViews/GeneralTabView.axaml
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/imagecatalog/AvaloniaViews/GeneralTabView.axaml.cs b/imagecatalog/AvaloniaViews/GeneralTabView.axaml.cs
new file mode 100644
index 0000000..cdcbec9
--- /dev/null
+++ b/imagecatalog/AvaloniaViews/GeneralTabView.axaml.cs
@@ -0,0 +1,56 @@
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using System;
+using System.Diagnostics;
+using System.IO;
+
+namespace ImageCatalog_2.AvaloniaViews;
+
+public partial class GeneralTabView : Avalonia.Controls.UserControl
+{
+ public GeneralTabView()
+ {
+ InitializeComponent();
+ }
+
+ private void OpenSourceFolder_Click(object? sender, RoutedEventArgs e)
+ {
+ if (DataContext is DataModel model)
+ {
+ OpenInExplorer(model.SourcePath);
+ }
+ }
+
+ private void OpenDestinationFolder_Click(object? sender, RoutedEventArgs e)
+ {
+ if (DataContext is DataModel model)
+ {
+ OpenInExplorer(model.DestinationPath);
+ }
+ }
+
+ 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}\"");
+ }
+ else if (Directory.Exists(normalizedPath))
+ {
+ Process.Start(new ProcessStartInfo { FileName = normalizedPath, UseShellExecute = true });
+ }
+ }
+ catch
+ {
+ // Ignore failures when opening Explorer.
+ }
+ }
+}
diff --git a/imagecatalog/AvaloniaViews/LogoTabView.axaml b/imagecatalog/AvaloniaViews/LogoTabView.axaml
new file mode 100644
index 0000000..30d09d6
--- /dev/null
+++ b/imagecatalog/AvaloniaViews/LogoTabView.axaml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/imagecatalog/AvaloniaViews/LogoTabView.axaml.cs b/imagecatalog/AvaloniaViews/LogoTabView.axaml.cs
new file mode 100644
index 0000000..0868016
--- /dev/null
+++ b/imagecatalog/AvaloniaViews/LogoTabView.axaml.cs
@@ -0,0 +1,62 @@
+using Avalonia.Controls;
+using Avalonia.Media.Imaging;
+using System.ComponentModel;
+using System.IO;
+
+namespace ImageCatalog_2.AvaloniaViews;
+
+public partial class LogoTabView : Avalonia.Controls.UserControl
+{
+ public LogoTabView()
+ {
+ InitializeComponent();
+ DataContextChanged += OnDataContextChanged;
+ }
+
+ private void OnDataContextChanged(object? sender, System.EventArgs e)
+ {
+ if (sender is not LogoTabView)
+ {
+ return;
+ }
+
+ if (DataContext is DataModel model)
+ {
+ model.PropertyChanged -= ModelOnPropertyChanged;
+ model.PropertyChanged += ModelOnPropertyChanged;
+ UpdateLogoPreview(model.LogoFile);
+ }
+ }
+
+ private void ModelOnPropertyChanged(object? sender, PropertyChangedEventArgs e)
+ {
+ if (e.PropertyName == nameof(DataModel.LogoFile) && sender is DataModel model)
+ {
+ UpdateLogoPreview(model.LogoFile);
+ }
+ }
+
+ private void UpdateLogoPreview(string? path)
+ {
+ var preview = this.FindControl("LogoPreview");
+ if (preview is null)
+ {
+ return;
+ }
+
+ if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
+ {
+ preview.Source = null;
+ return;
+ }
+
+ try
+ {
+ preview.Source = new Avalonia.Media.Imaging.Bitmap(path);
+ }
+ catch
+ {
+ preview.Source = null;
+ }
+ }
+}
diff --git a/imagecatalog/AvaloniaViews/PhotoTabView.axaml b/imagecatalog/AvaloniaViews/PhotoTabView.axaml
new file mode 100644
index 0000000..beac2a3
--- /dev/null
+++ b/imagecatalog/AvaloniaViews/PhotoTabView.axaml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/imagecatalog/AvaloniaViews/PhotoTabView.axaml.cs b/imagecatalog/AvaloniaViews/PhotoTabView.axaml.cs
new file mode 100644
index 0000000..363ac40
--- /dev/null
+++ b/imagecatalog/AvaloniaViews/PhotoTabView.axaml.cs
@@ -0,0 +1,11 @@
+using Avalonia.Controls;
+
+namespace ImageCatalog_2.AvaloniaViews;
+
+public partial class PhotoTabView : Avalonia.Controls.UserControl
+{
+ public PhotoTabView()
+ {
+ InitializeComponent();
+ }
+}
diff --git a/imagecatalog/AvaloniaViews/RaceUploadTabView.axaml b/imagecatalog/AvaloniaViews/RaceUploadTabView.axaml
new file mode 100644
index 0000000..307d938
--- /dev/null
+++ b/imagecatalog/AvaloniaViews/RaceUploadTabView.axaml
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/imagecatalog/AvaloniaViews/RaceUploadTabView.axaml.cs b/imagecatalog/AvaloniaViews/RaceUploadTabView.axaml.cs
new file mode 100644
index 0000000..28be1ed
--- /dev/null
+++ b/imagecatalog/AvaloniaViews/RaceUploadTabView.axaml.cs
@@ -0,0 +1,352 @@
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Catalog.Communication.Abstractions;
+using Catalog.Communication.Models;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace ImageCatalog_2.AvaloniaViews;
+
+public partial class RaceUploadTabView : Avalonia.Controls.UserControl
+{
+ private readonly IRaceUploadCommunicationClient _apiClient;
+ private readonly ILogger _logger;
+
+ public RaceUploadTabView()
+ {
+ InitializeComponent();
+ _apiClient = Program.ServiceProvider.GetService(typeof(IRaceUploadCommunicationClient)) as IRaceUploadCommunicationClient
+ ?? throw new InvalidOperationException("IRaceUploadCommunicationClient non disponibile.");
+ _logger = Program.ServiceProvider.GetService(typeof(ILogger)) as ILogger
+ ?? NullLogger.Instance;
+ }
+
+ private async void CreateRace_Click(object? sender, RoutedEventArgs e)
+ {
+ var outputBox = this.FindControl("ApiOutputTextBox");
+ var statusBlock = this.FindControl("ApiStatusTextBlock");
+ var createButton = this.FindControl("ApiCreateRaceButton");
+ var uploadButton = this.FindControl("ApiUploadButton");
+
+ if (outputBox is null || statusBlock is null || createButton is null || uploadButton is null)
+ {
+ return;
+ }
+
+ if (DataContext is not DataModel model)
+ {
+ statusBlock.Text = "DataContext non valido.";
+ return;
+ }
+
+ var login = model.ApiLogin?.Trim() ?? string.Empty;
+ var password = model.ApiPassword ?? string.Empty;
+ var descriptionRaw = model.ApiRaceDescription?.Trim() ?? string.Empty;
+
+ if (string.IsNullOrWhiteSpace(login) || string.IsNullOrWhiteSpace(password) || string.IsNullOrWhiteSpace(descriptionRaw))
+ {
+ statusBlock.Text = "Inserisci login, password e descrizione gara.";
+ return;
+ }
+
+ if (!long.TryParse(model.ApiRaceTypeId?.Trim(), out var tipoGaraId) || tipoGaraId <= 0)
+ {
+ statusBlock.Text = "Tipo gara non valido.";
+ return;
+ }
+
+ createButton.IsEnabled = false;
+ uploadButton.IsEnabled = false;
+ statusBlock.Text = "Creazione gara in corso...";
+ outputBox.Text = string.Empty;
+
+ try
+ {
+ var startDate = DateOnly.FromDateTime(model.ApiRaceStartDate.Date);
+ var endDate = DateOnly.FromDateTime((model.ApiRaceEndDate == default ? model.ApiRaceStartDate : model.ApiRaceEndDate).Date);
+ var sanitizedDescription = SanitizeRaceDescription(descriptionRaw);
+
+ var loginResponse = await LoginAsync(login, password).ConfigureAwait(true);
+
+ var saveResponse = await _apiClient.SaveRaceAsync(
+ new RaceSaveRequest
+ {
+ IdGara = 0,
+ Description = sanitizedDescription,
+ StartDate = startDate,
+ EndDate = endDate,
+ TipoGaraId = tipoGaraId,
+ EventoInLinea = model.ApiEventoInLineaIndex,
+ TipoIndicizzazione = model.ApiTipoIndexValue,
+ FreeEvent = model.ApiFreeEventIndex,
+ PathBase = model.ApiPathBase?.Trim(),
+ Localita = model.ApiLocalita?.Trim(),
+ },
+ CancellationToken.None);
+
+ var raceId = ExtractRaceId(saveResponse.Body);
+ if (raceId <= 0)
+ {
+ throw new InvalidOperationException("Impossibile ricavare id_gara dalla risposta di salvataggio.");
+ }
+
+ model.ApiRaceId = raceId.ToString();
+
+ var createPointsResponse = await _apiClient.CreateRacePointsAsync(raceId, CancellationToken.None);
+
+ var sb = new StringBuilder();
+ sb.AppendLine($"Login HTTP: {(int)loginResponse.StatusCode} {loginResponse.StatusCode}");
+ sb.AppendLine($"Save Gara HTTP: {(int)saveResponse.StatusCode} {saveResponse.StatusCode}");
+ sb.AppendLine($"Crea Punti HTTP: {(int)createPointsResponse.StatusCode} {createPointsResponse.StatusCode}");
+ sb.AppendLine($"id_gara: {raceId}");
+ sb.AppendLine();
+ sb.AppendLine("Gara creata e avvio creazione punti richiesto.");
+
+ outputBox.Text = sb.ToString();
+ statusBlock.Text = "Gara creata.";
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Race creation failed in Avalonia tab.");
+ outputBox.Text = ex.ToString();
+ statusBlock.Text = "Errore durante la creazione gara.";
+ }
+ finally
+ {
+ createButton.IsEnabled = true;
+ uploadButton.IsEnabled = true;
+ }
+ }
+
+ private async void UploadProcessed_Click(object? sender, RoutedEventArgs e)
+ {
+ var outputBox = this.FindControl("ApiOutputTextBox");
+ var statusBlock = this.FindControl("ApiStatusTextBlock");
+ var createButton = this.FindControl("ApiCreateRaceButton");
+ var uploadButton = this.FindControl("ApiUploadButton");
+
+ if (outputBox is null || statusBlock is null || createButton is null || uploadButton is null)
+ {
+ return;
+ }
+
+ if (DataContext is not DataModel model)
+ {
+ statusBlock.Text = "DataContext non valido.";
+ return;
+ }
+
+ var login = model.ApiLogin?.Trim() ?? string.Empty;
+ var password = model.ApiPassword ?? string.Empty;
+ var racePathBase = model.ApiPathBase?.Trim() ?? string.Empty;
+ var remoteProcessedBase = model.ApiRemoteProcessedBasePath?.Trim() ?? string.Empty;
+
+ if (!long.TryParse(model.ApiRaceId?.Trim(), out var raceId) || raceId <= 0)
+ {
+ statusBlock.Text = "id_gara non valido.";
+ return;
+ }
+
+ if (string.IsNullOrWhiteSpace(login) || string.IsNullOrWhiteSpace(password))
+ {
+ statusBlock.Text = "Inserisci login e password.";
+ return;
+ }
+
+ if (string.IsNullOrWhiteSpace(model.DestinationPath) || !Directory.Exists(model.DestinationPath))
+ {
+ statusBlock.Text = "Cartella destinazione locale non valida.";
+ return;
+ }
+
+ if (string.IsNullOrWhiteSpace(remoteProcessedBase))
+ {
+ statusBlock.Text = "Inserisci il path base remoto per le foto processate.";
+ return;
+ }
+
+ createButton.IsEnabled = false;
+ uploadButton.IsEnabled = false;
+ statusBlock.Text = "Upload foto processate in corso...";
+
+ try
+ {
+ await LoginAsync(login, password).ConfigureAwait(true);
+
+ var files = Directory
+ .EnumerateFiles(model.DestinationPath, "*.*", SearchOption.AllDirectories)
+ .Where(IsSupportedImage)
+ .ToList();
+
+ if (files.Count == 0)
+ {
+ statusBlock.Text = "Nessuna immagine trovata in destinazione.";
+ outputBox.Text = "Nessun file processato da inviare.";
+ return;
+ }
+
+ var uploaded = 0;
+ var sb = new StringBuilder();
+ sb.AppendLine($"File da inviare: {files.Count}");
+
+ foreach (var file in files)
+ {
+ var relativePath = Path.GetRelativePath(model.DestinationPath, file);
+ var relativeDir = Path.GetDirectoryName(relativePath) ?? string.Empty;
+ var remotePath = CombineRemotePath(remoteProcessedBase, racePathBase, relativeDir);
+
+ await using var stream = File.OpenRead(file);
+ await _apiClient.UploadFileToReceiverAsync(
+ new ReceiveFileUploadRequest
+ {
+ FileName = Path.GetFileName(file),
+ FileStream = stream,
+ DestinationPath = remotePath,
+ OverwriteRemoteFile = true,
+ },
+ CancellationToken.None).ConfigureAwait(true);
+
+ uploaded++;
+ if (uploaded % 20 == 0 || uploaded == files.Count)
+ {
+ statusBlock.Text = $"Upload foto: {uploaded}/{files.Count}";
+ }
+ }
+
+ sb.AppendLine($"Upload completato: {uploaded}/{files.Count}");
+ statusBlock.Text = "Creazione punti foto e indicizzazione in corso...";
+
+ await _apiClient.CreateRacePointsAsync(raceId, CancellationToken.None).ConfigureAwait(true);
+ var pointIds = await LoadPointIdsWithRetryAsync(raceId, CancellationToken.None).ConfigureAwait(true);
+
+ foreach (var pointId in pointIds)
+ {
+ await _apiClient.IndexRacePointAsync(pointId, CancellationToken.None).ConfigureAwait(true);
+ }
+
+ sb.AppendLine($"Punti foto indicizzati: {pointIds.Count}");
+ outputBox.Text = sb.ToString();
+ statusBlock.Text = "Upload e indicizzazione completati.";
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Upload flow failed in Avalonia tab.");
+ outputBox.Text = ex.ToString();
+ statusBlock.Text = "Errore durante upload/indicizzazione.";
+ }
+ finally
+ {
+ createButton.IsEnabled = true;
+ uploadButton.IsEnabled = true;
+ }
+ }
+
+ private async Task LoginAsync(string login, string password)
+ {
+ return await _apiClient.LoginAdminAsync(
+ new AdminLoginRequest
+ {
+ Login = login,
+ Password = password,
+ Command = "check",
+ },
+ CancellationToken.None).ConfigureAwait(false);
+ }
+
+ private async Task> LoadPointIdsWithRetryAsync(long raceId, CancellationToken cancellationToken)
+ {
+ const int maxAttempts = 10;
+
+ for (var attempt = 1; attempt <= maxAttempts; attempt++)
+ {
+ var response = await _apiClient.GetRaceDetailAsync(raceId, cancellationToken).ConfigureAwait(false);
+ var ids = ExtractPointIds(response.Body);
+
+ if (ids.Count > 0)
+ {
+ return ids;
+ }
+
+ await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken).ConfigureAwait(false);
+ }
+
+ return [];
+ }
+
+ private static List ExtractPointIds(string html)
+ {
+ return Regex
+ .Matches(html ?? string.Empty, @"indexFoto\((\d+)\)", RegexOptions.IgnoreCase)
+ .Select(m => long.TryParse(m.Groups[1].Value, out var value) ? value : 0L)
+ .Where(v => v > 0)
+ .Distinct()
+ .ToList();
+ }
+
+ private static string SanitizeRaceDescription(string value)
+ {
+ if (string.IsNullOrWhiteSpace(value))
+ {
+ return string.Empty;
+ }
+
+ var cleaned = Regex.Replace(value, "[^A-Za-z0-9 _-]", " ");
+ return Regex.Replace(cleaned, "\\s+", " ").Trim();
+ }
+
+ private static string CombineRemotePath(string remoteBase, string racePathBase, string relativeDir)
+ {
+ var segments = new[] { remoteBase, racePathBase, relativeDir }
+ .Where(s => !string.IsNullOrWhiteSpace(s))
+ .Select(s => s!.Replace('\\', '/').Trim('/'));
+
+ var joined = string.Join('/', segments);
+ return string.IsNullOrWhiteSpace(joined) ? "/" : joined + "/";
+ }
+
+ private static bool IsSupportedImage(string filePath)
+ {
+ var extension = Path.GetExtension(filePath);
+ if (string.IsNullOrWhiteSpace(extension))
+ {
+ return false;
+ }
+
+ return extension.Equals(".jpg", StringComparison.OrdinalIgnoreCase)
+ || extension.Equals(".jpeg", StringComparison.OrdinalIgnoreCase)
+ || extension.Equals(".png", StringComparison.OrdinalIgnoreCase)
+ || extension.Equals(".bmp", StringComparison.OrdinalIgnoreCase)
+ || extension.Equals(".gif", StringComparison.OrdinalIgnoreCase);
+ }
+
+ private static long ExtractRaceId(string html)
+ {
+ if (string.IsNullOrWhiteSpace(html))
+ {
+ return 0;
+ }
+
+ var inputMatch = Regex.Match(
+ html,
+ "id=\\\"id_gara\\\"[^>]*value=\\\"(?\\d+)\\\"",
+ RegexOptions.IgnoreCase);
+
+ if (inputMatch.Success && long.TryParse(inputMatch.Groups["id"].Value, out var idFromInput))
+ {
+ return idFromInput;
+ }
+
+ var labelMatch = Regex.Match(html, "Descrizione \\(id: (?\\d+)\\)", RegexOptions.IgnoreCase);
+ return labelMatch.Success && long.TryParse(labelMatch.Groups["id"].Value, out var idFromLabel)
+ ? idFromLabel
+ : 0;
+ }
+}
diff --git a/imagecatalog/AvaloniaViews/TextTabView.axaml b/imagecatalog/AvaloniaViews/TextTabView.axaml
new file mode 100644
index 0000000..7e0ecc0
--- /dev/null
+++ b/imagecatalog/AvaloniaViews/TextTabView.axaml
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/imagecatalog/AvaloniaViews/TextTabView.axaml.cs b/imagecatalog/AvaloniaViews/TextTabView.axaml.cs
new file mode 100644
index 0000000..8d45992
--- /dev/null
+++ b/imagecatalog/AvaloniaViews/TextTabView.axaml.cs
@@ -0,0 +1,11 @@
+using Avalonia.Controls;
+
+namespace ImageCatalog_2.AvaloniaViews;
+
+public partial class TextTabView : Avalonia.Controls.UserControl
+{
+ public TextTabView()
+ {
+ InitializeComponent();
+ }
+}
diff --git a/imagecatalog/AvaloniaViews/ThumbnailsTabView.axaml b/imagecatalog/AvaloniaViews/ThumbnailsTabView.axaml
new file mode 100644
index 0000000..da2491f
--- /dev/null
+++ b/imagecatalog/AvaloniaViews/ThumbnailsTabView.axaml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Nessuna
+ Aggiungi scritta
+ Nome file
+ Aggiungi orario
+ Nome+Orario
+ Tempo gara
+
+
+
+
+
diff --git a/imagecatalog/AvaloniaViews/ThumbnailsTabView.axaml.cs b/imagecatalog/AvaloniaViews/ThumbnailsTabView.axaml.cs
new file mode 100644
index 0000000..f780931
--- /dev/null
+++ b/imagecatalog/AvaloniaViews/ThumbnailsTabView.axaml.cs
@@ -0,0 +1,11 @@
+using Avalonia.Controls;
+
+namespace ImageCatalog_2.AvaloniaViews;
+
+public partial class ThumbnailsTabView : Avalonia.Controls.UserControl
+{
+ public ThumbnailsTabView()
+ {
+ InitializeComponent();
+ }
+}