using Avalonia.Controls; using Avalonia.Interactivity; using Avalonia.Input; using Avalonia.Layout; using Avalonia.Media; using Avalonia.Media.Imaging; using Avalonia.Threading; using ImageCatalog_2.Models; using ImageCatalog_2.Services; using System; using System.ComponentModel; using System.IO; using System.Text; using System.Threading.Tasks; namespace ImageCatalog_2.AvaloniaViews; public partial class FaceAiTabView : Avalonia.Controls.UserControl { private INotifyPropertyChanged? _faceAiPropertySource; public FaceAiTabView() { InitializeComponent(); 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) { if (string.Equals(e.PropertyName, nameof(DataModel.FaceCommandOutput), StringComparison.Ordinal)) { ScrollOutputTextBoxToEnd("FaceOutputTextBox"); return; } if (string.Equals(e.PropertyName, nameof(DataModel.FaceMatcherCommandOutput), StringComparison.Ordinal)) { ScrollOutputTextBoxToEnd("FaceMatcherOutputTextBox"); } } private void ScrollOutputTextBoxToEnd(string controlName) { var outputBox = this.FindControl(controlName); if (outputBox is null) { return; } Dispatcher.UIThread.Post(() => { var textLength = outputBox.Text?.Length ?? 0; outputBox.CaretIndex = textLength; }); } private async void OpenFaceMatcherPreview_Click(object? sender, RoutedEventArgs e) { if (sender is not Button { Tag: FaceMatcherResultItem item }) { return; } await OpenFaceMatcherPreviewAsync(item); } private async Task OpenFaceMatcherPreviewAsync(FaceMatcherResultItem item) { var owner = TopLevel.GetTopLevel(this) as Window; var dialog = BuildFaceMatcherPreviewDialog(item); if (owner is not null) { await dialog.ShowDialog(owner); return; } dialog.Show(); } private async void FaceMatcherResults_DoubleTapped(object? sender, TappedEventArgs e) { if (sender is not Avalonia.Controls.DataGrid { SelectedItem: FaceMatcherResultItem item }) { return; } await OpenFaceMatcherPreviewAsync(item); } private Window BuildFaceMatcherPreviewDialog(FaceMatcherResultItem item) { var dialog = new Window { 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; } } 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) { debugBuilder.AppendLine($"Dimensione file: {fileInfo.Length / 1024.0:F1} KB"); debugBuilder.AppendLine($"Ultima modifica: {fileInfo.LastWriteTime:yyyy-MM-dd HH:mm:ss}"); } 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}"); } if (!string.IsNullOrWhiteSpace(item.RawRow)) { 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), RowDefinitions = new RowDefinitions("Auto,*,Auto") }; 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}%"; } var toolbar = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8, Margin = new Avalonia.Thickness(0, 0, 0, 12) }; 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"), MinHeight = 180, Margin = new Avalonia.Thickness(0, 12, 0, 0) }; 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" }; openFileButton.Click += (_, _) => PathShellService.OpenInExplorer(item.ResolvedImagePath); footer.Children.Add(openFileButton); var openFolderButton = new Button { Content = "Apri cartella" }; openFolderButton.Click += (_, _) => { var directory = Path.GetDirectoryName(item.ResolvedImagePath); PathShellService.OpenInExplorer(string.IsNullOrWhiteSpace(directory) ? item.ResolvedImagePath : directory); }; 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; } }