2026-02-26 18:43:07 +01:00
|
|
|
using Avalonia;
|
|
|
|
|
using Avalonia.Controls;
|
|
|
|
|
using Avalonia.Interactivity;
|
|
|
|
|
using Avalonia.Media.Imaging;
|
|
|
|
|
using Avalonia.Platform.Storage;
|
|
|
|
|
using Avalonia.Styling;
|
|
|
|
|
using Avalonia.Threading;
|
2026-02-28 15:30:57 +01:00
|
|
|
using Catalog.Communication.Abstractions;
|
|
|
|
|
using Catalog.Communication.Models;
|
|
|
|
|
using ImageCatalog;
|
|
|
|
|
using Microsoft.Extensions.Logging;
|
2026-02-26 18:43:07 +01:00
|
|
|
using System;
|
|
|
|
|
using System.IO;
|
2026-02-28 15:30:57 +01:00
|
|
|
using System.Linq;
|
|
|
|
|
using System.Net;
|
|
|
|
|
using System.Text;
|
|
|
|
|
using System.Text.RegularExpressions;
|
|
|
|
|
using System.Threading;
|
|
|
|
|
using System.Threading.Tasks;
|
2026-02-26 18:43:07 +01:00
|
|
|
|
|
|
|
|
namespace ImageCatalog_2;
|
|
|
|
|
|
|
|
|
|
public partial class AvaloniaMainWindow : Window
|
|
|
|
|
{
|
2026-02-28 15:30:57 +01:00
|
|
|
private const string ApiLoginKey = "ApiTest.Login";
|
|
|
|
|
private const string ApiPasswordKey = "ApiTest.Password";
|
2026-02-28 16:54:08 +01:00
|
|
|
private const string ApiRaceTypeKey = "RaceUpload.TipoGaraId";
|
|
|
|
|
private const string ApiRacePathBaseKey = "RaceUpload.PathBase";
|
|
|
|
|
private const string ApiRemoteProcessedBaseKey = "RaceUpload.RemoteProcessedBasePath";
|
|
|
|
|
private const string ApiRaceOnlineFlagKey = "RaceUpload.FlgEventoInLinea";
|
|
|
|
|
private const string ApiRaceIndexFlagKey = "RaceUpload.FlgTipoIndex";
|
|
|
|
|
private const string ApiRaceFreeFlagKey = "RaceUpload.FlgFree";
|
|
|
|
|
private const string ApiLastRaceIdKey = "RaceUpload.LastRaceId";
|
2026-02-28 15:30:57 +01:00
|
|
|
|
2026-02-26 18:43:07 +01:00
|
|
|
private readonly DataModel _model;
|
2026-02-28 15:30:57 +01:00
|
|
|
private readonly IRaceUploadCommunicationClient _apiClient;
|
|
|
|
|
private readonly ParametriSetup _parametriSetup;
|
|
|
|
|
private readonly ILogger<AvaloniaMainWindow> _logger;
|
2026-02-26 18:43:07 +01:00
|
|
|
private bool _isDarkTheme = false;
|
|
|
|
|
|
2026-02-28 15:30:57 +01:00
|
|
|
public AvaloniaMainWindow(
|
|
|
|
|
DataModel model,
|
|
|
|
|
IRaceUploadCommunicationClient apiClient,
|
|
|
|
|
ParametriSetup parametriSetup,
|
|
|
|
|
ILogger<AvaloniaMainWindow> logger)
|
2026-02-26 18:43:07 +01:00
|
|
|
{
|
|
|
|
|
InitializeComponent();
|
|
|
|
|
_model = model;
|
2026-02-28 15:30:57 +01:00
|
|
|
_apiClient = apiClient;
|
|
|
|
|
_parametriSetup = parametriSetup;
|
|
|
|
|
_logger = logger;
|
2026-02-26 18:43:07 +01:00
|
|
|
DataContext = _model;
|
|
|
|
|
|
|
|
|
|
// Provide Avalonia dispatcher so DataModel can marshal UI updates
|
|
|
|
|
_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;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
_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;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
_model.SelectLogoFileRequested += async (_, _) =>
|
|
|
|
|
{
|
|
|
|
|
var files = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
|
|
|
|
|
{
|
|
|
|
|
Title = "Seleziona logo",
|
|
|
|
|
FileTypeFilter = new[] { new FilePickerFileType("Immagini") { Patterns = new[] { "*.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;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
_model.SelectCsvOutputRequested += async (_, _) =>
|
|
|
|
|
{
|
|
|
|
|
var file = await StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
|
|
|
|
|
{
|
|
|
|
|
Title = "Salva CSV",
|
|
|
|
|
DefaultExtension = "csv",
|
|
|
|
|
FileTypeChoices = new[] { new FilePickerFileType("CSV") { Patterns = new[] { "*.csv" } } }
|
|
|
|
|
});
|
|
|
|
|
if (file != null) _model.CsvOutputPath = file.Path.LocalPath;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
_model.SaveSettingsRequested += async (_, _) =>
|
|
|
|
|
{
|
|
|
|
|
var file = await StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
|
|
|
|
|
{
|
|
|
|
|
Title = "Salva impostazioni",
|
|
|
|
|
DefaultExtension = "xml",
|
|
|
|
|
FileTypeChoices = new[] { new FilePickerFileType("Setup") { Patterns = new[] { "*.xml" } } }
|
|
|
|
|
});
|
|
|
|
|
if (file != null) await _model.SaveSettingsToFileAsync(file.Path.LocalPath);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
_model.LoadSettingsRequested += async (_, _) =>
|
|
|
|
|
{
|
|
|
|
|
var files = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
|
|
|
|
|
{
|
|
|
|
|
Title = "Carica impostazioni",
|
|
|
|
|
FileTypeFilter = new[] { new FilePickerFileType("Setup") { Patterns = new[] { "*.xml" } } }
|
|
|
|
|
});
|
|
|
|
|
if (files.Count > 0) await _model.LoadSettingsFromFileAsync(files[0].Path.LocalPath);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
_model.SelectColorRequested += (_, _) =>
|
|
|
|
|
{
|
|
|
|
|
// 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);
|
|
|
|
|
};
|
2026-02-28 15:30:57 +01:00
|
|
|
|
|
|
|
|
LoadApiTestCredentials();
|
2026-02-28 16:54:08 +01:00
|
|
|
LoadRaceUploadSettings();
|
2026-02-26 18:43:07 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void ToggleTheme_Click(object? sender, RoutedEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
_isDarkTheme = !_isDarkTheme;
|
|
|
|
|
if (Avalonia.Application.Current != null)
|
|
|
|
|
Avalonia.Application.Current.RequestedThemeVariant = _isDarkTheme ? ThemeVariant.Dark : ThemeVariant.Light;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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<Avalonia.Controls.Image>("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; }
|
|
|
|
|
}
|
2026-02-28 15:30:57 +01:00
|
|
|
|
|
|
|
|
private void LoadApiTestCredentials()
|
|
|
|
|
{
|
|
|
|
|
var loginBox = this.FindControl<Avalonia.Controls.TextBox>("ApiLoginTextBox");
|
|
|
|
|
var passwordBox = this.FindControl<Avalonia.Controls.TextBox>("ApiPasswordTextBox");
|
|
|
|
|
if (loginBox is null || passwordBox is null)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
loginBox.Text = _parametriSetup.LeggiParametroString(ApiLoginKey);
|
|
|
|
|
passwordBox.Text = _parametriSetup.LeggiParametroString(ApiPasswordKey);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void SaveApiTestCredentials()
|
|
|
|
|
{
|
|
|
|
|
var loginBox = this.FindControl<Avalonia.Controls.TextBox>("ApiLoginTextBox");
|
|
|
|
|
var passwordBox = this.FindControl<Avalonia.Controls.TextBox>("ApiPasswordTextBox");
|
|
|
|
|
if (loginBox is null || passwordBox is null)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_parametriSetup.AggiornaParametro(ApiLoginKey, loginBox.Text ?? string.Empty);
|
|
|
|
|
_parametriSetup.AggiornaParametro(ApiPasswordKey, passwordBox.Text ?? string.Empty);
|
|
|
|
|
_parametriSetup.SalvaParametriSetup();
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-28 16:54:08 +01:00
|
|
|
private void LoadRaceUploadSettings()
|
|
|
|
|
{
|
|
|
|
|
var raceTypeBox = this.FindControl<Avalonia.Controls.TextBox>("ApiRaceTypeIdTextBox");
|
|
|
|
|
var pathBaseBox = this.FindControl<Avalonia.Controls.TextBox>("ApiPathBaseTextBox");
|
|
|
|
|
var remoteBaseBox = this.FindControl<Avalonia.Controls.TextBox>("ApiRemoteProcessedBasePathTextBox");
|
|
|
|
|
var onlineBox = this.FindControl<Avalonia.Controls.ComboBox>("ApiEventoInLineaComboBox");
|
|
|
|
|
var indexBox = this.FindControl<Avalonia.Controls.ComboBox>("ApiTipoIndexComboBox");
|
|
|
|
|
var freeBox = this.FindControl<Avalonia.Controls.ComboBox>("ApiFreeEventComboBox");
|
|
|
|
|
var raceIdBox = this.FindControl<Avalonia.Controls.TextBox>("ApiRaceIdTextBox");
|
|
|
|
|
|
|
|
|
|
if (raceTypeBox is not null)
|
|
|
|
|
{
|
|
|
|
|
raceTypeBox.Text = _parametriSetup.LeggiParametroString(ApiRaceTypeKey);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (pathBaseBox is not null)
|
|
|
|
|
{
|
|
|
|
|
pathBaseBox.Text = _parametriSetup.LeggiParametroString(ApiRacePathBaseKey);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (remoteBaseBox is not null)
|
|
|
|
|
{
|
|
|
|
|
remoteBaseBox.Text = _parametriSetup.LeggiParametroString(ApiRemoteProcessedBaseKey);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
SetComboSelection(onlineBox, _parametriSetup.LeggiParametro(ApiRaceOnlineFlagKey, 0));
|
|
|
|
|
SetComboSelection(indexBox, _parametriSetup.LeggiParametro(ApiRaceIndexFlagKey, 1));
|
|
|
|
|
SetComboSelection(freeBox, _parametriSetup.LeggiParametro(ApiRaceFreeFlagKey, 0));
|
|
|
|
|
|
|
|
|
|
if (raceIdBox is not null)
|
|
|
|
|
{
|
|
|
|
|
raceIdBox.Text = _parametriSetup.LeggiParametroString(ApiLastRaceIdKey);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void SaveRaceUploadSettings()
|
|
|
|
|
{
|
|
|
|
|
var raceTypeBox = this.FindControl<Avalonia.Controls.TextBox>("ApiRaceTypeIdTextBox");
|
|
|
|
|
var pathBaseBox = this.FindControl<Avalonia.Controls.TextBox>("ApiPathBaseTextBox");
|
|
|
|
|
var remoteBaseBox = this.FindControl<Avalonia.Controls.TextBox>("ApiRemoteProcessedBasePathTextBox");
|
|
|
|
|
var onlineBox = this.FindControl<Avalonia.Controls.ComboBox>("ApiEventoInLineaComboBox");
|
|
|
|
|
var indexBox = this.FindControl<Avalonia.Controls.ComboBox>("ApiTipoIndexComboBox");
|
|
|
|
|
var freeBox = this.FindControl<Avalonia.Controls.ComboBox>("ApiFreeEventComboBox");
|
|
|
|
|
var raceIdBox = this.FindControl<Avalonia.Controls.TextBox>("ApiRaceIdTextBox");
|
|
|
|
|
|
|
|
|
|
_parametriSetup.AggiornaParametro(ApiRaceTypeKey, raceTypeBox?.Text ?? string.Empty);
|
|
|
|
|
_parametriSetup.AggiornaParametro(ApiRacePathBaseKey, pathBaseBox?.Text ?? string.Empty);
|
|
|
|
|
_parametriSetup.AggiornaParametro(ApiRemoteProcessedBaseKey, remoteBaseBox?.Text ?? string.Empty);
|
|
|
|
|
_parametriSetup.AggiornaParametro(ApiRaceOnlineFlagKey, GetComboSelection(onlineBox).ToString());
|
|
|
|
|
_parametriSetup.AggiornaParametro(ApiRaceIndexFlagKey, GetComboSelection(indexBox).ToString());
|
|
|
|
|
_parametriSetup.AggiornaParametro(ApiRaceFreeFlagKey, GetComboSelection(freeBox).ToString());
|
|
|
|
|
_parametriSetup.AggiornaParametro(ApiLastRaceIdKey, raceIdBox?.Text ?? string.Empty);
|
|
|
|
|
_parametriSetup.SalvaParametriSetup();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static void SetComboSelection(Avalonia.Controls.ComboBox? comboBox, int value)
|
|
|
|
|
{
|
|
|
|
|
if (comboBox is null)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
comboBox.SelectedIndex = value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static int GetComboSelection(Avalonia.Controls.ComboBox? comboBox)
|
|
|
|
|
{
|
|
|
|
|
return comboBox?.SelectedIndex is int index && index >= 0 ? index : 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async void CreateRace_Click(object? sender, RoutedEventArgs e)
|
2026-02-28 15:30:57 +01:00
|
|
|
{
|
|
|
|
|
var loginBox = this.FindControl<Avalonia.Controls.TextBox>("ApiLoginTextBox");
|
|
|
|
|
var passwordBox = this.FindControl<Avalonia.Controls.TextBox>("ApiPasswordTextBox");
|
2026-02-28 16:54:08 +01:00
|
|
|
var raceTypeBox = this.FindControl<Avalonia.Controls.TextBox>("ApiRaceTypeIdTextBox");
|
|
|
|
|
var raceDescriptionBox = this.FindControl<Avalonia.Controls.TextBox>("ApiRaceDescriptionTextBox");
|
|
|
|
|
var raceStartPicker = this.FindControl<CalendarDatePicker>("ApiRaceStartDatePicker");
|
|
|
|
|
var raceEndPicker = this.FindControl<CalendarDatePicker>("ApiRaceEndDatePicker");
|
|
|
|
|
var pathBaseBox = this.FindControl<Avalonia.Controls.TextBox>("ApiPathBaseTextBox");
|
|
|
|
|
var localitaBox = this.FindControl<Avalonia.Controls.TextBox>("ApiLocalitaTextBox");
|
|
|
|
|
var raceIdBox = this.FindControl<Avalonia.Controls.TextBox>("ApiRaceIdTextBox");
|
|
|
|
|
var eventoInLineaBox = this.FindControl<Avalonia.Controls.ComboBox>("ApiEventoInLineaComboBox");
|
|
|
|
|
var tipoIndexBox = this.FindControl<Avalonia.Controls.ComboBox>("ApiTipoIndexComboBox");
|
|
|
|
|
var freeBox = this.FindControl<Avalonia.Controls.ComboBox>("ApiFreeEventComboBox");
|
2026-02-28 15:30:57 +01:00
|
|
|
var outputBox = this.FindControl<Avalonia.Controls.TextBox>("ApiOutputTextBox");
|
|
|
|
|
var statusBlock = this.FindControl<TextBlock>("ApiStatusTextBlock");
|
2026-02-28 16:54:08 +01:00
|
|
|
var createButton = this.FindControl<Avalonia.Controls.Button>("ApiCreateRaceButton");
|
|
|
|
|
var uploadButton = this.FindControl<Avalonia.Controls.Button>("ApiUploadButton");
|
2026-02-28 15:30:57 +01:00
|
|
|
|
2026-02-28 16:54:08 +01:00
|
|
|
if (loginBox is null || passwordBox is null || raceTypeBox is null || raceDescriptionBox is null || raceStartPicker is null ||
|
|
|
|
|
raceEndPicker is null || pathBaseBox is null || localitaBox is null || raceIdBox is null ||
|
|
|
|
|
eventoInLineaBox is null || tipoIndexBox is null || freeBox is null ||
|
|
|
|
|
outputBox is null || statusBlock is null || createButton is null || uploadButton is null)
|
2026-02-28 15:30:57 +01:00
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var login = loginBox.Text?.Trim() ?? string.Empty;
|
|
|
|
|
var password = passwordBox.Text ?? string.Empty;
|
2026-02-28 16:54:08 +01:00
|
|
|
var descriptionRaw = raceDescriptionBox.Text?.Trim() ?? string.Empty;
|
|
|
|
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(login) || string.IsNullOrWhiteSpace(password) || string.IsNullOrWhiteSpace(descriptionRaw))
|
2026-02-28 15:30:57 +01:00
|
|
|
{
|
2026-02-28 16:54:08 +01:00
|
|
|
statusBlock.Text = "Inserisci login, password e descrizione gara.";
|
2026-02-28 15:30:57 +01:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-28 16:54:08 +01:00
|
|
|
if (!long.TryParse(raceTypeBox.Text?.Trim(), out var tipoGaraId) || tipoGaraId <= 0)
|
|
|
|
|
{
|
|
|
|
|
statusBlock.Text = "Tipo gara non valido.";
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!raceStartPicker.SelectedDate.HasValue)
|
|
|
|
|
{
|
|
|
|
|
statusBlock.Text = "Seleziona la data di inizio gara.";
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
createButton.IsEnabled = false;
|
|
|
|
|
uploadButton.IsEnabled = false;
|
|
|
|
|
statusBlock.Text = "Creazione gara in corso...";
|
2026-02-28 15:30:57 +01:00
|
|
|
outputBox.Text = string.Empty;
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
2026-02-28 16:54:08 +01:00
|
|
|
var startDate = DateOnly.FromDateTime(raceStartPicker.SelectedDate.Value.Date);
|
|
|
|
|
var endDate = raceEndPicker.SelectedDate.HasValue
|
|
|
|
|
? DateOnly.FromDateTime(raceEndPicker.SelectedDate.Value.Date)
|
|
|
|
|
: startDate;
|
|
|
|
|
var sanitizedDescription = SanitizeRaceDescription(descriptionRaw);
|
|
|
|
|
|
2026-02-28 15:30:57 +01:00
|
|
|
SaveApiTestCredentials();
|
2026-02-28 16:54:08 +01:00
|
|
|
SaveRaceUploadSettings();
|
2026-02-28 15:30:57 +01:00
|
|
|
|
2026-02-28 16:54:08 +01:00
|
|
|
var loginResponse = await LoginAsync(login, password).ConfigureAwait(true);
|
2026-02-28 15:30:57 +01:00
|
|
|
|
2026-02-28 16:54:08 +01:00
|
|
|
var saveResponse = await _apiClient.SaveRaceAsync(
|
|
|
|
|
new RaceSaveRequest
|
2026-02-28 15:30:57 +01:00
|
|
|
{
|
2026-02-28 16:54:08 +01:00
|
|
|
IdGara = 0,
|
|
|
|
|
Description = sanitizedDescription,
|
|
|
|
|
StartDate = startDate,
|
|
|
|
|
EndDate = endDate,
|
|
|
|
|
TipoGaraId = tipoGaraId,
|
|
|
|
|
EventoInLinea = GetComboSelection(eventoInLineaBox),
|
|
|
|
|
TipoIndicizzazione = GetComboSelection(tipoIndexBox),
|
|
|
|
|
FreeEvent = GetComboSelection(freeBox),
|
|
|
|
|
PathBase = pathBaseBox.Text?.Trim(),
|
|
|
|
|
Localita = localitaBox.Text?.Trim(),
|
2026-02-28 15:30:57 +01:00
|
|
|
},
|
|
|
|
|
CancellationToken.None);
|
|
|
|
|
|
2026-02-28 16:54:08 +01:00
|
|
|
var raceId = ExtractRaceId(saveResponse.Body);
|
|
|
|
|
if (raceId <= 0)
|
|
|
|
|
{
|
|
|
|
|
throw new InvalidOperationException("Impossibile ricavare id_gara dalla risposta di salvataggio.");
|
|
|
|
|
}
|
2026-02-28 15:30:57 +01:00
|
|
|
|
2026-02-28 16:54:08 +01:00
|
|
|
raceIdBox.Text = raceId.ToString();
|
|
|
|
|
SaveRaceUploadSettings();
|
|
|
|
|
|
|
|
|
|
var createPointsResponse = await _apiClient.CreateRacePointsAsync(raceId, CancellationToken.None);
|
2026-02-28 15:30:57 +01:00
|
|
|
|
|
|
|
|
var sb = new StringBuilder();
|
|
|
|
|
sb.AppendLine($"Login HTTP: {(int)loginResponse.StatusCode} {loginResponse.StatusCode}");
|
2026-02-28 16:54:08 +01:00
|
|
|
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}");
|
2026-02-28 15:30:57 +01:00
|
|
|
sb.AppendLine();
|
2026-02-28 16:54:08 +01:00
|
|
|
sb.AppendLine("Gara creata e avvio creazione punti richiesto.");
|
2026-02-28 15:30:57 +01:00
|
|
|
|
|
|
|
|
outputBox.Text = sb.ToString();
|
2026-02-28 16:54:08 +01:00
|
|
|
statusBlock.Text = "Gara creata.";
|
2026-02-28 15:30:57 +01:00
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
2026-02-28 16:54:08 +01:00
|
|
|
_logger.LogError(ex, "Race creation failed in Avalonia tab.");
|
2026-02-28 15:30:57 +01:00
|
|
|
outputBox.Text = ex.ToString();
|
2026-02-28 16:54:08 +01:00
|
|
|
statusBlock.Text = "Errore durante la creazione gara.";
|
2026-02-28 15:30:57 +01:00
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
2026-02-28 16:54:08 +01:00
|
|
|
createButton.IsEnabled = true;
|
|
|
|
|
uploadButton.IsEnabled = true;
|
2026-02-28 15:30:57 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-28 16:54:08 +01:00
|
|
|
private async void UploadProcessed_Click(object? sender, RoutedEventArgs e)
|
2026-02-28 15:30:57 +01:00
|
|
|
{
|
2026-02-28 16:54:08 +01:00
|
|
|
var loginBox = this.FindControl<Avalonia.Controls.TextBox>("ApiLoginTextBox");
|
|
|
|
|
var passwordBox = this.FindControl<Avalonia.Controls.TextBox>("ApiPasswordTextBox");
|
|
|
|
|
var raceIdBox = this.FindControl<Avalonia.Controls.TextBox>("ApiRaceIdTextBox");
|
|
|
|
|
var pathBaseBox = this.FindControl<Avalonia.Controls.TextBox>("ApiPathBaseTextBox");
|
|
|
|
|
var remoteBaseBox = this.FindControl<Avalonia.Controls.TextBox>("ApiRemoteProcessedBasePathTextBox");
|
|
|
|
|
var outputBox = this.FindControl<Avalonia.Controls.TextBox>("ApiOutputTextBox");
|
|
|
|
|
var statusBlock = this.FindControl<TextBlock>("ApiStatusTextBlock");
|
|
|
|
|
var createButton = this.FindControl<Avalonia.Controls.Button>("ApiCreateRaceButton");
|
|
|
|
|
var uploadButton = this.FindControl<Avalonia.Controls.Button>("ApiUploadButton");
|
|
|
|
|
|
|
|
|
|
if (loginBox is null || passwordBox is null || raceIdBox is null || pathBaseBox is null || remoteBaseBox is null ||
|
|
|
|
|
outputBox is null || statusBlock is null || createButton is null || uploadButton is null)
|
2026-02-28 15:30:57 +01:00
|
|
|
{
|
2026-02-28 16:54:08 +01:00
|
|
|
return;
|
2026-02-28 15:30:57 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-28 16:54:08 +01:00
|
|
|
var login = loginBox.Text?.Trim() ?? string.Empty;
|
|
|
|
|
var password = passwordBox.Text ?? string.Empty;
|
|
|
|
|
var racePathBase = pathBaseBox.Text?.Trim() ?? string.Empty;
|
|
|
|
|
var remoteProcessedBase = remoteBaseBox.Text?.Trim() ?? string.Empty;
|
2026-02-28 15:30:57 +01:00
|
|
|
|
2026-02-28 16:54:08 +01:00
|
|
|
if (!long.TryParse(raceIdBox.Text?.Trim(), out var raceId) || raceId <= 0)
|
2026-02-28 15:30:57 +01:00
|
|
|
{
|
2026-02-28 16:54:08 +01:00
|
|
|
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...";
|
2026-02-28 15:30:57 +01:00
|
|
|
|
2026-02-28 16:54:08 +01:00
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
SaveApiTestCredentials();
|
|
|
|
|
SaveRaceUploadSettings();
|
|
|
|
|
await LoginAsync(login, password).ConfigureAwait(true);
|
|
|
|
|
|
|
|
|
|
var files = Directory
|
|
|
|
|
.EnumerateFiles(_model.DestinationPath, "*.*", SearchOption.AllDirectories)
|
|
|
|
|
.Where(IsSupportedImage)
|
|
|
|
|
.ToList();
|
|
|
|
|
|
|
|
|
|
if (files.Count == 0)
|
2026-02-28 15:30:57 +01:00
|
|
|
{
|
2026-02-28 16:54:08 +01:00
|
|
|
statusBlock.Text = "Nessuna immagine trovata in destinazione.";
|
|
|
|
|
outputBox.Text = "Nessun file processato da inviare.";
|
|
|
|
|
return;
|
2026-02-28 15:30:57 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-28 16:54:08 +01:00
|
|
|
var uploaded = 0;
|
|
|
|
|
var sb = new StringBuilder();
|
|
|
|
|
sb.AppendLine($"File da inviare: {files.Count}");
|
|
|
|
|
|
|
|
|
|
foreach (var file in files)
|
2026-02-28 15:30:57 +01:00
|
|
|
{
|
2026-02-28 16:54:08 +01:00
|
|
|
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}";
|
|
|
|
|
}
|
2026-02-28 15:30:57 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-28 16:54:08 +01:00
|
|
|
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)
|
2026-02-28 15:30:57 +01:00
|
|
|
{
|
2026-02-28 16:54:08 +01:00
|
|
|
await _apiClient.IndexRacePointAsync(pointId, CancellationToken.None).ConfigureAwait(true);
|
2026-02-28 15:30:57 +01:00
|
|
|
}
|
2026-02-28 16:54:08 +01:00
|
|
|
|
|
|
|
|
sb.AppendLine($"Punti foto indicizzati: {pointIds.Count}");
|
|
|
|
|
outputBox.Text = sb.ToString();
|
|
|
|
|
statusBlock.Text = "Upload e indicizzazione completati.";
|
2026-02-28 15:30:57 +01:00
|
|
|
}
|
2026-02-28 16:54:08 +01:00
|
|
|
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<RawEndpointResponse> LoginAsync(string login, string password)
|
|
|
|
|
{
|
|
|
|
|
return await _apiClient.LoginAdminAsync(
|
|
|
|
|
new AdminLoginRequest
|
|
|
|
|
{
|
|
|
|
|
Login = login,
|
|
|
|
|
Password = password,
|
|
|
|
|
Command = "check",
|
|
|
|
|
},
|
|
|
|
|
CancellationToken.None).ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task<List<long>> LoadPointIdsWithRetryAsync(long raceId, CancellationToken cancellationToken)
|
|
|
|
|
{
|
|
|
|
|
const int maxAttempts = 10;
|
2026-02-28 15:30:57 +01:00
|
|
|
|
2026-02-28 16:54:08 +01:00
|
|
|
for (var attempt = 1; attempt <= maxAttempts; attempt++)
|
2026-02-28 15:30:57 +01:00
|
|
|
{
|
2026-02-28 16:54:08 +01:00
|
|
|
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);
|
2026-02-28 15:30:57 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-28 16:54:08 +01:00
|
|
|
return new List<long>();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static List<long> 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()
|
2026-02-28 15:30:57 +01:00
|
|
|
.ToList();
|
|
|
|
|
|
2026-02-28 16:54:08 +01:00
|
|
|
return ids;
|
2026-02-28 15:30:57 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-28 16:54:08 +01:00
|
|
|
private static string SanitizeRaceDescription(string value)
|
2026-02-28 15:30:57 +01:00
|
|
|
{
|
2026-02-28 16:54:08 +01:00
|
|
|
if (string.IsNullOrWhiteSpace(value))
|
|
|
|
|
{
|
|
|
|
|
return string.Empty;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var cleaned = Regex.Replace(value, "[^A-Za-z0-9 _-]", " ");
|
|
|
|
|
return Regex.Replace(cleaned, "\\s+", " ").Trim();
|
2026-02-28 15:30:57 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-28 16:54:08 +01:00
|
|
|
private static string CombineRemotePath(string remoteBase, string racePathBase, string relativeDir)
|
2026-02-28 15:30:57 +01:00
|
|
|
{
|
2026-02-28 16:54:08 +01:00
|
|
|
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 + "/";
|
2026-02-28 15:30:57 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-28 16:54:08 +01:00
|
|
|
private static bool IsSupportedImage(string filePath)
|
2026-02-28 15:30:57 +01:00
|
|
|
{
|
2026-02-28 16:54:08 +01:00
|
|
|
var extension = Path.GetExtension(filePath);
|
|
|
|
|
if (string.IsNullOrWhiteSpace(extension))
|
2026-02-28 15:30:57 +01:00
|
|
|
{
|
2026-02-28 16:54:08 +01:00
|
|
|
return false;
|
2026-02-28 15:30:57 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-28 16:54:08 +01:00
|
|
|
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);
|
2026-02-28 15:30:57 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-28 16:54:08 +01:00
|
|
|
private static long ExtractRaceId(string html)
|
2026-02-28 15:30:57 +01:00
|
|
|
{
|
2026-02-28 16:54:08 +01:00
|
|
|
if (string.IsNullOrWhiteSpace(html))
|
|
|
|
|
{
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var inputMatch = Regex.Match(
|
|
|
|
|
html,
|
|
|
|
|
"id=\\\"id_gara\\\"[^>]*value=\\\"(?<id>\\d+)\\\"",
|
|
|
|
|
RegexOptions.IgnoreCase);
|
|
|
|
|
|
|
|
|
|
if (inputMatch.Success && long.TryParse(inputMatch.Groups["id"].Value, out var idFromInput))
|
2026-02-28 15:30:57 +01:00
|
|
|
{
|
2026-02-28 16:54:08 +01:00
|
|
|
return idFromInput;
|
2026-02-28 15:30:57 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-28 16:54:08 +01:00
|
|
|
var labelMatch = Regex.Match(html, "Descrizione \\(id: (?<id>\\d+)\\)", RegexOptions.IgnoreCase);
|
|
|
|
|
return labelMatch.Success && long.TryParse(labelMatch.Groups["id"].Value, out var idFromLabel)
|
|
|
|
|
? idFromLabel
|
|
|
|
|
: 0;
|
2026-02-28 15:30:57 +01:00
|
|
|
}
|
2026-02-26 18:43:07 +01:00
|
|
|
}
|