Refactor path handling in UI components
Some checks failed
Build Windows Avalonia / build (push) Failing after 1m46s
Build Windows Avalonia / release (push) Has been skipped

- Removed redundant folder and file opening methods from AiTabView and FaceAiTabView.
- Introduced PathPickerField control to streamline path selection and opening functionality across multiple views.
- Updated FaceAiTabView and GeneralTabView to utilize PathPickerField for source and destination path selection.
- Created PathShellService to encapsulate logic for opening paths in the file explorer.
- Simplified XAML structure by replacing manual grid definitions with PathPickerField components.
- Removed unused namespaces and cleaned up code for better readability and maintainability.
This commit is contained in:
Maddo 2026-05-24 19:07:17 +02:00
commit 398cfa310e
10 changed files with 484 additions and 562 deletions

View file

@ -1,6 +1,7 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:avaloniaDataGrid="clr-namespace:Avalonia.Controls;assembly=Avalonia.Controls.DataGrid"
xmlns:controls="clr-namespace:ImageCatalog_2.Controls"
xmlns:iconPacks="https://github.com/MahApps/IconPacks.Avalonia"
x:Class="ImageCatalog_2.AvaloniaViews.AiTabView"
x:CompileBindings="False">
@ -31,25 +32,13 @@
FontWeight="SemiBold" />
</Grid>
<Grid ColumnDefinitions="Auto,*,Auto,Auto" ColumnSpacing="6">
<TextBlock Grid.Column="0" Text="Sorgente:" VerticalAlignment="Center" />
<TextBox Grid.Column="1"
Text="{Binding DestinationPath, Mode=OneWay}"
IsReadOnly="True"
VerticalAlignment="Center" />
<Button Grid.Column="2" Width="104" Command="{Binding SelectDestinationFolderCommand}">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
<iconPacks:PackIconMaterial Kind="FolderOpenOutline" Width="14" Height="14" />
<TextBlock Text="Scegli..." />
</StackPanel>
</Button>
<Button Grid.Column="3" Width="72" Click="OpenAiSourceFolder_Click">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
<iconPacks:PackIconMaterial Kind="Folder" Width="14" Height="14" />
<TextBlock Text="Apri" />
</StackPanel>
</Button>
</Grid>
<controls:PathPickerField Label="Sorgente:"
Text="{Binding DestinationPath, Mode=TwoWay}"
IsTextReadOnly="True"
PreferenceKey="Picker.DestinationFolder.LastPath"
PickerTitle="Seleziona cartella sorgente AI"
PickerMode="Folder"
AppendDirectorySeparator="True" />
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left" Spacing="8">
<Button Command="{Binding StartAiCommand}" Width="132">
@ -67,22 +56,14 @@
</StackPanel>
<TextBlock Text="Output CSV" FontWeight="Bold" />
<Grid ColumnDefinitions="Auto,*,Auto,Auto" ColumnSpacing="6">
<TextBlock Text="Percorso CSV:" VerticalAlignment="Center" Margin="0,0,8,0" Grid.Column="0" />
<TextBox Text="{Binding CsvOutputPath, Mode=TwoWay}" Grid.Column="1" VerticalAlignment="Center" />
<Button Width="104" Command="{Binding SelectCsvOutputCommand}" Grid.Column="2">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
<iconPacks:PackIconMaterial Kind="FileOutline" Width="14" Height="14" />
<TextBlock Text="Scegli..." />
</StackPanel>
</Button>
<Button Width="72" Grid.Column="3" Click="OpenCsvOutputFolder_Click">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
<iconPacks:PackIconMaterial Kind="Folder" Width="14" Height="14" />
<TextBlock Text="Apri" />
</StackPanel>
</Button>
</Grid>
<controls:PathPickerField Label="Percorso CSV:"
Text="{Binding CsvOutputPath, Mode=TwoWay}"
PreferenceKey="Picker.CsvOutput.LastPath"
PickerTitle="Salva CSV"
PickerMode="SaveFile"
FileTypeName="CSV"
FilePatterns="*.csv"
DefaultExtension="csv" />
<TextBlock Text="Anteprima risultati" FontWeight="Bold" />
<ProgressBar Minimum="0" Maximum="100" Value="{Binding AiProgress}" Height="16" Margin="0,0,0,4" />
@ -108,22 +89,12 @@
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Margin="8" Spacing="8">
<TextBlock Text="Modelli" FontWeight="Bold" />
<Grid ColumnDefinitions="Auto,*,Auto,Auto" ColumnSpacing="6">
<TextBlock Text="Cartella modelli:" VerticalAlignment="Center" Margin="0,0,8,0" Grid.Column="0" />
<TextBox Text="{Binding ModelsFolderPath, Mode=TwoWay}" Grid.Column="1" VerticalAlignment="Center" />
<Button Width="104" Command="{Binding SelectModelsFolderCommand}" Grid.Column="2">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
<iconPacks:PackIconMaterial Kind="FolderOpenOutline" Width="14" Height="14" />
<TextBlock Text="Scegli..." />
</StackPanel>
</Button>
<Button Width="72" Grid.Column="3" Click="OpenModelsFolder_Click">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
<iconPacks:PackIconMaterial Kind="Folder" Width="14" Height="14" />
<TextBlock Text="Apri" />
</StackPanel>
</Button>
</Grid>
<controls:PathPickerField Label="Cartella modelli:"
Text="{Binding ModelsFolderPath, Mode=TwoWay}"
PreferenceKey="Picker.ModelsFolder.LastPath"
PickerTitle="Seleziona cartella modelli"
PickerMode="Folder"
AppendDirectorySeparator="True" />
</StackPanel>
</ScrollViewer>
</TabItem>

View file

@ -1,8 +1,4 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using System.Diagnostics;
using System.IO;
namespace ImageCatalog_2.AvaloniaViews;
public partial class AiTabView : Avalonia.Controls.UserControl
@ -11,56 +7,4 @@ public partial class AiTabView : Avalonia.Controls.UserControl
{
InitializeComponent();
}
private void OpenModelsFolder_Click(object? sender, RoutedEventArgs e)
{
if (DataContext is DataModel model)
{
OpenInExplorer(model.ModelsFolderPath);
}
}
private void OpenCsvOutputFolder_Click(object? sender, RoutedEventArgs e)
{
if (DataContext is not DataModel model)
{
return;
}
var directory = Path.GetDirectoryName(model.CsvOutputPath);
OpenInExplorer(string.IsNullOrWhiteSpace(directory) ? model.CsvOutputPath : directory);
}
private void OpenAiSourceFolder_Click(object? sender, RoutedEventArgs e)
{
if (DataContext is DataModel model)
{
OpenInExplorer(model.DestinationPath);
}
}
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.
}
}
}

View file

@ -1,6 +1,7 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:avaloniaDataGrid="clr-namespace:Avalonia.Controls;assembly=Avalonia.Controls.DataGrid"
xmlns:controls="clr-namespace:ImageCatalog_2.Controls"
xmlns:iconPacks="https://github.com/MahApps/IconPacks.Avalonia"
xmlns:converters="clr-namespace:ImageCatalog_2.Converters"
x:Class="ImageCatalog_2.AvaloniaViews.FaceAiTabView"
@ -37,34 +38,21 @@
<TextBlock Grid.Column="4" Text="Usa --multicore in CPU e --multiprocess in GPU." VerticalAlignment="Center" TextWrapping="Wrap" Opacity="0.75" />
</Grid>
<Grid ColumnDefinitions="Auto,*,Auto,Auto" ColumnSpacing="6">
<TextBlock Grid.Column="0" Text="Sorgente:" VerticalAlignment="Center" />
<TextBox Grid.Column="1" Name="FaceDestinationPathTextBox" Text="{Binding DestinationPath, Mode=OneWay}" IsReadOnly="True" />
<Button Grid.Column="3" Name="FaceOpenDestinationButton" Click="OpenFaceDestinationFolder_Click" Width="72" Margin="6,0,0,0">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
<iconPacks:PackIconMaterial Kind="Folder" Width="14" Height="14" />
<TextBlock Text="Apri" />
</StackPanel>
</Button>
</Grid>
<controls:PathPickerField Label="Sorgente:"
Text="{Binding DestinationPath, Mode=TwoWay}"
IsTextReadOnly="True"
PreferenceKey="Picker.DestinationFolder.LastPath"
PickerTitle="Seleziona cartella sorgente Face Encoder"
PickerMode="Folder"
AppendDirectorySeparator="True" />
<TextBlock Text="Output encodings" FontWeight="Bold" Margin="0,4,0,0" />
<Grid ColumnDefinitions="Auto,*,Auto,Auto" ColumnSpacing="6">
<TextBlock Grid.Column="0" Text="Cartella out:" VerticalAlignment="Center" />
<TextBox Grid.Column="1" Name="FaceOutputFolderTextBox" Text="{Binding FaceOutputFolderPath, Mode=TwoWay}" Watermark="C:\output\face_encoder" />
<Button Grid.Column="2" Name="FaceSelectOutputButton" Click="SelectFaceOutputFolder_Click" Width="104" Margin="6,0,0,0">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
<iconPacks:PackIconMaterial Kind="FolderOutline" Width="14" Height="14" />
<TextBlock Text="Scegli..." />
</StackPanel>
</Button>
<Button Grid.Column="3" Name="FaceOpenOutputButton" Click="OpenFaceOutputFolder_Click" Width="72" Margin="6,0,0,0">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
<iconPacks:PackIconMaterial Kind="Folder" Width="14" Height="14" />
<TextBlock Text="Apri" />
</StackPanel>
</Button>
</Grid>
<controls:PathPickerField Label="Cartella out:"
Text="{Binding FaceOutputFolderPath, Mode=TwoWay}"
Watermark="C:\output\face_encoder"
PreferenceKey="Picker.FaceOutputFolder.LastPath"
PickerTitle="Seleziona la cartella output per encodings e log"
PickerMode="Folder" />
<TextBlock Text="I file vengono creati come face_encodings_yyyyMMdd_HHmmss_nomecartella.pkl e encoder_log_yyyyMMdd_HHmmss_nomecartella.txt."
TextWrapping="Wrap"
Opacity="0.75" />
@ -116,22 +104,12 @@
TextWrapping="Wrap"
Opacity="0.75" />
<Grid ColumnDefinitions="Auto,*,Auto,Auto" ColumnSpacing="6">
<TextBlock Grid.Column="0" Text="Percorso:" VerticalAlignment="Center" />
<TextBox Grid.Column="1" Name="FaceExecutablePathTextBox" Text="{Binding FaceExecutablePath, Mode=TwoWay}" Watermark="C:\tools\Face_Recognition_Windows" />
<Button Grid.Column="2" Name="FaceSelectExecutableButton" Click="SelectFaceExecutable_Click" Width="104" Margin="6,0,0,0">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
<iconPacks:PackIconMaterial Kind="FolderOutline" Width="14" Height="14" />
<TextBlock Text="Scegli..." />
</StackPanel>
</Button>
<Button Grid.Column="3" Name="FaceOpenExecutableButton" Click="OpenFaceExecutableFolder_Click" Width="72" Margin="6,0,0,0">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
<iconPacks:PackIconMaterial Kind="Folder" Width="14" Height="14" />
<TextBlock Text="Apri" />
</StackPanel>
</Button>
</Grid>
<controls:PathPickerField Label="Percorso:"
Text="{Binding FaceExecutablePath, Mode=TwoWay}"
Watermark="C:\tools\Face_Recognition_Windows"
PreferenceKey="Picker.FaceExecutableFolder.LastPath"
PickerTitle="Seleziona la cartella Face Recognition Windows"
PickerMode="Folder" />
</StackPanel>
</ScrollViewer>
</TabItem>
@ -146,40 +124,24 @@
TextWrapping="Wrap" Opacity="0.8" />
<TextBlock Text="Eseguibile matcher" FontWeight="Bold" Margin="0,4,0,0" />
<Grid ColumnDefinitions="Auto,*,Auto,Auto" ColumnSpacing="6">
<TextBlock Grid.Column="0" Text="Percorso:" VerticalAlignment="Center" />
<TextBox Grid.Column="1" Name="FaceMatcherExecutablePathTextBox" Text="{Binding FaceMatcherExecutablePath, Mode=TwoWay}" Watermark="C:\tools\Face_Recognition_Windows\face_matcher.exe" />
<Button Grid.Column="2" Click="SelectFaceMatcherExecutable_Click" Width="104" Margin="6,0,0,0">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
<iconPacks:PackIconMaterial Kind="FileOutline" Width="14" Height="14" />
<TextBlock Text="Scegli..." />
</StackPanel>
</Button>
<Button Grid.Column="3" Click="OpenFaceMatcherExecutable_Click" Width="72" Margin="6,0,0,0">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
<iconPacks:PackIconMaterial Kind="Folder" Width="14" Height="14" />
<TextBlock Text="Apri" />
</StackPanel>
</Button>
</Grid>
<controls:PathPickerField Label="Percorso:"
Text="{Binding FaceMatcherExecutablePath, Mode=TwoWay}"
Watermark="C:\tools\Face_Recognition_Windows\face_matcher.exe"
PreferenceKey="Picker.FaceMatcherExecutable.LastPath"
PickerTitle="Seleziona face_matcher.exe"
PickerMode="OpenFile"
FileTypeName="Eseguibile"
FilePatterns="*.exe" />
<TextBlock Text="Immagine da testare" FontWeight="Bold" Margin="0,4,0,0" />
<Grid ColumnDefinitions="Auto,*,Auto,Auto" ColumnSpacing="6">
<TextBlock Grid.Column="0" Text="Immagine:" VerticalAlignment="Center" />
<TextBox Grid.Column="1" Name="FaceMatcherImagePathTextBox" Text="{Binding FaceMatcherSelectedImagePath, Mode=TwoWay}" Watermark="Percorso immagine da usare per il match" />
<Button Grid.Column="2" Click="SelectFaceMatcherImage_Click" Width="104" Margin="6,0,0,0">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
<iconPacks:PackIconMaterial Kind="ImageOutline" Width="14" Height="14" />
<TextBlock Text="Scegli..." />
</StackPanel>
</Button>
<Button Grid.Column="3" Click="OpenFaceMatcherImage_Click" Width="72" Margin="6,0,0,0">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
<iconPacks:PackIconMaterial Kind="Folder" Width="14" Height="14" />
<TextBlock Text="Apri" />
</StackPanel>
</Button>
</Grid>
<controls:PathPickerField Label="Immagine:"
Text="{Binding FaceMatcherSelectedImagePath, Mode=TwoWay}"
Watermark="Percorso immagine da usare per il match"
PreferenceKey="Picker.FaceMatcherImage.LastPath"
PickerTitle="Seleziona immagine per il match"
PickerMode="OpenFile"
FileTypeName="Immagini"
FilePatterns="*.jpg;*.jpeg;*.png;*.bmp;*.gif;*.webp" />
<TextBlock Text="Questa immagine resta solo runtime e non viene salvata nel file impostazioni."
TextWrapping="Wrap"
Opacity="0.75" />
@ -193,68 +155,43 @@
</Border>
</StackPanel>
<Grid ColumnDefinitions="Auto,*,Auto" ColumnSpacing="6">
<TextBlock Grid.Column="0" Text="Destinazione attuale:" VerticalAlignment="Center" />
<TextBox Grid.Column="1" Name="FaceMatcherDestinationPathTextBox" Text="{Binding DestinationPath, Mode=OneWay}" IsReadOnly="True" />
<Button Grid.Column="2" Click="OpenFaceDestinationFolder_Click" Width="72" Margin="6,0,0,0">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
<iconPacks:PackIconMaterial Kind="Folder" Width="14" Height="14" />
<TextBlock Text="Apri" />
</StackPanel>
</Button>
</Grid>
<controls:PathPickerField Label="Destinazione attuale:"
Text="{Binding DestinationPath, Mode=TwoWay}"
IsTextReadOnly="True"
PreferenceKey="Picker.DestinationFolder.LastPath"
PickerTitle="Seleziona cartella destinazione Face Matcher"
PickerMode="Folder"
AppendDirectorySeparator="True" />
<TextBlock Text="Encodings e file di appoggio" FontWeight="Bold" Margin="0,4,0,0" />
<Grid ColumnDefinitions="Auto,*,Auto,Auto" ColumnSpacing="6">
<TextBlock Grid.Column="0" Text="Encodings .pkl:" VerticalAlignment="Center" />
<TextBox Grid.Column="1" Name="FaceMatcherEncodingsPathTextBox" Text="{Binding FaceMatcherEncodingsPath, Mode=TwoWay}" Watermark="Se vuoto usa l'ultimo .pkl in output encodings" />
<Button Grid.Column="2" Click="SelectFaceMatcherEncodings_Click" Width="104" Margin="6,0,0,0">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
<iconPacks:PackIconMaterial Kind="FileCodeOutline" Width="14" Height="14" />
<TextBlock Text="Scegli..." />
</StackPanel>
</Button>
<Button Grid.Column="3" Click="OpenFaceMatcherEncodings_Click" Width="72" Margin="6,0,0,0">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
<iconPacks:PackIconMaterial Kind="Folder" Width="14" Height="14" />
<TextBlock Text="Apri" />
</StackPanel>
</Button>
</Grid>
<controls:PathPickerField Label="Encodings .pkl:"
Text="{Binding FaceMatcherEncodingsPath, Mode=TwoWay}"
Watermark="Se vuoto usa l'ultimo .pkl in output encodings"
PreferenceKey="Picker.FaceMatcherEncodings.LastPath"
PickerTitle="Seleziona file encodings .pkl"
PickerMode="OpenFile"
FileTypeName="Encodings"
FilePatterns="*.pkl" />
<Grid ColumnDefinitions="Auto,*,Auto,Auto" ColumnSpacing="6">
<TextBlock Grid.Column="0" Text="Risultato CSV:" VerticalAlignment="Center" />
<TextBox Grid.Column="1" Name="FaceMatcherOutputPathTextBox" Text="{Binding FaceMatcherOutputPath, Mode=TwoWay}" Watermark="Opzionale: file/cartella output CSV" />
<Button Grid.Column="2" Click="SelectFaceMatcherOutput_Click" Width="104" Margin="6,0,0,0">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
<iconPacks:PackIconMaterial Kind="FileDelimitedOutline" Width="14" Height="14" />
<TextBlock Text="Scegli..." />
</StackPanel>
</Button>
<Button Grid.Column="3" Click="OpenFaceMatcherOutput_Click" Width="72" Margin="6,0,0,0">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
<iconPacks:PackIconMaterial Kind="Folder" Width="14" Height="14" />
<TextBlock Text="Apri" />
</StackPanel>
</Button>
</Grid>
<controls:PathPickerField Label="Risultato CSV:"
Text="{Binding FaceMatcherOutputPath, Mode=TwoWay}"
Watermark="Opzionale: file/cartella output CSV"
PreferenceKey="Picker.FaceMatcherOutput.LastPath"
PickerTitle="Seleziona output CSV del matcher"
PickerMode="SaveFile"
FileTypeName="CSV"
FilePatterns="*.csv"
DefaultExtension="csv" />
<Grid ColumnDefinitions="Auto,*,Auto,Auto" ColumnSpacing="6">
<TextBlock Grid.Column="0" Text="Log matcher:" VerticalAlignment="Center" />
<TextBox Grid.Column="1" Name="FaceMatcherLogPathTextBox" Text="{Binding FaceMatcherLogPath, Mode=TwoWay}" Watermark="Opzionale: file/cartella log txt" />
<Button Grid.Column="2" Click="SelectFaceMatcherLog_Click" Width="104" Margin="6,0,0,0">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
<iconPacks:PackIconMaterial Kind="FileDocumentOutline" Width="14" Height="14" />
<TextBlock Text="Scegli..." />
</StackPanel>
</Button>
<Button Grid.Column="3" Click="OpenFaceMatcherLog_Click" Width="72" Margin="6,0,0,0">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
<iconPacks:PackIconMaterial Kind="Folder" Width="14" Height="14" />
<TextBlock Text="Apri" />
</StackPanel>
</Button>
</Grid>
<controls:PathPickerField Label="Log matcher:"
Text="{Binding FaceMatcherLogPath, Mode=TwoWay}"
Watermark="Opzionale: file/cartella log txt"
PreferenceKey="Picker.FaceMatcherLog.LastPath"
PickerTitle="Seleziona log TXT del matcher"
PickerMode="SaveFile"
FileTypeName="Log"
FilePatterns="*.txt;*.log"
DefaultExtension="txt" />
<Grid ColumnDefinitions="Auto,100,*" ColumnSpacing="6">
<TextBlock Grid.Column="0" Text="Tolleranza:" VerticalAlignment="Center" />

View file

@ -4,15 +4,11 @@ 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;
@ -22,11 +18,9 @@ namespace ImageCatalog_2.AvaloniaViews;
public partial class FaceAiTabView : Avalonia.Controls.UserControl
{
private INotifyPropertyChanged? _faceAiPropertySource;
private readonly PickerPreferenceService _pickerPreferenceService;
public FaceAiTabView()
{
_pickerPreferenceService = Program.ServiceProvider.GetRequiredService<PickerPreferenceService>();
InitializeComponent();
DataContextChanged += OnDataContextChanged;
}
@ -74,142 +68,6 @@ public partial class FaceAiTabView : Avalonia.Controls.UserControl
});
}
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 })
@ -444,14 +302,14 @@ public partial class FaceAiTabView : Avalonia.Controls.UserControl
Grid.SetRow(footer, 2);
var openFileButton = new Button { Content = "Apri file" };
openFileButton.Click += (_, _) => OpenInExplorer(item.ResolvedImagePath);
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);
OpenInExplorer(string.IsNullOrWhiteSpace(directory) ? item.ResolvedImagePath : directory);
PathShellService.OpenInExplorer(string.IsNullOrWhiteSpace(directory) ? item.ResolvedImagePath : directory);
};
footer.Children.Add(openFolderButton);
@ -464,104 +322,4 @@ public partial class FaceAiTabView : Avalonia.Controls.UserControl
return dialog;
}
private void OpenFromTextBox(string textBoxName)
{
var textBox = this.FindControl<Avalonia.Controls.TextBox>(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<IReadOnlyList<IStorageFolder>> OpenFolderPickerAsync(string title, string preferenceKey, string? currentPath)
{
var topLevel = TopLevel.GetTopLevel(this);
var storageProvider = topLevel?.StorageProvider;
if (storageProvider is null)
{
return Array.Empty<IStorageFolder>();
}
var suggestedStartLocation = await _pickerPreferenceService.TryGetStartFolderAsync(storageProvider, preferenceKey, currentPath);
return await storageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
{
Title = title,
SuggestedStartLocation = suggestedStartLocation
});
}
private async Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(string title, IReadOnlyList<FilePickerFileType> fileTypes, string preferenceKey, string? currentPath)
{
var topLevel = TopLevel.GetTopLevel(this);
var storageProvider = topLevel?.StorageProvider;
if (storageProvider is null)
{
return Array.Empty<IStorageFile>();
}
var suggestedStartLocation = await _pickerPreferenceService.TryGetStartFolderAsync(storageProvider, preferenceKey, currentPath);
return await storageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
{
Title = title,
FileTypeFilter = fileTypes,
SuggestedStartLocation = suggestedStartLocation
});
}
private async Task<IStorageFile?> SaveFilePickerAsync(string title, string defaultExtension, IReadOnlyList<FilePickerFileType> 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.
}
}
}

View file

@ -1,43 +1,24 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:iconPacks="https://github.com/MahApps/IconPacks.Avalonia"
xmlns:controls="clr-namespace:ImageCatalog_2.Controls"
x:Class="ImageCatalog_2.AvaloniaViews.GeneralTabView">
<ScrollViewer>
<StackPanel Margin="4" Spacing="8">
<TextBlock Text="Percorsi" FontWeight="Bold" />
<StackPanel Margin="0,2,0,0" Spacing="6">
<Grid Margin="0,0,0,2" ColumnDefinitions="Auto,*,Auto,Auto" ColumnSpacing="6">
<TextBlock Text="Sorgente:" VerticalAlignment="Center" Margin="0,0,8,0" Grid.Column="0" />
<TextBox Text="{Binding SourcePath, Mode=TwoWay}" Grid.Column="1" VerticalAlignment="Center" />
<Button Width="104" Command="{Binding SelectSourceFolderCommand}" Grid.Column="2">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
<iconPacks:PackIconMaterial Kind="FolderOpenOutline" Width="14" Height="14" />
<TextBlock Text="Scegli..." />
</StackPanel>
</Button>
<Button Width="72" Grid.Column="3" Click="OpenSourceFolder_Click">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
<iconPacks:PackIconMaterial Kind="Folder" Width="14" Height="14" />
<TextBlock Text="Apri" />
</StackPanel>
</Button>
</Grid>
<Grid ColumnDefinitions="Auto,*,Auto,Auto" ColumnSpacing="6">
<TextBlock Text="Destinazione:" VerticalAlignment="Center" Margin="0,0,8,0" Grid.Column="0" />
<TextBox Text="{Binding DestinationPath, Mode=TwoWay}" Grid.Column="1" VerticalAlignment="Center" />
<Button Width="104" Command="{Binding SelectDestinationFolderCommand}" Grid.Column="2">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
<iconPacks:PackIconMaterial Kind="FolderOpenOutline" Width="14" Height="14" />
<TextBlock Text="Scegli..." />
</StackPanel>
</Button>
<Button Width="72" Grid.Column="3" Click="OpenDestinationFolder_Click">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
<iconPacks:PackIconMaterial Kind="Folder" Width="14" Height="14" />
<TextBlock Text="Apri" />
</StackPanel>
</Button>
</Grid>
<controls:PathPickerField Margin="0,0,0,2"
Label="Sorgente:"
Text="{Binding SourcePath, Mode=TwoWay}"
PreferenceKey="Picker.SourceFolder.LastPath"
PickerTitle="Seleziona cartella sorgente"
PickerMode="Folder"
AppendDirectorySeparator="True" />
<controls:PathPickerField Label="Destinazione:"
Text="{Binding DestinationPath, Mode=TwoWay}"
PreferenceKey="Picker.DestinationFolder.LastPath"
PickerTitle="Seleziona cartella destinazione"
PickerMode="Folder"
AppendDirectorySeparator="True" />
</StackPanel>
<Grid ColumnDefinitions="*,*" ColumnSpacing="24" Margin="0,4,0,0">

View file

@ -1,9 +1,4 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using System;
using System.Diagnostics;
using System.IO;
namespace ImageCatalog_2.AvaloniaViews;
public partial class GeneralTabView : Avalonia.Controls.UserControl
@ -12,45 +7,4 @@ public partial class GeneralTabView : Avalonia.Controls.UserControl
{
InitializeComponent();
}
private void OpenSourceFolder_Click(object? sender, RoutedEventArgs e)
{
if (DataContext is DataModel model)
{
OpenInExplorer(model.SourcePath);
}
}
private void OpenDestinationFolder_Click(object? sender, RoutedEventArgs e)
{
if (DataContext is DataModel model)
{
OpenInExplorer(model.DestinationPath);
}
}
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.
}
}
}

View file

@ -0,0 +1,34 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:iconPacks="https://github.com/MahApps/IconPacks.Avalonia"
x:Class="ImageCatalog_2.Controls.PathPickerField"
x:Name="Root">
<Grid ColumnDefinitions="Auto,*,Auto,Auto" ColumnSpacing="6">
<TextBlock Grid.Column="0"
Text="{Binding Label, ElementName=Root}"
VerticalAlignment="Center"
Margin="0,0,8,0" />
<TextBox Grid.Column="1"
Text="{Binding Text, ElementName=Root, Mode=TwoWay}"
IsReadOnly="{Binding IsTextReadOnly, ElementName=Root}"
Watermark="{Binding Watermark, ElementName=Root}"
VerticalAlignment="Center" />
<Button Grid.Column="2"
Width="104"
IsVisible="{Binding ShowPickerButton, ElementName=Root}"
Click="PickPath_Click">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
<iconPacks:PackIconMaterial Kind="FolderOpenOutline" Width="14" Height="14" />
<TextBlock Text="Scegli..." />
</StackPanel>
</Button>
<Button Grid.Column="3"
Width="72"
Click="OpenPath_Click">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
<iconPacks:PackIconMaterial Kind="Folder" Width="14" Height="14" />
<TextBlock Text="Apri" />
</StackPanel>
</Button>
</Grid>
</UserControl>

View file

@ -0,0 +1,285 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Platform.Storage;
using ImageCatalog_2.Services;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace ImageCatalog_2.Controls;
public partial class PathPickerField : UserControl
{
private readonly PickerPreferenceService _pickerPreferenceService;
public static readonly StyledProperty<string> LabelProperty =
AvaloniaProperty.Register<PathPickerField, string>(nameof(Label), string.Empty);
public static readonly StyledProperty<string> TextProperty =
AvaloniaProperty.Register<PathPickerField, string>(nameof(Text), string.Empty, defaultBindingMode: BindingMode.TwoWay);
public static readonly StyledProperty<string> WatermarkProperty =
AvaloniaProperty.Register<PathPickerField, string>(nameof(Watermark), string.Empty);
public static readonly StyledProperty<string> PreferenceKeyProperty =
AvaloniaProperty.Register<PathPickerField, string>(nameof(PreferenceKey), string.Empty);
public static readonly StyledProperty<string> PickerTitleProperty =
AvaloniaProperty.Register<PathPickerField, string>(nameof(PickerTitle), string.Empty);
public static readonly StyledProperty<string> FileTypeNameProperty =
AvaloniaProperty.Register<PathPickerField, string>(nameof(FileTypeName), string.Empty);
public static readonly StyledProperty<string> FilePatternsProperty =
AvaloniaProperty.Register<PathPickerField, string>(nameof(FilePatterns), string.Empty);
public static readonly StyledProperty<string> DefaultExtensionProperty =
AvaloniaProperty.Register<PathPickerField, string>(nameof(DefaultExtension), string.Empty);
public static readonly StyledProperty<PathPickerSelectionMode> PickerModeProperty =
AvaloniaProperty.Register<PathPickerField, PathPickerSelectionMode>(nameof(PickerMode), PathPickerSelectionMode.Folder);
public static readonly StyledProperty<bool> IsTextReadOnlyProperty =
AvaloniaProperty.Register<PathPickerField, bool>(nameof(IsTextReadOnly), false);
public static readonly StyledProperty<bool> ShowPickerButtonProperty =
AvaloniaProperty.Register<PathPickerField, bool>(nameof(ShowPickerButton), true);
public static readonly StyledProperty<bool> AppendDirectorySeparatorProperty =
AvaloniaProperty.Register<PathPickerField, bool>(nameof(AppendDirectorySeparator), false);
public PathPickerField()
{
_pickerPreferenceService = Program.ServiceProvider.GetRequiredService<PickerPreferenceService>();
InitializeComponent();
}
public string Label
{
get => GetValue(LabelProperty);
set => SetValue(LabelProperty, value);
}
public string Text
{
get => GetValue(TextProperty);
set => SetValue(TextProperty, value);
}
public string Watermark
{
get => GetValue(WatermarkProperty);
set => SetValue(WatermarkProperty, value);
}
public string PreferenceKey
{
get => GetValue(PreferenceKeyProperty);
set => SetValue(PreferenceKeyProperty, value);
}
public string PickerTitle
{
get => GetValue(PickerTitleProperty);
set => SetValue(PickerTitleProperty, value);
}
public string FileTypeName
{
get => GetValue(FileTypeNameProperty);
set => SetValue(FileTypeNameProperty, value);
}
public string FilePatterns
{
get => GetValue(FilePatternsProperty);
set => SetValue(FilePatternsProperty, value);
}
public string DefaultExtension
{
get => GetValue(DefaultExtensionProperty);
set => SetValue(DefaultExtensionProperty, value);
}
public PathPickerSelectionMode PickerMode
{
get => GetValue(PickerModeProperty);
set => SetValue(PickerModeProperty, value);
}
public bool IsTextReadOnly
{
get => GetValue(IsTextReadOnlyProperty);
set => SetValue(IsTextReadOnlyProperty, value);
}
public bool ShowPickerButton
{
get => GetValue(ShowPickerButtonProperty);
set => SetValue(ShowPickerButtonProperty, value);
}
public bool AppendDirectorySeparator
{
get => GetValue(AppendDirectorySeparatorProperty);
set => SetValue(AppendDirectorySeparatorProperty, value);
}
private async void PickPath_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
var topLevel = TopLevel.GetTopLevel(this);
var storageProvider = topLevel?.StorageProvider;
if (storageProvider is null)
{
return;
}
var selectedPath = await PickPathAsync(storageProvider);
if (string.IsNullOrWhiteSpace(selectedPath))
{
return;
}
Text = selectedPath;
if (!string.IsNullOrWhiteSpace(PreferenceKey))
{
_pickerPreferenceService.RememberPath(PreferenceKey, selectedPath);
}
}
private void OpenPath_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
PathShellService.OpenInExplorer(Text);
}
private async Task<string?> PickPathAsync(IStorageProvider storageProvider)
{
var suggestedStartLocation = await TryGetSuggestedStartLocationAsync(storageProvider);
var pickerTitle = ResolvePickerTitle();
switch (PickerMode)
{
case PathPickerSelectionMode.Folder:
{
var folders = await storageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
{
Title = pickerTitle,
SuggestedStartLocation = suggestedStartLocation
});
return folders.Count == 0
? null
: NormalizeSelectedPath(folders[0].Path.LocalPath);
}
case PathPickerSelectionMode.OpenFile:
{
var files = await storageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
{
Title = pickerTitle,
SuggestedStartLocation = suggestedStartLocation,
FileTypeFilter = BuildFileTypes()
});
return files.Count == 0
? null
: files[0].Path.LocalPath;
}
case PathPickerSelectionMode.SaveFile:
{
var file = await storageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
{
Title = pickerTitle,
SuggestedStartLocation = suggestedStartLocation,
DefaultExtension = NormalizeDefaultExtension(DefaultExtension),
FileTypeChoices = BuildFileTypes()
});
return file?.Path.LocalPath;
}
default:
return null;
}
}
private async Task<IStorageFolder?> TryGetSuggestedStartLocationAsync(IStorageProvider storageProvider)
{
if (string.IsNullOrWhiteSpace(PreferenceKey))
{
return null;
}
return await _pickerPreferenceService.TryGetStartFolderAsync(storageProvider, PreferenceKey, Text);
}
private IReadOnlyList<FilePickerFileType> BuildFileTypes()
{
var patterns = (FilePatterns ?? string.Empty)
.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (patterns.Length == 0)
{
return Array.Empty<FilePickerFileType>();
}
var fileTypeName = string.IsNullOrWhiteSpace(FileTypeName)
? "File"
: FileTypeName.Trim();
return
[
new FilePickerFileType(fileTypeName)
{
Patterns = patterns.ToList()
}
];
}
private string ResolvePickerTitle()
{
if (!string.IsNullOrWhiteSpace(PickerTitle))
{
return PickerTitle;
}
var cleanedLabel = string.IsNullOrWhiteSpace(Label)
? "percorso"
: Label.Trim().TrimEnd(':');
return PickerMode == PathPickerSelectionMode.SaveFile
? $"Salva {cleanedLabel.ToLowerInvariant()}"
: $"Seleziona {cleanedLabel.ToLowerInvariant()}";
}
private string NormalizeSelectedPath(string selectedPath)
{
if (PickerMode != PathPickerSelectionMode.Folder || !AppendDirectorySeparator)
{
return selectedPath;
}
if (string.IsNullOrWhiteSpace(selectedPath))
{
return string.Empty;
}
return selectedPath.EndsWith(Path.DirectorySeparatorChar)
|| selectedPath.EndsWith(Path.AltDirectorySeparatorChar)
? selectedPath
: selectedPath + Path.DirectorySeparatorChar;
}
private static string NormalizeDefaultExtension(string extension)
{
if (string.IsNullOrWhiteSpace(extension))
{
return string.Empty;
}
return extension.Trim().TrimStart('.');
}
}

View file

@ -0,0 +1,8 @@
namespace ImageCatalog_2.Controls;
public enum PathPickerSelectionMode
{
Folder,
OpenFile,
SaveFile
}

View file

@ -0,0 +1,50 @@
using System.Diagnostics;
using System.IO;
namespace ImageCatalog_2.Services;
public static class PathShellService
{
public 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}\"");
return;
}
if (Directory.Exists(normalizedPath))
{
Process.Start(new ProcessStartInfo
{
FileName = normalizedPath,
UseShellExecute = true
});
return;
}
var containingDirectory = Path.GetDirectoryName(normalizedPath);
if (!string.IsNullOrWhiteSpace(containingDirectory) && Directory.Exists(containingDirectory))
{
Process.Start(new ProcessStartInfo
{
FileName = containingDirectory,
UseShellExecute = true
});
}
}
catch
{
// Ignore failures when opening Explorer.
}
}
}