2026-02-28 21:48:05 +01:00
|
|
|
using Avalonia.Controls;
|
|
|
|
|
using Avalonia.Interactivity;
|
2026-05-09 20:27:44 +02:00
|
|
|
using Avalonia.Input;
|
|
|
|
|
using Avalonia.Layout;
|
|
|
|
|
using Avalonia.Media;
|
|
|
|
|
using Avalonia.Media.Imaging;
|
2026-05-09 15:46:41 +02:00
|
|
|
using Avalonia.Threading;
|
2026-05-09 20:27:44 +02:00
|
|
|
using ImageCatalog_2.Models;
|
|
|
|
|
using ImageCatalog_2.Services;
|
2026-02-28 21:48:05 +01:00
|
|
|
using System;
|
2026-05-09 15:46:41 +02:00
|
|
|
using System.ComponentModel;
|
2026-02-28 21:48:05 +01:00
|
|
|
using System.IO;
|
2026-05-09 20:27:44 +02:00
|
|
|
using System.Text;
|
|
|
|
|
using System.Threading.Tasks;
|
2026-02-28 21:48:05 +01:00
|
|
|
|
|
|
|
|
namespace ImageCatalog_2.AvaloniaViews;
|
|
|
|
|
|
|
|
|
|
public partial class FaceAiTabView : Avalonia.Controls.UserControl
|
|
|
|
|
{
|
2026-05-09 15:46:41 +02:00
|
|
|
private INotifyPropertyChanged? _faceAiPropertySource;
|
|
|
|
|
|
2026-02-28 21:48:05 +01:00
|
|
|
public FaceAiTabView()
|
|
|
|
|
{
|
|
|
|
|
InitializeComponent();
|
2026-05-09 15:46:41 +02:00
|
|
|
DataContextChanged += OnDataContextChanged;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void OnDataContextChanged(object? sender, EventArgs e)
|
|
|
|
|
{
|
|
|
|
|
if (_faceAiPropertySource is not null)
|
|
|
|
|
{
|
|
|
|
|
_faceAiPropertySource.PropertyChanged -= OnFaceAiPropertyChanged;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_faceAiPropertySource = DataContext as INotifyPropertyChanged;
|
|
|
|
|
if (_faceAiPropertySource is not null)
|
|
|
|
|
{
|
|
|
|
|
_faceAiPropertySource.PropertyChanged += OnFaceAiPropertyChanged;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void OnFaceAiPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
|
|
|
|
{
|
2026-05-09 20:27:44 +02:00
|
|
|
if (string.Equals(e.PropertyName, nameof(DataModel.FaceCommandOutput), StringComparison.Ordinal))
|
2026-05-09 15:46:41 +02:00
|
|
|
{
|
2026-05-09 20:27:44 +02:00
|
|
|
ScrollOutputTextBoxToEnd("FaceOutputTextBox");
|
2026-05-09 15:46:41 +02:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-09 20:27:44 +02:00
|
|
|
if (string.Equals(e.PropertyName, nameof(DataModel.FaceMatcherCommandOutput), StringComparison.Ordinal))
|
|
|
|
|
{
|
|
|
|
|
ScrollOutputTextBoxToEnd("FaceMatcherOutputTextBox");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void ScrollOutputTextBoxToEnd(string controlName)
|
|
|
|
|
{
|
|
|
|
|
var outputBox = this.FindControl<Avalonia.Controls.TextBox>(controlName);
|
2026-05-09 15:46:41 +02:00
|
|
|
if (outputBox is null)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Dispatcher.UIThread.Post(() =>
|
|
|
|
|
{
|
|
|
|
|
var textLength = outputBox.Text?.Length ?? 0;
|
|
|
|
|
outputBox.CaretIndex = textLength;
|
|
|
|
|
});
|
2026-02-28 21:48:05 +01:00
|
|
|
}
|
|
|
|
|
|
2026-05-09 20:27:44 +02:00
|
|
|
private async void OpenFaceMatcherPreview_Click(object? sender, RoutedEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
if (sender is not Button { Tag: FaceMatcherResultItem item })
|
2026-02-28 21:48:05 +01:00
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-09 20:27:44 +02:00
|
|
|
await OpenFaceMatcherPreviewAsync(item);
|
2026-02-28 21:48:05 +01:00
|
|
|
}
|
|
|
|
|
|
2026-05-09 20:27:44 +02:00
|
|
|
private async Task OpenFaceMatcherPreviewAsync(FaceMatcherResultItem item)
|
2026-02-28 21:48:05 +01:00
|
|
|
{
|
2026-05-09 20:27:44 +02:00
|
|
|
var owner = TopLevel.GetTopLevel(this) as Window;
|
|
|
|
|
var dialog = BuildFaceMatcherPreviewDialog(item);
|
|
|
|
|
if (owner is not null)
|
2026-02-28 21:48:05 +01:00
|
|
|
{
|
2026-05-09 20:27:44 +02:00
|
|
|
await dialog.ShowDialog(owner);
|
2026-02-28 21:48:05 +01:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-09 20:27:44 +02:00
|
|
|
dialog.Show();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async void FaceMatcherResults_DoubleTapped(object? sender, TappedEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
if (sender is not Avalonia.Controls.DataGrid { SelectedItem: FaceMatcherResultItem item })
|
2026-03-12 23:24:44 +01:00
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-09 20:27:44 +02:00
|
|
|
await OpenFaceMatcherPreviewAsync(item);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Window BuildFaceMatcherPreviewDialog(FaceMatcherResultItem item)
|
|
|
|
|
{
|
|
|
|
|
var dialog = new Window
|
2026-05-09 15:46:41 +02:00
|
|
|
{
|
2026-05-09 20:27:44 +02:00
|
|
|
Title = $"Preview match: {item.PhotoId}",
|
|
|
|
|
Width = 1180,
|
|
|
|
|
Height = 900,
|
|
|
|
|
WindowStartupLocation = WindowStartupLocation.CenterOwner
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Bitmap? bitmap = null;
|
|
|
|
|
var dimensionText = "n/d";
|
|
|
|
|
if (!string.IsNullOrWhiteSpace(item.ResolvedImagePath) && File.Exists(item.ResolvedImagePath))
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
bitmap = new Bitmap(item.ResolvedImagePath);
|
|
|
|
|
dimensionText = $"{bitmap.PixelSize.Width} x {bitmap.PixelSize.Height}px";
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
bitmap = null;
|
|
|
|
|
}
|
2026-05-09 15:46:41 +02:00
|
|
|
}
|
|
|
|
|
|
2026-05-09 20:27:44 +02:00
|
|
|
var fileInfo = !string.IsNullOrWhiteSpace(item.ResolvedImagePath) && File.Exists(item.ResolvedImagePath)
|
|
|
|
|
? new FileInfo(item.ResolvedImagePath)
|
|
|
|
|
: null;
|
|
|
|
|
|
|
|
|
|
var debugBuilder = new StringBuilder();
|
|
|
|
|
debugBuilder.AppendLine($"File matcher: {item.PhotoId}");
|
|
|
|
|
debugBuilder.AppendLine($"Score: {item.ScoreDisplay}");
|
|
|
|
|
debugBuilder.AppendLine($"Path risolto: {item.ResolvedImagePath}");
|
|
|
|
|
debugBuilder.AppendLine($"Candidati trovati in destinazione: {item.CandidateCount}");
|
|
|
|
|
debugBuilder.AppendLine($"Dimensioni immagine: {dimensionText}");
|
|
|
|
|
if (fileInfo is not null)
|
2026-03-12 23:24:44 +01:00
|
|
|
{
|
2026-05-09 20:27:44 +02:00
|
|
|
debugBuilder.AppendLine($"Dimensione file: {fileInfo.Length / 1024.0:F1} KB");
|
|
|
|
|
debugBuilder.AppendLine($"Ultima modifica: {fileInfo.LastWriteTime:yyyy-MM-dd HH:mm:ss}");
|
2026-03-12 23:24:44 +01:00
|
|
|
}
|
|
|
|
|
|
2026-05-09 20:27:44 +02:00
|
|
|
debugBuilder.AppendLine($"Immagine ricerca: {item.SearchImagePath}");
|
|
|
|
|
debugBuilder.AppendLine($"CSV risultati: {item.CsvPath}");
|
|
|
|
|
debugBuilder.AppendLine($"Log matcher: {item.LogPath}");
|
|
|
|
|
if (!string.IsNullOrWhiteSpace(item.DebugSummary))
|
|
|
|
|
{
|
|
|
|
|
debugBuilder.AppendLine($"Dettagli riga: {item.DebugSummary}");
|
|
|
|
|
}
|
2026-03-12 23:24:44 +01:00
|
|
|
|
2026-05-09 20:27:44 +02:00
|
|
|
if (!string.IsNullOrWhiteSpace(item.RawRow))
|
2026-03-12 23:24:44 +01:00
|
|
|
{
|
2026-05-09 20:27:44 +02:00
|
|
|
debugBuilder.AppendLine($"Raw CSV: {item.RawRow}");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var layout = new Grid
|
|
|
|
|
{
|
|
|
|
|
Margin = new Avalonia.Thickness(16),
|
|
|
|
|
RowDefinitions = new RowDefinitions("Auto,*,Auto")
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var header = new StackPanel { Spacing = 6 };
|
|
|
|
|
header.Children.Add(new TextBlock
|
|
|
|
|
{
|
|
|
|
|
Text = item.PhotoId,
|
|
|
|
|
FontWeight = FontWeight.Bold,
|
|
|
|
|
FontSize = 18
|
|
|
|
|
});
|
|
|
|
|
header.Children.Add(new TextBlock
|
|
|
|
|
{
|
|
|
|
|
Text = string.IsNullOrWhiteSpace(item.ScoreDisplay)
|
|
|
|
|
? "Score: n/d"
|
|
|
|
|
: $"Score: {item.ScoreDisplay}%",
|
|
|
|
|
FontWeight = FontWeight.SemiBold,
|
|
|
|
|
Opacity = 0.9
|
|
|
|
|
});
|
|
|
|
|
header.Children.Add(new TextBlock
|
|
|
|
|
{
|
|
|
|
|
Text = string.IsNullOrWhiteSpace(item.ResolvedImagePath)
|
|
|
|
|
? "Nessun file immagine risolto nella cartella Destinazione."
|
|
|
|
|
: item.ResolvedImagePath,
|
|
|
|
|
TextWrapping = TextWrapping.Wrap,
|
|
|
|
|
Opacity = 0.8
|
|
|
|
|
});
|
|
|
|
|
layout.Children.Add(header);
|
|
|
|
|
|
|
|
|
|
var contentGrid = new Grid
|
|
|
|
|
{
|
|
|
|
|
Margin = new Avalonia.Thickness(0, 12, 0, 12),
|
2026-05-24 18:33:54 +02:00
|
|
|
RowDefinitions = new RowDefinitions("Auto,*,Auto")
|
2026-05-09 20:27:44 +02:00
|
|
|
};
|
|
|
|
|
Grid.SetRow(contentGrid, 1);
|
|
|
|
|
|
|
|
|
|
var zoomLevel = 1.0;
|
|
|
|
|
var zoomText = new TextBlock
|
|
|
|
|
{
|
|
|
|
|
Text = "100%",
|
|
|
|
|
VerticalAlignment = VerticalAlignment.Center,
|
|
|
|
|
MinWidth = 52,
|
|
|
|
|
TextAlignment = TextAlignment.Center
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var imageControl = bitmap is null
|
|
|
|
|
? null
|
|
|
|
|
: new Image
|
|
|
|
|
{
|
|
|
|
|
Source = bitmap,
|
|
|
|
|
Stretch = Stretch.None,
|
|
|
|
|
HorizontalAlignment = HorizontalAlignment.Left,
|
|
|
|
|
VerticalAlignment = VerticalAlignment.Top,
|
|
|
|
|
RenderTransform = new ScaleTransform(1, 1)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
void UpdateZoom(double delta)
|
|
|
|
|
{
|
|
|
|
|
if (imageControl is null)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
zoomLevel = Math.Clamp(zoomLevel + delta, 0.1, 8.0);
|
|
|
|
|
imageControl.RenderTransform = new ScaleTransform(zoomLevel, zoomLevel);
|
|
|
|
|
zoomText.Text = $"{zoomLevel * 100:0}%";
|
2026-03-12 23:24:44 +01:00
|
|
|
}
|
|
|
|
|
|
2026-05-09 20:27:44 +02:00
|
|
|
var toolbar = new StackPanel
|
|
|
|
|
{
|
|
|
|
|
Orientation = Orientation.Horizontal,
|
2026-05-24 18:33:54 +02:00
|
|
|
Spacing = 8,
|
|
|
|
|
Margin = new Avalonia.Thickness(0, 0, 0, 12)
|
2026-05-09 20:27:44 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var zoomOutButton = new Button { Content = "Zoom -", MinWidth = 80, IsEnabled = imageControl is not null };
|
|
|
|
|
zoomOutButton.Click += (_, _) => UpdateZoom(-0.1);
|
|
|
|
|
toolbar.Children.Add(zoomOutButton);
|
|
|
|
|
|
|
|
|
|
var zoomInButton = new Button { Content = "Zoom +", MinWidth = 80, IsEnabled = imageControl is not null };
|
|
|
|
|
zoomInButton.Click += (_, _) => UpdateZoom(0.1);
|
|
|
|
|
toolbar.Children.Add(zoomInButton);
|
|
|
|
|
|
|
|
|
|
var resetZoomButton = new Button { Content = "100%", MinWidth = 72, IsEnabled = imageControl is not null };
|
|
|
|
|
resetZoomButton.Click += (_, _) =>
|
|
|
|
|
{
|
|
|
|
|
if (imageControl is null)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
zoomLevel = 1.0;
|
|
|
|
|
imageControl.RenderTransform = new ScaleTransform(1, 1);
|
|
|
|
|
zoomText.Text = "100%";
|
|
|
|
|
};
|
|
|
|
|
toolbar.Children.Add(resetZoomButton);
|
|
|
|
|
toolbar.Children.Add(zoomText);
|
|
|
|
|
contentGrid.Children.Add(toolbar);
|
|
|
|
|
|
|
|
|
|
var previewBorder = new Border
|
|
|
|
|
{
|
|
|
|
|
BorderBrush = Brushes.Gray,
|
|
|
|
|
BorderThickness = new Avalonia.Thickness(1),
|
|
|
|
|
Padding = new Avalonia.Thickness(8),
|
|
|
|
|
Child = new ScrollViewer
|
|
|
|
|
{
|
|
|
|
|
HorizontalScrollBarVisibility = Avalonia.Controls.Primitives.ScrollBarVisibility.Auto,
|
|
|
|
|
VerticalScrollBarVisibility = Avalonia.Controls.Primitives.ScrollBarVisibility.Auto,
|
|
|
|
|
Content = bitmap is null
|
|
|
|
|
? new TextBlock
|
|
|
|
|
{
|
|
|
|
|
Text = "Anteprima non disponibile",
|
|
|
|
|
HorizontalAlignment = HorizontalAlignment.Center,
|
|
|
|
|
VerticalAlignment = VerticalAlignment.Center
|
|
|
|
|
}
|
|
|
|
|
: imageControl
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
Grid.SetRow(previewBorder, 1);
|
|
|
|
|
contentGrid.Children.Add(previewBorder);
|
|
|
|
|
|
|
|
|
|
var debugBox = new TextBox
|
|
|
|
|
{
|
|
|
|
|
Text = debugBuilder.ToString(),
|
|
|
|
|
IsReadOnly = true,
|
|
|
|
|
AcceptsReturn = true,
|
|
|
|
|
TextWrapping = TextWrapping.Wrap,
|
|
|
|
|
FontFamily = new FontFamily("Cascadia Mono, Consolas, monospace"),
|
2026-05-24 18:33:54 +02:00
|
|
|
MinHeight = 180,
|
|
|
|
|
Margin = new Avalonia.Thickness(0, 12, 0, 0)
|
2026-05-09 20:27:44 +02:00
|
|
|
};
|
|
|
|
|
Grid.SetRow(debugBox, 2);
|
|
|
|
|
contentGrid.Children.Add(debugBox);
|
|
|
|
|
|
|
|
|
|
layout.Children.Add(contentGrid);
|
|
|
|
|
|
|
|
|
|
var footer = new StackPanel
|
|
|
|
|
{
|
|
|
|
|
Orientation = Orientation.Horizontal,
|
|
|
|
|
HorizontalAlignment = HorizontalAlignment.Right,
|
|
|
|
|
Spacing = 8
|
|
|
|
|
};
|
|
|
|
|
Grid.SetRow(footer, 2);
|
|
|
|
|
|
|
|
|
|
var openFileButton = new Button { Content = "Apri file" };
|
2026-05-24 19:07:17 +02:00
|
|
|
openFileButton.Click += (_, _) => PathShellService.OpenInExplorer(item.ResolvedImagePath);
|
2026-05-09 20:27:44 +02:00
|
|
|
footer.Children.Add(openFileButton);
|
|
|
|
|
|
|
|
|
|
var openFolderButton = new Button { Content = "Apri cartella" };
|
|
|
|
|
openFolderButton.Click += (_, _) =>
|
|
|
|
|
{
|
|
|
|
|
var directory = Path.GetDirectoryName(item.ResolvedImagePath);
|
2026-05-24 19:07:17 +02:00
|
|
|
PathShellService.OpenInExplorer(string.IsNullOrWhiteSpace(directory) ? item.ResolvedImagePath : directory);
|
2026-05-09 20:27:44 +02:00
|
|
|
};
|
|
|
|
|
footer.Children.Add(openFolderButton);
|
|
|
|
|
|
|
|
|
|
var closeButton = new Button { Content = "Chiudi", MinWidth = 88 };
|
|
|
|
|
closeButton.Click += (_, _) => dialog.Close();
|
|
|
|
|
footer.Children.Add(closeButton);
|
|
|
|
|
|
|
|
|
|
layout.Children.Add(footer);
|
|
|
|
|
dialog.Content = layout;
|
|
|
|
|
return dialog;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-28 21:48:05 +01:00
|
|
|
}
|