using Avalonia.Controls; using Avalonia.Interactivity; using Avalonia.Input; using Avalonia.Layout; using Avalonia.Media; using Avalonia.Media.Imaging; using Avalonia.Platform.Storage; using Avalonia.Threading; using ImageCatalog_2.Models; using ImageCatalog_2.Services; using Microsoft.Extensions.DependencyInjection; using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.IO; using System.Text; using System.Threading.Tasks; namespace ImageCatalog_2.AvaloniaViews; public partial class FaceAiTabView : Avalonia.Controls.UserControl { private INotifyPropertyChanged? _faceAiPropertySource; private readonly PickerPreferenceService _pickerPreferenceService; public FaceAiTabView() { _pickerPreferenceService = Program.ServiceProvider.GetRequiredService(); 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 SelectFaceExecutable_Click(object? sender, RoutedEventArgs e) { var currentPath = DataContext is DataModel currentModel ? currentModel.FaceExecutablePath : null; var folders = await OpenFolderPickerAsync("Seleziona la cartella Face Recognition Windows", PickerPreferenceKeys.FaceExecutableFolder, currentPath); if (folders.Count > 0 && DataContext is DataModel model) { model.FaceExecutablePath = folders[0].Path.LocalPath; _pickerPreferenceService.RememberPath(PickerPreferenceKeys.FaceExecutableFolder, model.FaceExecutablePath); } } private async void SelectFaceOutputFolder_Click(object? sender, RoutedEventArgs e) { var currentPath = DataContext is DataModel currentModel ? currentModel.FaceOutputFolderPath : null; var folders = await OpenFolderPickerAsync("Seleziona la cartella output per encodings e log", PickerPreferenceKeys.FaceOutputFolder, currentPath); if (folders.Count > 0 && DataContext is DataModel model) { model.FaceOutputFolderPath = folders[0].Path.LocalPath; _pickerPreferenceService.RememberPath(PickerPreferenceKeys.FaceOutputFolder, model.FaceOutputFolderPath); } } private async void SelectFaceMatcherExecutable_Click(object? sender, RoutedEventArgs e) { var files = await OpenFilePickerAsync( "Seleziona face_matcher.exe", [new FilePickerFileType("Eseguibile") { Patterns = ["*.exe"] }], PickerPreferenceKeys.FaceMatcherExecutable, DataContext is DataModel currentModel ? currentModel.FaceMatcherExecutablePath : null); if (files.Count > 0 && DataContext is DataModel model) { model.FaceMatcherExecutablePath = files[0].Path.LocalPath; _pickerPreferenceService.RememberPath(PickerPreferenceKeys.FaceMatcherExecutable, model.FaceMatcherExecutablePath); } } private async void SelectFaceMatcherImage_Click(object? sender, RoutedEventArgs e) { var files = await OpenFilePickerAsync( "Seleziona immagine per il match", [new FilePickerFileType("Immagini") { Patterns = ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"] }], PickerPreferenceKeys.FaceMatcherImage, DataContext is DataModel currentModel ? currentModel.FaceMatcherSelectedImagePath : null); if (files.Count > 0 && DataContext is DataModel model) { model.FaceMatcherSelectedImagePath = files[0].Path.LocalPath; _pickerPreferenceService.RememberPath(PickerPreferenceKeys.FaceMatcherImage, model.FaceMatcherSelectedImagePath); } } private async void SelectFaceMatcherEncodings_Click(object? sender, RoutedEventArgs e) { var files = await OpenFilePickerAsync( "Seleziona file encodings .pkl", [new FilePickerFileType("Encodings") { Patterns = ["*.pkl"] }], PickerPreferenceKeys.FaceMatcherEncodings, DataContext is DataModel currentModel ? currentModel.FaceMatcherEncodingsPath : null); if (files.Count > 0 && DataContext is DataModel model) { model.FaceMatcherEncodingsPath = files[0].Path.LocalPath; _pickerPreferenceService.RememberPath(PickerPreferenceKeys.FaceMatcherEncodings, model.FaceMatcherEncodingsPath); } } private async void SelectFaceMatcherOutput_Click(object? sender, RoutedEventArgs e) { var file = await SaveFilePickerAsync( "Seleziona output CSV del matcher", "csv", [new FilePickerFileType("CSV") { Patterns = ["*.csv"] }], PickerPreferenceKeys.FaceMatcherOutput, DataContext is DataModel currentModel ? currentModel.FaceMatcherOutputPath : null); if (file is not null && DataContext is DataModel model) { model.FaceMatcherOutputPath = file.Path.LocalPath; _pickerPreferenceService.RememberPath(PickerPreferenceKeys.FaceMatcherOutput, model.FaceMatcherOutputPath); } } private async void SelectFaceMatcherLog_Click(object? sender, RoutedEventArgs e) { var file = await SaveFilePickerAsync( "Seleziona log TXT del matcher", "txt", [new FilePickerFileType("Log") { Patterns = ["*.txt", "*.log"] }], PickerPreferenceKeys.FaceMatcherLog, DataContext is DataModel currentModel ? currentModel.FaceMatcherLogPath : null); if (file is not null && DataContext is DataModel model) { model.FaceMatcherLogPath = file.Path.LocalPath; _pickerPreferenceService.RememberPath(PickerPreferenceKeys.FaceMatcherLog, model.FaceMatcherLogPath); } } private void OpenFaceExecutableFolder_Click(object? sender, RoutedEventArgs e) => OpenFromTextBox("FaceExecutablePathTextBox"); private void OpenFaceOutputFolder_Click(object? sender, RoutedEventArgs e) => OpenFromTextBox("FaceOutputFolderTextBox"); private void OpenFaceMatcherExecutable_Click(object? sender, RoutedEventArgs e) => OpenFromTextBox("FaceMatcherExecutablePathTextBox"); private void OpenFaceMatcherImage_Click(object? sender, RoutedEventArgs e) => OpenFromTextBox("FaceMatcherImagePathTextBox"); private void OpenFaceMatcherEncodings_Click(object? sender, RoutedEventArgs e) => OpenFromTextBox("FaceMatcherEncodingsPathTextBox"); private void OpenFaceMatcherOutput_Click(object? sender, RoutedEventArgs e) => OpenFromTextBox("FaceMatcherOutputPathTextBox"); private void OpenFaceMatcherLog_Click(object? sender, RoutedEventArgs e) => OpenFromTextBox("FaceMatcherLogPathTextBox"); private void OpenFaceDestinationFolder_Click(object? sender, RoutedEventArgs e) { string? path = null; if (DataContext is DataModel model) { path = (model.DestinationPath ?? string.Empty).Trim(); } if (string.IsNullOrWhiteSpace(path)) { return; } if (Directory.Exists(path)) { OpenInExplorer(path); return; } var directory = Path.GetDirectoryName(path); OpenInExplorer(string.IsNullOrWhiteSpace(directory) ? path : directory); } 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 += (_, _) => OpenInExplorer(item.ResolvedImagePath); footer.Children.Add(openFileButton); var openFolderButton = new Button { Content = "Apri cartella" }; openFolderButton.Click += (_, _) => { var directory = Path.GetDirectoryName(item.ResolvedImagePath); 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; } private void OpenFromTextBox(string textBoxName) { var textBox = this.FindControl(textBoxName); var path = textBox?.Text?.Trim(); if (string.IsNullOrWhiteSpace(path)) { return; } if (Directory.Exists(path) || File.Exists(path)) { OpenInExplorer(path); return; } var directory = Path.GetDirectoryName(path); OpenInExplorer(string.IsNullOrWhiteSpace(directory) ? path : directory); } private async Task> OpenFolderPickerAsync(string title, string preferenceKey, string? currentPath) { var topLevel = TopLevel.GetTopLevel(this); var storageProvider = topLevel?.StorageProvider; if (storageProvider is null) { return Array.Empty(); } var suggestedStartLocation = await _pickerPreferenceService.TryGetStartFolderAsync(storageProvider, preferenceKey, currentPath); return await storageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions { Title = title, SuggestedStartLocation = suggestedStartLocation }); } private async Task> OpenFilePickerAsync(string title, IReadOnlyList fileTypes, string preferenceKey, string? currentPath) { var topLevel = TopLevel.GetTopLevel(this); var storageProvider = topLevel?.StorageProvider; if (storageProvider is null) { return Array.Empty(); } var suggestedStartLocation = await _pickerPreferenceService.TryGetStartFolderAsync(storageProvider, preferenceKey, currentPath); return await storageProvider.OpenFilePickerAsync(new FilePickerOpenOptions { Title = title, FileTypeFilter = fileTypes, SuggestedStartLocation = suggestedStartLocation }); } private async Task SaveFilePickerAsync(string title, string defaultExtension, IReadOnlyList fileTypes, string preferenceKey, string? currentPath) { var topLevel = TopLevel.GetTopLevel(this); var storageProvider = topLevel?.StorageProvider; if (storageProvider is null) { return null; } var suggestedStartLocation = await _pickerPreferenceService.TryGetStartFolderAsync(storageProvider, preferenceKey, currentPath); return await storageProvider.SaveFilePickerAsync(new FilePickerSaveOptions { Title = title, DefaultExtension = defaultExtension, FileTypeChoices = fileTypes, SuggestedStartLocation = suggestedStartLocation }); } private static void OpenInExplorer(string? path) { if (string.IsNullOrWhiteSpace(path)) { return; } var normalizedPath = path.Trim().Trim('"'); try { if (File.Exists(normalizedPath)) { Process.Start("explorer.exe", $"/select,\"{normalizedPath}\""); } else if (Directory.Exists(normalizedPath)) { Process.Start(new ProcessStartInfo { FileName = normalizedPath, UseShellExecute = true }); } } catch { // Ignore failures when opening Explorer. } } }