Added avalonia integration and remote proof of concept
This commit is contained in:
parent
775080a178
commit
4a0973b681
23 changed files with 2043 additions and 6 deletions
|
|
@ -12,8 +12,7 @@ public partial class AvaloniaApp : Avalonia.Application
|
|||
{
|
||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
var model = Program.ServiceProvider.GetRequiredService<DataModel>();
|
||||
desktop.MainWindow = new AvaloniaMainWindow(model);
|
||||
desktop.MainWindow = Program.ServiceProvider.GetRequiredService<AvaloniaMainWindow>();
|
||||
}
|
||||
base.OnFrameworkInitializationCompleted();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -270,6 +270,37 @@
|
|||
</ScrollViewer>
|
||||
</TabItem>
|
||||
|
||||
<!-- Tab 7: API Test -->
|
||||
<TabItem Header="API Test">
|
||||
<ScrollViewer>
|
||||
<StackPanel Margin="8" Spacing="8">
|
||||
<TextBlock Text="Test comunicazione API (non distruttivo)" FontWeight="Bold" />
|
||||
<TextBlock Text="Questa prova esegue login admin e una ricerca gare (cmd=search), poi mostra una sintesi delle prime 3 righe rilevate."
|
||||
TextWrapping="Wrap" Opacity="0.8" />
|
||||
|
||||
<Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto,Auto" ColumnSpacing="8" RowSpacing="8">
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="Login:" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Row="0" Grid.Column="1" Name="ApiLoginTextBox" Watermark="admin user" />
|
||||
|
||||
<TextBlock Grid.Row="1" Grid.Column="0" Text="Password:" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Row="1" Grid.Column="1" Name="ApiPasswordTextBox" PasswordChar="*" />
|
||||
</Grid>
|
||||
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Button Name="ApiTestButton" Content="Test login + ultime 3 gare" Click="ApiTestLoginAndGetRaces_Click" />
|
||||
<TextBlock Name="ApiStatusTextBlock" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Text="Output" FontWeight="Bold" Margin="0,4,0,0" />
|
||||
<TextBox Name="ApiOutputTextBox"
|
||||
IsReadOnly="True"
|
||||
AcceptsReturn="True"
|
||||
TextWrapping="Wrap"
|
||||
MinHeight="240" />
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</TabItem>
|
||||
|
||||
</TabControl>
|
||||
|
||||
<!-- Right: Controls and live info -->
|
||||
|
|
|
|||
|
|
@ -5,20 +5,43 @@ 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<AvaloniaMainWindow> _logger;
|
||||
private bool _isDarkTheme = false;
|
||||
|
||||
public AvaloniaMainWindow(DataModel model)
|
||||
public AvaloniaMainWindow(
|
||||
DataModel model,
|
||||
IRaceUploadCommunicationClient apiClient,
|
||||
ParametriSetup parametriSetup,
|
||||
ILogger<AvaloniaMainWindow> logger)
|
||||
{
|
||||
InitializeComponent();
|
||||
_model = model;
|
||||
_apiClient = apiClient;
|
||||
_parametriSetup = parametriSetup;
|
||||
_logger = logger;
|
||||
DataContext = _model;
|
||||
|
||||
// Provide Avalonia dispatcher so DataModel can marshal UI updates
|
||||
|
|
@ -104,6 +127,8 @@ public partial class AvaloniaMainWindow : Window
|
|||
if (e.PropertyName == nameof(_model.LogoFile))
|
||||
UpdateLogoPreview(_model.LogoFile);
|
||||
};
|
||||
|
||||
LoadApiTestCredentials();
|
||||
}
|
||||
|
||||
private void ToggleTheme_Click(object? sender, RoutedEventArgs e)
|
||||
|
|
@ -148,4 +173,205 @@ public partial class AvaloniaMainWindow : Window
|
|||
try { preview.Source = new Avalonia.Media.Imaging.Bitmap(path); }
|
||||
catch { preview.Source = null; }
|
||||
}
|
||||
|
||||
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) + "...";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@
|
|||
</None>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Catalog.Communication\Catalog.Communication.csproj" />
|
||||
<ProjectReference Include="..\MaddoShared\MaddoShared.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Runtime.InteropServices;
|
||||
using Catalog.Communication.DependencyInjection;
|
||||
using ImageCatalog;
|
||||
using ImageCatalog_2.Services;
|
||||
using MaddoShared;
|
||||
|
|
@ -59,7 +60,7 @@ static class Program
|
|||
}
|
||||
#endif
|
||||
|
||||
public static IServiceProvider ServiceProvider { get; private set; }
|
||||
public static IServiceProvider ServiceProvider { get; private set; } = default!;
|
||||
|
||||
public static Avalonia.AppBuilder BuildAvaloniaApp()
|
||||
=> Avalonia.AppBuilder.Configure<AvaloniaApp>()
|
||||
|
|
@ -172,6 +173,17 @@ static class Program
|
|||
services.AddSingleton(new ParametriSetup(userPrefsPath));
|
||||
services.AddSingleton<PicSettings>();
|
||||
|
||||
services.AddCatalogCommunication(options =>
|
||||
{
|
||||
options.BaseUri = new Uri("https://www.regalamiunsorriso.it/");
|
||||
options.AdminPageBasePath = "admin/pg_RUS";
|
||||
options.RequestTimeout = TimeSpan.FromSeconds(30);
|
||||
options.RetryCount = 2;
|
||||
options.RetryBaseDelay = TimeSpan.FromMilliseconds(250);
|
||||
});
|
||||
|
||||
services.AddTransient<AvaloniaMainWindow>();
|
||||
|
||||
#if WINDOWS
|
||||
services.AddTransient<MainForm>();
|
||||
services.AddTransient<ImageCatalog_2.MainWindow>();
|
||||
|
|
@ -199,7 +211,7 @@ public static class ConsoleLoggerExtensions
|
|||
}
|
||||
public sealed class CustomLoggingFormatter : ConsoleFormatter, IDisposable
|
||||
{
|
||||
private readonly IDisposable _optionsReloadToken;
|
||||
private readonly IDisposable? _optionsReloadToken;
|
||||
private ConsoleFormatterOptions _formatterOptions;
|
||||
public CustomLoggingFormatter(IOptionsMonitor<ConsoleFormatterOptions> options)
|
||||
// Case insensitive
|
||||
|
|
@ -214,6 +226,11 @@ public sealed class CustomLoggingFormatter : ConsoleFormatter, IDisposable
|
|||
IExternalScopeProvider? scopeProvider,
|
||||
TextWriter? textWriter)
|
||||
{
|
||||
if (textWriter is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string? message =
|
||||
logEntry.Formatter?.Invoke(
|
||||
logEntry.State, logEntry.Exception);
|
||||
|
|
@ -223,7 +240,20 @@ public sealed class CustomLoggingFormatter : ConsoleFormatter, IDisposable
|
|||
return;
|
||||
}
|
||||
|
||||
textWriter.WriteLine($"{message}");
|
||||
var timestamp = DateTimeOffset.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
|
||||
var level = logEntry.LogLevel.ToString().ToUpperInvariant();
|
||||
var category = logEntry.Category ?? "App";
|
||||
|
||||
var line = $"{timestamp} [{level}] {category}: {message}";
|
||||
textWriter.WriteLine(line);
|
||||
System.Diagnostics.Debug.WriteLine(line);
|
||||
|
||||
if (logEntry.Exception is not null)
|
||||
{
|
||||
var exceptionText = logEntry.Exception.ToString();
|
||||
textWriter.WriteLine(exceptionText);
|
||||
System.Diagnostics.Debug.WriteLine(exceptionText);
|
||||
}
|
||||
}
|
||||
public void Dispose() => _optionsReloadToken?.Dispose();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue