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.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 const string ApiLoginKey = "ApiTest.Login"; private const string ApiPasswordKey = "ApiTest.Password"; 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"; private readonly DataModel _model; private readonly IRaceUploadCommunicationClient _apiClient; private readonly ParametriSetup _parametriSetup; private readonly ILogger _logger; private bool _isDarkTheme = false; public AvaloniaMainWindow( DataModel model, IRaceUploadCommunicationClient apiClient, ParametriSetup parametriSetup, ILogger logger) { InitializeComponent(); _model = model; _apiClient = apiClient; _parametriSetup = parametriSetup; _logger = logger; 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); }; LoadApiTestCredentials(); LoadRaceUploadSettings(); } 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("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 void LoadApiTestCredentials() { var loginBox = this.FindControl("ApiLoginTextBox"); var passwordBox = this.FindControl("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("ApiLoginTextBox"); var passwordBox = this.FindControl("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(); } private void LoadRaceUploadSettings() { var raceTypeBox = this.FindControl("ApiRaceTypeIdTextBox"); var pathBaseBox = this.FindControl("ApiPathBaseTextBox"); var remoteBaseBox = this.FindControl("ApiRemoteProcessedBasePathTextBox"); var onlineBox = this.FindControl("ApiEventoInLineaComboBox"); var indexBox = this.FindControl("ApiTipoIndexComboBox"); var freeBox = this.FindControl("ApiFreeEventComboBox"); var raceIdBox = this.FindControl("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("ApiRaceTypeIdTextBox"); var pathBaseBox = this.FindControl("ApiPathBaseTextBox"); var remoteBaseBox = this.FindControl("ApiRemoteProcessedBasePathTextBox"); var onlineBox = this.FindControl("ApiEventoInLineaComboBox"); var indexBox = this.FindControl("ApiTipoIndexComboBox"); var freeBox = this.FindControl("ApiFreeEventComboBox"); var raceIdBox = this.FindControl("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) { var loginBox = this.FindControl("ApiLoginTextBox"); var passwordBox = this.FindControl("ApiPasswordTextBox"); var raceTypeBox = this.FindControl("ApiRaceTypeIdTextBox"); var raceDescriptionBox = this.FindControl("ApiRaceDescriptionTextBox"); var raceStartPicker = this.FindControl("ApiRaceStartDatePicker"); var raceEndPicker = this.FindControl("ApiRaceEndDatePicker"); var pathBaseBox = this.FindControl("ApiPathBaseTextBox"); var localitaBox = this.FindControl("ApiLocalitaTextBox"); var raceIdBox = this.FindControl("ApiRaceIdTextBox"); var eventoInLineaBox = this.FindControl("ApiEventoInLineaComboBox"); var tipoIndexBox = this.FindControl("ApiTipoIndexComboBox"); var freeBox = this.FindControl("ApiFreeEventComboBox"); var outputBox = this.FindControl("ApiOutputTextBox"); var statusBlock = this.FindControl("ApiStatusTextBlock"); var createButton = this.FindControl("ApiCreateRaceButton"); var uploadButton = this.FindControl("ApiUploadButton"); 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) { return; } var login = loginBox.Text?.Trim() ?? string.Empty; var password = passwordBox.Text ?? string.Empty; var descriptionRaw = raceDescriptionBox.Text?.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(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..."; outputBox.Text = string.Empty; try { var startDate = DateOnly.FromDateTime(raceStartPicker.SelectedDate.Value.Date); var endDate = raceEndPicker.SelectedDate.HasValue ? DateOnly.FromDateTime(raceEndPicker.SelectedDate.Value.Date) : startDate; var sanitizedDescription = SanitizeRaceDescription(descriptionRaw); SaveApiTestCredentials(); SaveRaceUploadSettings(); 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 = GetComboSelection(eventoInLineaBox), TipoIndicizzazione = GetComboSelection(tipoIndexBox), FreeEvent = GetComboSelection(freeBox), PathBase = pathBaseBox.Text?.Trim(), Localita = localitaBox.Text?.Trim(), }, CancellationToken.None); var raceId = ExtractRaceId(saveResponse.Body); if (raceId <= 0) { throw new InvalidOperationException("Impossibile ricavare id_gara dalla risposta di salvataggio."); } raceIdBox.Text = raceId.ToString(); SaveRaceUploadSettings(); 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 loginBox = this.FindControl("ApiLoginTextBox"); var passwordBox = this.FindControl("ApiPasswordTextBox"); var raceIdBox = this.FindControl("ApiRaceIdTextBox"); var pathBaseBox = this.FindControl("ApiPathBaseTextBox"); var remoteBaseBox = this.FindControl("ApiRemoteProcessedBasePathTextBox"); var outputBox = this.FindControl("ApiOutputTextBox"); var statusBlock = this.FindControl("ApiStatusTextBlock"); var createButton = this.FindControl("ApiCreateRaceButton"); var uploadButton = this.FindControl("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) { return; } 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; if (!long.TryParse(raceIdBox.Text?.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 { SaveApiTestCredentials(); SaveRaceUploadSettings(); 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 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; } }