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-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-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();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async void ApiTestLoginAndGetRaces_Click(object? sender, RoutedEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
var loginBox = this.FindControl<Avalonia.Controls.TextBox>("ApiLoginTextBox");
|
|
|
|
|
var passwordBox = this.FindControl<Avalonia.Controls.TextBox>("ApiPasswordTextBox");
|
|
|
|
|
var outputBox = this.FindControl<Avalonia.Controls.TextBox>("ApiOutputTextBox");
|
|
|
|
|
var statusBlock = this.FindControl<TextBlock>("ApiStatusTextBlock");
|
|
|
|
|
var testButton = this.FindControl<Avalonia.Controls.Button>("ApiTestButton");
|
|
|
|
|
|
|
|
|
|
if (loginBox is null || passwordBox is null || outputBox is null || statusBlock is null || testButton is null)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var login = loginBox.Text?.Trim() ?? string.Empty;
|
|
|
|
|
var password = passwordBox.Text ?? string.Empty;
|
|
|
|
|
if (string.IsNullOrWhiteSpace(login) || string.IsNullOrWhiteSpace(password))
|
|
|
|
|
{
|
|
|
|
|
statusBlock.Text = "Inserisci login e password.";
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
testButton.IsEnabled = false;
|
|
|
|
|
statusBlock.Text = "Esecuzione test...";
|
|
|
|
|
outputBox.Text = string.Empty;
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
_logger.LogDebug("Starting API test request from Avalonia tab for user '{User}'.", login);
|
|
|
|
|
SaveApiTestCredentials();
|
|
|
|
|
|
|
|
|
|
var loginResponse = await _apiClient.LoginAdminAsync(
|
|
|
|
|
new AdminLoginRequest
|
|
|
|
|
{
|
|
|
|
|
Login = login,
|
|
|
|
|
Password = password,
|
|
|
|
|
Command = "check",
|
|
|
|
|
},
|
|
|
|
|
CancellationToken.None);
|
|
|
|
|
|
|
|
|
|
var searchResponse = await _apiClient.ExecuteGaraCommandAsync(
|
|
|
|
|
new Dictionary<string, string?>
|
|
|
|
|
{
|
|
|
|
|
["cmd"] = "search",
|
|
|
|
|
["pageNumber"] = "1",
|
|
|
|
|
},
|
|
|
|
|
CancellationToken.None);
|
|
|
|
|
|
|
|
|
|
_logger.LogDebug(
|
|
|
|
|
"API test completed requests. LoginStatus={LoginStatusCode}, SearchStatus={SearchStatusCode}",
|
|
|
|
|
(int)loginResponse.StatusCode,
|
|
|
|
|
(int)searchResponse.StatusCode);
|
|
|
|
|
|
|
|
|
|
var extracted = ExtractTopRaceLines(searchResponse.Body, 3);
|
|
|
|
|
|
|
|
|
|
var sb = new StringBuilder();
|
|
|
|
|
sb.AppendLine($"Login HTTP: {(int)loginResponse.StatusCode} {loginResponse.StatusCode}");
|
|
|
|
|
sb.AppendLine($"Search HTTP: {(int)searchResponse.StatusCode} {searchResponse.StatusCode}");
|
|
|
|
|
sb.AppendLine();
|
|
|
|
|
|
|
|
|
|
if (extracted.Count > 0)
|
|
|
|
|
{
|
|
|
|
|
sb.AppendLine("Prime 3 righe gare (estrazione semplice):");
|
|
|
|
|
for (var i = 0; i < extracted.Count; i++)
|
|
|
|
|
{
|
|
|
|
|
sb.AppendLine($"{i + 1}. {extracted[i]}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
sb.AppendLine("Nessuna riga gara riconosciuta in modo affidabile. Mostro anteprima raw:");
|
|
|
|
|
sb.AppendLine();
|
|
|
|
|
sb.AppendLine(Truncate(CollapseWhitespace(searchResponse.Body), 1500));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
outputBox.Text = sb.ToString();
|
|
|
|
|
statusBlock.Text = "Test completato.";
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
_logger.LogError(ex, "API test failed in Avalonia tab.");
|
|
|
|
|
_logger.LogDebug("API test exception details: {ExceptionDetails}", ex.ToString());
|
|
|
|
|
outputBox.Text = ex.ToString();
|
|
|
|
|
statusBlock.Text = "Errore durante il test.";
|
|
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
testButton.IsEnabled = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static List<string> ExtractTopRaceLines(string html, int take)
|
|
|
|
|
{
|
|
|
|
|
if (string.IsNullOrWhiteSpace(html))
|
|
|
|
|
{
|
|
|
|
|
return new List<string>();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var rowMatches = Regex.Matches(html, "<tr[^>]*>(.*?)</tr>", RegexOptions.IgnoreCase | RegexOptions.Singleline);
|
|
|
|
|
var lines = new List<string>();
|
|
|
|
|
|
|
|
|
|
foreach (Match row in rowMatches)
|
|
|
|
|
{
|
|
|
|
|
var cells = Regex.Matches(row.Groups[1].Value, "<t[dh][^>]*>(.*?)</t[dh]>", RegexOptions.IgnoreCase | RegexOptions.Singleline)
|
|
|
|
|
.Select(m => CollapseWhitespace(WebUtility.HtmlDecode(StripTags(m.Groups[1].Value))))
|
|
|
|
|
.Where(s => !string.IsNullOrWhiteSpace(s))
|
|
|
|
|
.ToArray();
|
|
|
|
|
|
|
|
|
|
if (cells.Length < 2)
|
|
|
|
|
{
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var joined = string.Join(" | ", cells);
|
|
|
|
|
if (IsHeaderLike(joined))
|
|
|
|
|
{
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
lines.Add(joined);
|
|
|
|
|
if (lines.Count >= take)
|
|
|
|
|
{
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (lines.Count > 0)
|
|
|
|
|
{
|
|
|
|
|
return lines;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var textRows = Regex.Matches(html, "(?is)<li[^>]*>(.*?)</li>")
|
|
|
|
|
.Select(m => CollapseWhitespace(WebUtility.HtmlDecode(StripTags(m.Groups[1].Value))))
|
|
|
|
|
.Where(s => s.Length > 8)
|
|
|
|
|
.Take(take)
|
|
|
|
|
.ToList();
|
|
|
|
|
|
|
|
|
|
return textRows;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static bool IsHeaderLike(string text)
|
|
|
|
|
{
|
|
|
|
|
var lower = text.ToLowerInvariant();
|
|
|
|
|
return lower.Contains("descrizione")
|
|
|
|
|
|| lower.Contains("data")
|
|
|
|
|
|| lower.Contains("stato")
|
|
|
|
|
|| lower.Contains("azioni")
|
|
|
|
|
|| lower.Contains("cerca");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static string StripTags(string value)
|
|
|
|
|
{
|
|
|
|
|
return Regex.Replace(value, "<[^>]+>", " ", RegexOptions.Singleline);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static string CollapseWhitespace(string? value)
|
|
|
|
|
{
|
|
|
|
|
if (string.IsNullOrWhiteSpace(value))
|
|
|
|
|
{
|
|
|
|
|
return string.Empty;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Regex.Replace(value, "\\s+", " ").Trim();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static string Truncate(string value, int max)
|
|
|
|
|
{
|
|
|
|
|
if (value.Length <= max)
|
|
|
|
|
{
|
|
|
|
|
return value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return value.Substring(0, max) + "...";
|
|
|
|
|
}
|
2026-02-26 18:43:07 +01:00
|
|
|
}
|