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 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(); } 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 async void ApiTestLoginAndGetRaces_Click(object? sender, RoutedEventArgs e) { var loginBox = this.FindControl("ApiLoginTextBox"); var passwordBox = this.FindControl("ApiPasswordTextBox"); var outputBox = this.FindControl("ApiOutputTextBox"); var statusBlock = this.FindControl("ApiStatusTextBlock"); var testButton = this.FindControl("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 { ["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 ExtractTopRaceLines(string html, int take) { if (string.IsNullOrWhiteSpace(html)) { return new List(); } var rowMatches = Regex.Matches(html, "]*>(.*?)", RegexOptions.IgnoreCase | RegexOptions.Singleline); var lines = new List(); foreach (Match row in rowMatches) { var cells = Regex.Matches(row.Groups[1].Value, "]*>(.*?)", 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)]*>(.*?)") .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) + "..."; } }