Compare commits
7 commits
f57dc1edba
...
398cfa310e
| Author | SHA1 | Date | |
|---|---|---|---|
| 398cfa310e | |||
| f3ac1ea920 | |||
| 6e05869b04 | |||
| af74c90ce7 | |||
| 55e8f0face | |||
| 5511817896 | |||
| c261557a29 |
33 changed files with 2843 additions and 553 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -257,3 +257,4 @@ paket-files/
|
||||||
*.sln.iml
|
*.sln.iml
|
||||||
.vscode/settings.json
|
.vscode/settings.json
|
||||||
tmp/**
|
tmp/**
|
||||||
|
TestArtifacts/**
|
||||||
|
|
@ -8,10 +8,10 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.3" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.8" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.8" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.3" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.8" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.3" />
|
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.8" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"settings": {
|
"settings": {
|
||||||
"commentTranslate.hover.enabled": false
|
"commentTranslate.hover.enabled": false,
|
||||||
|
"github.copilot.chat.otel.dbSpanExporter.enabled": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -10,8 +10,8 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="BenchmarkDotNet" Version="0.15.8" />
|
<PackageReference Include="BenchmarkDotNet" Version="0.15.8" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.3" />
|
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.8" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="10.0.3" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="10.0.8" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -9,15 +9,15 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.5.1" />
|
||||||
<PackageReference Include="MSTest.TestAdapter" Version="4.1.0" />
|
<PackageReference Include="MSTest.TestAdapter" Version="4.2.3" />
|
||||||
<PackageReference Include="MSTest.TestFramework" Version="4.1.0" />
|
<PackageReference Include="MSTest.TestFramework" Version="4.2.3" />
|
||||||
<PackageReference Include="NSubstitute" Version="5.3.0" />
|
<PackageReference Include="NSubstitute" Version="5.3.0" />
|
||||||
<PackageReference Include="Shouldly" Version="4.3.0" />
|
<PackageReference Include="Shouldly" Version="4.3.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.3" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.8" />
|
||||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
|
<PackageReference Include="SixLabors.ImageSharp" Version="4.0.0" />
|
||||||
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="2.1.7" />
|
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="3.0.0" />
|
||||||
<PackageReference Include="SixLabors.Fonts" Version="2.1.3" />
|
<PackageReference Include="SixLabors.Fonts" Version="3.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,45 @@ public class DataModelCharacterizationTests
|
||||||
model.DestinationPath.ShouldBe($"C:{System.IO.Path.DirectorySeparatorChar}output{System.IO.Path.DirectorySeparatorChar}");
|
model.DestinationPath.ShouldBe($"C:{System.IO.Path.DirectorySeparatorChar}output{System.IO.Path.DirectorySeparatorChar}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void DestinationPathChange_UpdatesAiCsvFileNameToMatchFinalFolderSegment()
|
||||||
|
{
|
||||||
|
var model = CreateModel();
|
||||||
|
model.CsvOutputPath = @"K:\various\catalogtest\aioutput\test2.csv";
|
||||||
|
|
||||||
|
model.DestinationPath = @"K:\various\catalogtest\Dest\03.KM_8_A\";
|
||||||
|
|
||||||
|
model.CsvOutputPath.ShouldBe(@"K:\various\catalogtest\aioutput\03.KM_8_A.csv");
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public async Task ConfirmAiCsvOverwriteIfNeededAsync_CanCancelWhenCsvAlreadyExists()
|
||||||
|
{
|
||||||
|
using var tempDirectory = new TemporaryDirectory();
|
||||||
|
var csvPath = Path.Combine(tempDirectory.Path, "existing.csv");
|
||||||
|
File.WriteAllText(csvPath, "existing");
|
||||||
|
|
||||||
|
var model = CreateModel();
|
||||||
|
model.CsvOutputPath = csvPath;
|
||||||
|
|
||||||
|
string? requestedTitle = null;
|
||||||
|
string? requestedMessage = null;
|
||||||
|
model.ConfirmAiCsvOverwriteAsync = (title, message) =>
|
||||||
|
{
|
||||||
|
requestedTitle = title;
|
||||||
|
requestedMessage = message;
|
||||||
|
return Task.FromResult(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
var shouldContinue = await model.ConfirmAiCsvOverwriteIfNeededAsync();
|
||||||
|
|
||||||
|
shouldContinue.ShouldBeFalse();
|
||||||
|
requestedTitle.ShouldBe("File CSV gia esistente");
|
||||||
|
requestedMessage.ShouldNotBeNull();
|
||||||
|
requestedMessage.ShouldContain("Vuoi sovrascriverlo?");
|
||||||
|
requestedMessage.ShouldContain(csvPath);
|
||||||
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void AiChildChange_RaisesDataModelPropertyChanged()
|
public void AiChildChange_RaisesDataModelPropertyChanged()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="NSubstitute" Version="5.3.0" />
|
<PackageReference Include="NSubstitute" Version="5.3.0" />
|
||||||
<PackageReference Include="Shouldly" Version="4.3.0" />
|
<PackageReference Include="Shouldly" Version="4.3.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.5" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.8" />
|
||||||
<PackageReference Include="System.Drawing.Common" Version="10.0.3" />
|
<PackageReference Include="System.Drawing.Common" Version="10.0.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
|
||||||
58
MaddoShared.Tests/PickerPreferenceServiceTests.cs
Normal file
58
MaddoShared.Tests/PickerPreferenceServiceTests.cs
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using ImageCatalog;
|
||||||
|
using ImageCatalog_2.Services;
|
||||||
|
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||||
|
using Shouldly;
|
||||||
|
|
||||||
|
namespace MaddoShared.Tests;
|
||||||
|
|
||||||
|
[TestClass]
|
||||||
|
public class PickerPreferenceServiceTests
|
||||||
|
{
|
||||||
|
[TestMethod]
|
||||||
|
public void RememberValue_PersistsExactFilePath()
|
||||||
|
{
|
||||||
|
using var tempDirectory = new TemporaryDirectory();
|
||||||
|
var preferencesFile = Path.Combine(tempDirectory.Path, "userprefs.xml");
|
||||||
|
var service = new PickerPreferenceService(new ParametriSetup(preferencesFile));
|
||||||
|
var settingsFile = Path.Combine(tempDirectory.Path, "nested", "settings.xml");
|
||||||
|
|
||||||
|
service.RememberValue(PickerPreferenceKeys.LastSettingsFile, settingsFile);
|
||||||
|
|
||||||
|
service.GetRememberedValue(PickerPreferenceKeys.LastSettingsFile).ShouldBe(settingsFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ForgetValue_RemovesStoredPreference()
|
||||||
|
{
|
||||||
|
using var tempDirectory = new TemporaryDirectory();
|
||||||
|
var preferencesFile = Path.Combine(tempDirectory.Path, "userprefs.xml");
|
||||||
|
var service = new PickerPreferenceService(new ParametriSetup(preferencesFile));
|
||||||
|
|
||||||
|
service.RememberValue(PickerPreferenceKeys.LastSettingsFile, Path.Combine(tempDirectory.Path, "settings.xml"));
|
||||||
|
|
||||||
|
service.ForgetValue(PickerPreferenceKeys.LastSettingsFile);
|
||||||
|
|
||||||
|
service.GetRememberedValue(PickerPreferenceKeys.LastSettingsFile).ShouldBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TemporaryDirectory : IDisposable
|
||||||
|
{
|
||||||
|
public TemporaryDirectory()
|
||||||
|
{
|
||||||
|
Path = System.IO.Path.Combine(System.IO.Path.GetTempPath(), System.IO.Path.GetRandomFileName());
|
||||||
|
Directory.CreateDirectory(Path);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Path { get; }
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (Directory.Exists(Path))
|
||||||
|
{
|
||||||
|
Directory.Delete(Path, recursive: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,48 @@
|
||||||
namespace MaddoShared.Tests
|
using ImageCatalog_2.Services;
|
||||||
|
using ImageCatalog_2.Models;
|
||||||
|
using Shouldly;
|
||||||
|
|
||||||
|
namespace MaddoShared.Tests;
|
||||||
|
|
||||||
|
[TestClass]
|
||||||
|
public sealed class AiExtractionServiceCsvTests
|
||||||
{
|
{
|
||||||
[TestClass]
|
|
||||||
public sealed class Test1
|
|
||||||
{
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void TestMethod1()
|
public void WriteCsvOutput_UsesLegacyCompatibleHeaderAndFilenameColumn()
|
||||||
{
|
{
|
||||||
|
using var tempDir = new TempDirectory();
|
||||||
|
var csvPath = Path.Combine(tempDir.Path, "ocr.csv");
|
||||||
|
|
||||||
|
AiExtractionService.WriteCsvOutput(
|
||||||
|
csvPath,
|
||||||
|
[
|
||||||
|
new AiResultItem { Path = @"C:\images\IMG_7146.JPG", Text = "43,84,61" },
|
||||||
|
new AiResultItem { Path = @"C:\images\IMG_7207.JPG", Text = "a\"b" }
|
||||||
|
]);
|
||||||
|
|
||||||
|
var lines = File.ReadAllLines(csvPath);
|
||||||
|
|
||||||
|
lines[0].ShouldBe("filename,text");
|
||||||
|
lines[1].ShouldBe("\"IMG_7146.JPG\",\"43,84,61\"");
|
||||||
|
lines[2].ShouldBe("\"IMG_7207.JPG\",\"a\"\"b\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TempDirectory : IDisposable
|
||||||
|
{
|
||||||
|
public TempDirectory()
|
||||||
|
{
|
||||||
|
Path = System.IO.Path.Combine(System.IO.Path.GetTempPath(), System.IO.Path.GetRandomFileName());
|
||||||
|
Directory.CreateDirectory(Path);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Path { get; }
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (Directory.Exists(Path))
|
||||||
|
{
|
||||||
|
Directory.Delete(Path, recursive: true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<OutputType>Library</OutputType>
|
<OutputType>Library</OutputType>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||||
<PlatformTarget>x64</PlatformTarget>
|
<PlatformTarget>x64</PlatformTarget>
|
||||||
<!-- WINDOWS preprocessor symbol mirrors the -windows TFM suffix so #if WINDOWS guards work -->
|
<!-- WINDOWS preprocessor symbol mirrors the -windows TFM suffix so #if WINDOWS guards work -->
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,7 @@
|
||||||
<Setter Property="MinHeight" Value="16" />
|
<Setter Property="MinHeight" Value="16" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<StyleInclude Source="avares://IconPacks.Avalonia/Icons.axaml" />
|
<StyleInclude Source="avares://IconPacks.Avalonia/Icons.axaml"/>
|
||||||
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml"/>
|
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml"/>
|
||||||
</Application.Styles>
|
</Application.Styles>
|
||||||
</Application>
|
</Application>
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
xmlns:views="clr-namespace:ImageCatalog_2.AvaloniaViews"
|
xmlns:views="clr-namespace:ImageCatalog_2.AvaloniaViews"
|
||||||
xmlns:iconPacks="https://github.com/MahApps/IconPacks.Avalonia"
|
xmlns:iconPacks="https://github.com/MahApps/IconPacks.Avalonia"
|
||||||
x:Class="ImageCatalog_2.AvaloniaMainWindow"
|
x:Class="ImageCatalog_2.AvaloniaMainWindow"
|
||||||
|
x:CompileBindings="False"
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
Title="Image Catalog - Avalonia" Height="540" Width="800">
|
Title="Image Catalog - Avalonia" Height="540" Width="800">
|
||||||
|
|
||||||
|
|
@ -106,7 +107,7 @@
|
||||||
</Button>
|
</Button>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<Border Grid.Row="1" BorderThickness="1" Padding="10" MaxWidth="280" MinWidth="0">
|
<Border Grid.Row="1" BorderThickness="1" Padding="10" MaxWidth="280" MinWidth="0">
|
||||||
<Grid RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,*" RowSpacing="8" MinWidth="0">
|
<Grid RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,*" MinWidth="0">
|
||||||
<StackPanel Grid.Row="0">
|
<StackPanel Grid.Row="0">
|
||||||
<Button HorizontalAlignment="Stretch" Margin="0,0,0,4" Command="{Binding LoadSettingsCommand}">
|
<Button HorizontalAlignment="Stretch" Margin="0,0,0,4" Command="{Binding LoadSettingsCommand}">
|
||||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ using Avalonia.Layout;
|
||||||
using Avalonia.Platform.Storage;
|
using Avalonia.Platform.Storage;
|
||||||
using Avalonia.Styling;
|
using Avalonia.Styling;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
|
using ImageCatalog_2.Services;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
|
||||||
|
|
@ -13,52 +15,73 @@ namespace ImageCatalog_2;
|
||||||
public partial class AvaloniaMainWindow : Window
|
public partial class AvaloniaMainWindow : Window
|
||||||
{
|
{
|
||||||
private readonly DataModel _model;
|
private readonly DataModel _model;
|
||||||
|
private readonly PickerPreferenceService _pickerPreferenceService;
|
||||||
private bool _isDarkTheme;
|
private bool _isDarkTheme;
|
||||||
|
private bool _startupSettingsRestoreAttempted;
|
||||||
|
|
||||||
|
public AvaloniaMainWindow()
|
||||||
|
: this(Program.ServiceProvider.GetRequiredService<DataModel>())
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
public AvaloniaMainWindow(DataModel model)
|
public AvaloniaMainWindow(DataModel model)
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
_model = model;
|
_model = model;
|
||||||
|
_pickerPreferenceService = Program.ServiceProvider.GetRequiredService<PickerPreferenceService>();
|
||||||
DataContext = _model;
|
DataContext = _model;
|
||||||
|
|
||||||
Opened += (_, _) => SyncThemeStateFromCurrentTheme();
|
Opened += async (_, _) =>
|
||||||
|
{
|
||||||
|
SyncThemeStateFromCurrentTheme();
|
||||||
|
await TryLoadLastSettingsOnStartupAsync();
|
||||||
|
};
|
||||||
Closing += AvaloniaMainWindow_Closing;
|
Closing += AvaloniaMainWindow_Closing;
|
||||||
|
|
||||||
// Let DataModel marshal callbacks onto Avalonia UI thread.
|
// Let DataModel marshal callbacks onto Avalonia UI thread.
|
||||||
_model.UiInvoker = action => Dispatcher.UIThread.Invoke(action);
|
_model.UiInvoker = action => Dispatcher.UIThread.Invoke(action);
|
||||||
|
_model.ConfirmAiCsvOverwriteAsync = ShowConfirmationDialogAsync;
|
||||||
|
|
||||||
_model.SelectSourceFolderRequested += async (_, _) =>
|
_model.SelectSourceFolderRequested += async (_, _) =>
|
||||||
{
|
{
|
||||||
|
var suggestedStartLocation = await _pickerPreferenceService.TryGetStartFolderAsync(StorageProvider, PickerPreferenceKeys.SourceFolder, _model.SourcePath);
|
||||||
var folders = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
|
var folders = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
|
||||||
{
|
{
|
||||||
Title = "Seleziona cartella sorgente"
|
Title = "Seleziona cartella sorgente",
|
||||||
|
SuggestedStartLocation = suggestedStartLocation
|
||||||
});
|
});
|
||||||
|
|
||||||
if (folders.Count > 0)
|
if (folders.Count > 0)
|
||||||
{
|
{
|
||||||
_model.SourcePath = folders[0].Path.LocalPath + Path.DirectorySeparatorChar;
|
_model.SourcePath = folders[0].Path.LocalPath + Path.DirectorySeparatorChar;
|
||||||
|
_pickerPreferenceService.RememberPath(PickerPreferenceKeys.SourceFolder, _model.SourcePath);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_model.SelectDestinationFolderRequested += async (_, _) =>
|
_model.SelectDestinationFolderRequested += async (_, _) =>
|
||||||
{
|
{
|
||||||
|
var suggestedStartLocation = await _pickerPreferenceService.TryGetStartFolderAsync(StorageProvider, PickerPreferenceKeys.DestinationFolder, _model.DestinationPath);
|
||||||
var folders = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
|
var folders = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
|
||||||
{
|
{
|
||||||
Title = "Seleziona cartella destinazione"
|
Title = "Seleziona cartella destinazione",
|
||||||
|
SuggestedStartLocation = suggestedStartLocation
|
||||||
});
|
});
|
||||||
|
|
||||||
if (folders.Count > 0)
|
if (folders.Count > 0)
|
||||||
{
|
{
|
||||||
_model.DestinationPath = folders[0].Path.LocalPath + Path.DirectorySeparatorChar;
|
_model.DestinationPath = folders[0].Path.LocalPath + Path.DirectorySeparatorChar;
|
||||||
|
_pickerPreferenceService.RememberPath(PickerPreferenceKeys.DestinationFolder, _model.DestinationPath);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_model.SelectLogoFileRequested += async (_, _) =>
|
_model.SelectLogoFileRequested += async (_, _) =>
|
||||||
{
|
{
|
||||||
|
var suggestedStartLocation = await _pickerPreferenceService.TryGetStartFolderAsync(StorageProvider, PickerPreferenceKeys.LogoFile, _model.LogoFile);
|
||||||
var files = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
|
var files = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
|
||||||
{
|
{
|
||||||
Title = "Seleziona logo",
|
Title = "Seleziona logo",
|
||||||
|
SuggestedStartLocation = suggestedStartLocation,
|
||||||
FileTypeFilter =
|
FileTypeFilter =
|
||||||
[
|
[
|
||||||
new FilePickerFileType("Immagini") { Patterns = ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif"] }
|
new FilePickerFileType("Immagini") { Patterns = ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif"] }
|
||||||
|
|
@ -68,63 +91,78 @@ public partial class AvaloniaMainWindow : Window
|
||||||
if (files.Count > 0)
|
if (files.Count > 0)
|
||||||
{
|
{
|
||||||
_model.LogoFile = files[0].Path.LocalPath;
|
_model.LogoFile = files[0].Path.LocalPath;
|
||||||
|
_pickerPreferenceService.RememberPath(PickerPreferenceKeys.LogoFile, _model.LogoFile);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_model.SelectModelsFolderRequested += async (_, _) =>
|
_model.SelectModelsFolderRequested += async (_, _) =>
|
||||||
{
|
{
|
||||||
|
var suggestedStartLocation = await _pickerPreferenceService.TryGetStartFolderAsync(StorageProvider, PickerPreferenceKeys.ModelsFolder, _model.ModelsFolderPath);
|
||||||
var folders = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
|
var folders = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
|
||||||
{
|
{
|
||||||
Title = "Seleziona cartella modelli"
|
Title = "Seleziona cartella modelli",
|
||||||
|
SuggestedStartLocation = suggestedStartLocation
|
||||||
});
|
});
|
||||||
|
|
||||||
if (folders.Count > 0)
|
if (folders.Count > 0)
|
||||||
{
|
{
|
||||||
_model.ModelsFolderPath = folders[0].Path.LocalPath + Path.DirectorySeparatorChar;
|
_model.ModelsFolderPath = folders[0].Path.LocalPath + Path.DirectorySeparatorChar;
|
||||||
|
_pickerPreferenceService.RememberPath(PickerPreferenceKeys.ModelsFolder, _model.ModelsFolderPath);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_model.SelectCsvOutputRequested += async (_, _) =>
|
_model.SelectCsvOutputRequested += async (_, _) =>
|
||||||
{
|
{
|
||||||
|
var suggestedStartLocation = await _pickerPreferenceService.TryGetStartFolderAsync(StorageProvider, PickerPreferenceKeys.CsvOutput, _model.CsvOutputPath);
|
||||||
var file = await StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
|
var file = await StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
|
||||||
{
|
{
|
||||||
Title = "Salva CSV",
|
Title = "Salva CSV",
|
||||||
DefaultExtension = "csv",
|
DefaultExtension = "csv",
|
||||||
FileTypeChoices = [new FilePickerFileType("CSV") { Patterns = ["*.csv"] }]
|
FileTypeChoices = [new FilePickerFileType("CSV") { Patterns = ["*.csv"] }],
|
||||||
|
SuggestedStartLocation = suggestedStartLocation
|
||||||
});
|
});
|
||||||
|
|
||||||
if (file is not null)
|
if (file is not null)
|
||||||
{
|
{
|
||||||
_model.CsvOutputPath = file.Path.LocalPath;
|
_model.CsvOutputPath = file.Path.LocalPath;
|
||||||
|
_pickerPreferenceService.RememberPath(PickerPreferenceKeys.CsvOutput, _model.CsvOutputPath);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_model.SaveSettingsRequested += async (_, _) =>
|
_model.SaveSettingsRequested += async (_, _) =>
|
||||||
{
|
{
|
||||||
|
var suggestedStartLocation = await _pickerPreferenceService.TryGetStartFolderAsync(StorageProvider, PickerPreferenceKeys.SettingsFile);
|
||||||
var file = await StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
|
var file = await StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
|
||||||
{
|
{
|
||||||
Title = "Salva impostazioni",
|
Title = "Salva impostazioni",
|
||||||
DefaultExtension = "xml",
|
DefaultExtension = "xml",
|
||||||
FileTypeChoices = [new FilePickerFileType("Setup") { Patterns = ["*.xml"] }]
|
FileTypeChoices = [new FilePickerFileType("Setup") { Patterns = ["*.xml"] }],
|
||||||
|
SuggestedStartLocation = suggestedStartLocation
|
||||||
});
|
});
|
||||||
|
|
||||||
if (file is not null)
|
if (file is not null)
|
||||||
{
|
{
|
||||||
await _model.SaveSettingsToFileAsync(file.Path.LocalPath);
|
await _model.SaveSettingsToFileAsync(file.Path.LocalPath);
|
||||||
|
_pickerPreferenceService.RememberPath(PickerPreferenceKeys.SettingsFile, file.Path.LocalPath);
|
||||||
|
_pickerPreferenceService.RememberValue(PickerPreferenceKeys.LastSettingsFile, file.Path.LocalPath);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_model.LoadSettingsRequested += async (_, _) =>
|
_model.LoadSettingsRequested += async (_, _) =>
|
||||||
{
|
{
|
||||||
|
var suggestedStartLocation = await _pickerPreferenceService.TryGetStartFolderAsync(StorageProvider, PickerPreferenceKeys.SettingsFile);
|
||||||
var files = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
|
var files = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
|
||||||
{
|
{
|
||||||
Title = "Carica impostazioni",
|
Title = "Carica impostazioni",
|
||||||
FileTypeFilter = [new FilePickerFileType("Setup") { Patterns = ["*.xml"] }]
|
FileTypeFilter = [new FilePickerFileType("Setup") { Patterns = ["*.xml"] }],
|
||||||
|
SuggestedStartLocation = suggestedStartLocation
|
||||||
});
|
});
|
||||||
|
|
||||||
if (files.Count > 0)
|
if (files.Count > 0)
|
||||||
{
|
{
|
||||||
await _model.LoadSettingsFromFileAsync(files[0].Path.LocalPath);
|
await _model.LoadSettingsFromFileAsync(files[0].Path.LocalPath);
|
||||||
|
_pickerPreferenceService.RememberPath(PickerPreferenceKeys.SettingsFile, files[0].Path.LocalPath);
|
||||||
|
_pickerPreferenceService.RememberValue(PickerPreferenceKeys.LastSettingsFile, files[0].Path.LocalPath);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -146,9 +184,41 @@ public partial class AvaloniaMainWindow : Window
|
||||||
|
|
||||||
private bool _isStoppingFaceEncoderForClose;
|
private bool _isStoppingFaceEncoderForClose;
|
||||||
|
|
||||||
|
private async Task TryLoadLastSettingsOnStartupAsync()
|
||||||
|
{
|
||||||
|
if (_startupSettingsRestoreAttempted)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_startupSettingsRestoreAttempted = true;
|
||||||
|
|
||||||
|
var lastSettingsFile = _pickerPreferenceService.GetRememberedValue(PickerPreferenceKeys.LastSettingsFile);
|
||||||
|
if (string.IsNullOrWhiteSpace(lastSettingsFile))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!File.Exists(lastSettingsFile))
|
||||||
|
{
|
||||||
|
_pickerPreferenceService.ForgetValue(PickerPreferenceKeys.LastSettingsFile);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _model.LoadSettingsFromFileAsync(lastSettingsFile);
|
||||||
|
_pickerPreferenceService.RememberPath(PickerPreferenceKeys.SettingsFile, lastSettingsFile);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
await ShowMessageDialogAsync("Impostazioni", $"Impossibile caricare il file impostazioni automatico:\n{ex.GetBaseException().Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async void AvaloniaMainWindow_Closing(object? sender, CancelEventArgs e)
|
private async void AvaloniaMainWindow_Closing(object? sender, CancelEventArgs e)
|
||||||
{
|
{
|
||||||
if (_isStoppingFaceEncoderForClose || !_model.IsFaceEncoderRunning)
|
if (_isStoppingFaceEncoderForClose || (!_model.IsFaceEncoderRunning && !_model.IsFaceMatcherRunning))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -157,9 +227,17 @@ public partial class AvaloniaMainWindow : Window
|
||||||
_isStoppingFaceEncoderForClose = true;
|
_isStoppingFaceEncoderForClose = true;
|
||||||
|
|
||||||
try
|
try
|
||||||
|
{
|
||||||
|
if (_model.IsFaceMatcherRunning)
|
||||||
|
{
|
||||||
|
await _model.StopFaceMatcherAsync("Arresto face matcher in chiusura...", waitForExit: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_model.IsFaceEncoderRunning)
|
||||||
{
|
{
|
||||||
await _model.StopFaceEncoderAsync("Arresto face encoder in chiusura...", waitForExit: true);
|
await _model.StopFaceEncoderAsync("Arresto face encoder in chiusura...", waitForExit: true);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
_isStoppingFaceEncoderForClose = false;
|
_isStoppingFaceEncoderForClose = false;
|
||||||
|
|
@ -206,6 +284,25 @@ public partial class AvaloniaMainWindow : Window
|
||||||
await dialog.ShowDialog(this);
|
await dialog.ShowDialog(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<bool> ShowConfirmationDialogAsync(string title, string message)
|
||||||
|
{
|
||||||
|
var dialog = new Window
|
||||||
|
{
|
||||||
|
Title = title,
|
||||||
|
Width = 520,
|
||||||
|
CanResize = false,
|
||||||
|
WindowStartupLocation = WindowStartupLocation.CenterOwner,
|
||||||
|
SizeToContent = SizeToContent.Height
|
||||||
|
};
|
||||||
|
|
||||||
|
dialog.Content = BuildConfirmationDialogContent(
|
||||||
|
message,
|
||||||
|
() => dialog.Close(true),
|
||||||
|
() => dialog.Close(false));
|
||||||
|
|
||||||
|
return await dialog.ShowDialog<bool>(this);
|
||||||
|
}
|
||||||
|
|
||||||
private static Control BuildMessageDialogContent(string message, Action closeDialog)
|
private static Control BuildMessageDialogContent(string message, Action closeDialog)
|
||||||
{
|
{
|
||||||
var layout = new StackPanel
|
var layout = new StackPanel
|
||||||
|
|
@ -233,4 +330,46 @@ public partial class AvaloniaMainWindow : Window
|
||||||
layout.Children.Add(closeButton);
|
layout.Children.Add(closeButton);
|
||||||
return layout;
|
return layout;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Control BuildConfirmationDialogContent(string message, Action confirmDialog, Action cancelDialog)
|
||||||
|
{
|
||||||
|
var layout = new StackPanel
|
||||||
|
{
|
||||||
|
Margin = new Thickness(16),
|
||||||
|
Spacing = 12
|
||||||
|
};
|
||||||
|
|
||||||
|
layout.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = message,
|
||||||
|
TextWrapping = Avalonia.Media.TextWrapping.Wrap,
|
||||||
|
MaxWidth = 460
|
||||||
|
});
|
||||||
|
|
||||||
|
var buttons = new StackPanel
|
||||||
|
{
|
||||||
|
Orientation = Orientation.Horizontal,
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Right,
|
||||||
|
Spacing = 8
|
||||||
|
};
|
||||||
|
|
||||||
|
var cancelButton = new Button
|
||||||
|
{
|
||||||
|
Content = "Annulla",
|
||||||
|
MinWidth = 96
|
||||||
|
};
|
||||||
|
cancelButton.Click += (_, _) => cancelDialog();
|
||||||
|
|
||||||
|
var confirmButton = new Button
|
||||||
|
{
|
||||||
|
Content = "Sovrascrivi",
|
||||||
|
MinWidth = 96
|
||||||
|
};
|
||||||
|
confirmButton.Click += (_, _) => confirmDialog();
|
||||||
|
|
||||||
|
buttons.Children.Add(cancelButton);
|
||||||
|
buttons.Children.Add(confirmButton);
|
||||||
|
layout.Children.Add(buttons);
|
||||||
|
return layout;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,25 @@
|
||||||
<UserControl xmlns="https://github.com/avaloniaui"
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:avaloniaDataGrid="clr-namespace:Avalonia.Controls;assembly=Avalonia.Controls.DataGrid"
|
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:iconPacks="https://github.com/MahApps/IconPacks.Avalonia"
|
||||||
x:Class="ImageCatalog_2.AvaloniaViews.AiTabView">
|
x:Class="ImageCatalog_2.AvaloniaViews.AiTabView"
|
||||||
<Grid>
|
x:CompileBindings="False">
|
||||||
<Grid.RowDefinitions>
|
<TabControl Margin="4">
|
||||||
<RowDefinition Height="Auto" />
|
<TabItem Header="Esecuzione">
|
||||||
<RowDefinition Height="*" />
|
<Grid RowDefinitions="Auto,*">
|
||||||
</Grid.RowDefinitions>
|
|
||||||
|
|
||||||
<ScrollViewer Grid.Row="0" VerticalScrollBarVisibility="Auto">
|
<ScrollViewer Grid.Row="0" VerticalScrollBarVisibility="Auto">
|
||||||
<StackPanel Margin="4">
|
<StackPanel Margin="4" Spacing="8">
|
||||||
<TextBlock Text="AI / OCR" FontWeight="Bold" />
|
<TextBlock Text="AI / OCR" FontWeight="Bold" />
|
||||||
<StackPanel Orientation="Horizontal" Spacing="12" Margin="0,6,0,0">
|
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="12">
|
||||||
<CheckBox Content="Usa GPU"
|
<CheckBox Content="Usa GPU"
|
||||||
IsChecked="{Binding UseNumberAiGpu, Mode=TwoWay}"
|
IsChecked="{Binding UseNumberAiGpu, Mode=TwoWay}"
|
||||||
IsEnabled="{Binding NumberAiGpuOptionEnabled}" />
|
IsEnabled="{Binding NumberAiGpuOptionEnabled}" />
|
||||||
<CheckBox Content="Includi thumbnail" IsChecked="{Binding IncludeNumberAiThumbnails, Mode=TwoWay}" />
|
<CheckBox Content="Includi thumbnail" IsChecked="{Binding IncludeNumberAiThumbnails, Mode=TwoWay}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<Grid Margin="0,8,0,0" ColumnDefinitions="Auto,Auto,*" ColumnSpacing="8">
|
<Grid ColumnDefinitions="Auto,Auto,*" ColumnSpacing="8">
|
||||||
<TextBlock Grid.Column="0" Text="Carico OCR:" VerticalAlignment="Center" />
|
<TextBlock Grid.Column="0" Text="Carico OCR:" VerticalAlignment="Center" />
|
||||||
<ComboBox Grid.Column="1"
|
<ComboBox Grid.Column="1"
|
||||||
Width="84"
|
Width="84"
|
||||||
|
|
@ -32,38 +32,15 @@
|
||||||
FontWeight="SemiBold" />
|
FontWeight="SemiBold" />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid Margin="0,8,0,0" ColumnDefinitions="Auto,*,Auto" ColumnSpacing="6">
|
<controls:PathPickerField Label="Sorgente:"
|
||||||
<TextBlock Grid.Column="0" Text="Sorgente:" VerticalAlignment="Center" />
|
Text="{Binding DestinationPath, Mode=TwoWay}"
|
||||||
<TextBox Grid.Column="1" Text="{Binding DestinationPath, Mode=OneWay}" IsReadOnly="True" VerticalAlignment="Center" />
|
IsTextReadOnly="True"
|
||||||
<Button Grid.Column="2" Width="72" Click="OpenAiDestinationFolder_Click">
|
PreferenceKey="Picker.DestinationFolder.LastPath"
|
||||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
|
PickerTitle="Seleziona cartella sorgente AI"
|
||||||
<iconPacks:PackIconMaterial Kind="Folder" Width="14" Height="14" />
|
PickerMode="Folder"
|
||||||
<TextBlock Text="Apri" />
|
AppendDirectorySeparator="True" />
|
||||||
</StackPanel>
|
|
||||||
</Button>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<TextBlock Text="Modelli" FontWeight="Bold" Margin="0,8,0,0" />
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left" Spacing="8">
|
||||||
<Grid Margin="0,4,0,0" ColumnDefinitions="Auto,*,Auto,Auto">
|
|
||||||
<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" Margin="6,0,0,0" 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" Margin="6,0,0,0" 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>
|
|
||||||
|
|
||||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left" Margin="0,8,0,0" Spacing="8">
|
|
||||||
<Button Command="{Binding StartAiCommand}" Width="132">
|
<Button Command="{Binding StartAiCommand}" Width="132">
|
||||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
|
||||||
<iconPacks:PackIconMaterial Kind="PlayCircle" Width="16" Height="16" Foreground="#2E7D32" />
|
<iconPacks:PackIconMaterial Kind="PlayCircle" Width="16" Height="16" Foreground="#2E7D32" />
|
||||||
|
|
@ -78,37 +55,48 @@
|
||||||
</Button>
|
</Button>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<TextBlock Text="Output CSV" FontWeight="Bold" Margin="0,8,0,0" />
|
<TextBlock Text="Output CSV" FontWeight="Bold" />
|
||||||
<Grid Margin="0,4,0,0" ColumnDefinitions="Auto,*,Auto,Auto">
|
<controls:PathPickerField Label="Percorso CSV:"
|
||||||
<TextBlock Text="Percorso CSV:" VerticalAlignment="Center" Margin="0,0,8,0" Grid.Column="0" />
|
Text="{Binding CsvOutputPath, Mode=TwoWay}"
|
||||||
<TextBox Text="{Binding CsvOutputPath, Mode=TwoWay}" Grid.Column="1" VerticalAlignment="Center" />
|
PreferenceKey="Picker.CsvOutput.LastPath"
|
||||||
<Button Width="104" Margin="6,0,0,0" Command="{Binding SelectCsvOutputCommand}"
|
PickerTitle="Salva CSV"
|
||||||
Grid.Column="2">
|
PickerMode="SaveFile"
|
||||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
|
FileTypeName="CSV"
|
||||||
<iconPacks:PackIconMaterial Kind="FileOutline" Width="14" Height="14" />
|
FilePatterns="*.csv"
|
||||||
<TextBlock Text="Scegli..." />
|
DefaultExtension="csv" />
|
||||||
</StackPanel>
|
|
||||||
</Button>
|
|
||||||
<Button Width="72" Margin="6,0,0,0" 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>
|
|
||||||
|
|
||||||
<TextBlock Text="Anteprima risultati" FontWeight="Bold" Margin="0,8,0,0" />
|
<TextBlock Text="Anteprima risultati" FontWeight="Bold" />
|
||||||
<ProgressBar Minimum="0" Maximum="100" Value="{Binding AiProgress}" Height="16" Margin="0,4,0,4" />
|
<ProgressBar Minimum="0" Maximum="100" Value="{Binding AiProgress}" Height="16" Margin="0,0,0,4" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
|
|
||||||
<avaloniaDataGrid:DataGrid Grid.Row="1" ItemsSource="{Binding PreviewResults}" IsReadOnly="True"
|
<avaloniaDataGrid:DataGrid Grid.Row="1"
|
||||||
AutoGenerateColumns="False" Margin="4,4,4,4" CanUserResizeColumns="True" VerticalAlignment="Stretch">
|
ItemsSource="{Binding PreviewResults}"
|
||||||
|
IsReadOnly="True"
|
||||||
|
AutoGenerateColumns="False"
|
||||||
|
Margin="4,4,4,4"
|
||||||
|
CanUserResizeColumns="True"
|
||||||
|
VerticalAlignment="Stretch">
|
||||||
<avaloniaDataGrid:DataGrid.Columns>
|
<avaloniaDataGrid:DataGrid.Columns>
|
||||||
<avaloniaDataGrid:DataGridTextColumn Header="Path" Binding="{Binding Path}" Width="*" />
|
<avaloniaDataGrid:DataGridTextColumn Header="Path" Binding="{Binding Path}" Width="*" />
|
||||||
<avaloniaDataGrid:DataGridTextColumn Header="Text" Binding="{Binding Text}" Width="2*" />
|
<avaloniaDataGrid:DataGridTextColumn Header="Text" Binding="{Binding Text}" Width="2*" />
|
||||||
</avaloniaDataGrid:DataGrid.Columns>
|
</avaloniaDataGrid:DataGrid.Columns>
|
||||||
</avaloniaDataGrid:DataGrid>
|
</avaloniaDataGrid:DataGrid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
<TabItem Header="Impostazioni">
|
||||||
|
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||||
|
<StackPanel Margin="8" Spacing="8">
|
||||||
|
<TextBlock Text="Modelli" FontWeight="Bold" />
|
||||||
|
<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>
|
||||||
|
</TabControl>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,4 @@
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Interactivity;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.IO;
|
|
||||||
|
|
||||||
namespace ImageCatalog_2.AvaloniaViews;
|
namespace ImageCatalog_2.AvaloniaViews;
|
||||||
|
|
||||||
public partial class AiTabView : Avalonia.Controls.UserControl
|
public partial class AiTabView : Avalonia.Controls.UserControl
|
||||||
|
|
@ -11,56 +7,4 @@ public partial class AiTabView : Avalonia.Controls.UserControl
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
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 OpenAiDestinationFolder_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.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,86 +1,66 @@
|
||||||
<UserControl xmlns="https://github.com/avaloniaui"
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
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:iconPacks="https://github.com/MahApps/IconPacks.Avalonia"
|
||||||
x:Class="ImageCatalog_2.AvaloniaViews.FaceAiTabView">
|
xmlns:converters="clr-namespace:ImageCatalog_2.Converters"
|
||||||
|
x:Class="ImageCatalog_2.AvaloniaViews.FaceAiTabView"
|
||||||
|
x:CompileBindings="False">
|
||||||
|
<UserControl.Resources>
|
||||||
|
<converters:FilePathToBitmapConverter x:Key="FilePathToBitmapConverter" />
|
||||||
|
</UserControl.Resources>
|
||||||
|
|
||||||
|
<TabControl Margin="4">
|
||||||
|
<TabItem Header="Encoder">
|
||||||
|
<TabControl Margin="0,4,0,0">
|
||||||
|
<TabItem Header="Esecuzione">
|
||||||
<ScrollViewer>
|
<ScrollViewer>
|
||||||
<StackPanel Margin="4" Spacing="6">
|
<StackPanel Margin="4" Spacing="6">
|
||||||
<TextBlock Text="Face Recognition Encoder" FontWeight="Bold" />
|
<TextBlock Text="Face Recognition Encoder" FontWeight="Bold" />
|
||||||
<TextBlock Text="Esegue il face encoder usando la cartella Destinazione corrente come --images e genera automaticamente file .pkl e log nella cartella di output scelta."
|
<TextBlock Text="Esegue il face encoder usando la cartella Destinazione corrente come --images e genera automaticamente file .pkl e log nella cartella di output scelta."
|
||||||
TextWrapping="Wrap" Opacity="0.8" />
|
TextWrapping="Wrap" Opacity="0.8" />
|
||||||
|
|
||||||
<TextBlock Text="Cartella Face Encoder" FontWeight="Bold" Margin="0,4,0,0" />
|
<WrapPanel>
|
||||||
<Grid ColumnDefinitions="Auto,*,Auto,Auto" ColumnSpacing="6">
|
<CheckBox Content="Ricorsivo (--recursive)" IsChecked="{Binding FaceRecursive, Mode=TwoWay}" Margin="0,0,12,6" />
|
||||||
<TextBlock Grid.Column="0" Text="Percorso:" VerticalAlignment="Center" />
|
<CheckBox Content="Includi thumbnail (--include-tn)" IsChecked="{Binding FaceIncludeThumbnails, Mode=TwoWay}" Margin="0,0,12,6" />
|
||||||
<TextBox Grid.Column="1" Name="FaceExecutablePathTextBox" Text="{Binding FaceExecutablePath, Mode=TwoWay}" Watermark="C:\tools\Face_Recognition_Windows" />
|
<CheckBox Content="Upsample (--upsample)" IsChecked="{Binding FaceUpsample, Mode=TwoWay}" Margin="0,0,12,6" />
|
||||||
<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>
|
|
||||||
|
|
||||||
<StackPanel Orientation="Horizontal" Spacing="12">
|
|
||||||
<CheckBox Content="Ricorsivo (--recursive)" IsChecked="{Binding FaceRecursive, Mode=TwoWay}" />
|
|
||||||
<CheckBox Content="Includi thumbnail (--include-tn)" IsChecked="{Binding FaceIncludeThumbnails, Mode=TwoWay}" />
|
|
||||||
<CheckBox Content="Upsample (--upsample)" IsChecked="{Binding FaceUpsample, Mode=TwoWay}" />
|
|
||||||
<CheckBox Content="Usa GPU"
|
<CheckBox Content="Usa GPU"
|
||||||
IsChecked="{Binding UseFaceGpu, Mode=TwoWay}"
|
IsChecked="{Binding UseFaceGpu, Mode=TwoWay}"
|
||||||
IsEnabled="{Binding FaceGpuOptionEnabled}" />
|
IsEnabled="{Binding FaceGpuOptionEnabled}"
|
||||||
</StackPanel>
|
Margin="0,0,12,6" />
|
||||||
<TextBlock Text="Seleziona la cartella base di Face Recognition Windows: l'app sceglie automaticamente face_encoder_cpu.exe o face_encoder_gpu.exe in base al checkbox GPU."
|
</WrapPanel>
|
||||||
TextWrapping="Wrap"
|
|
||||||
Opacity="0.75" />
|
|
||||||
|
|
||||||
<Grid ColumnDefinitions="Auto,120,Auto,120,*" ColumnSpacing="6">
|
<Grid ColumnDefinitions="Auto,120,Auto,120,*" ColumnSpacing="6">
|
||||||
<TextBlock Grid.Column="0" Text="Parallelismo:" VerticalAlignment="Center" />
|
<TextBlock Grid.Column="0" Text="Parallelismo:" VerticalAlignment="Center" />
|
||||||
<ComboBox Grid.Column="1" ItemsSource="{Binding FaceParallelismOptions}" SelectedItem="{Binding FaceParallelism, Mode=TwoWay}" />
|
<ComboBox Grid.Column="1" ItemsSource="{Binding FaceParallelismOptions}" SelectedItem="{Binding FaceParallelism, Mode=TwoWay}" />
|
||||||
<TextBlock Grid.Column="2" Text="Min size:" VerticalAlignment="Center" />
|
<TextBlock Grid.Column="2" Text="Min size:" VerticalAlignment="Center" />
|
||||||
<TextBox Grid.Column="3" Text="{Binding FaceMinSize, Mode=TwoWay}" Watermark="35" />
|
<TextBox Grid.Column="3" Text="{Binding FaceMinSize, Mode=TwoWay}" Watermark="35" />
|
||||||
<TextBlock Grid.Column="4" Text="Usa --multicore in CPU e --multiprocess in GPU." VerticalAlignment="Center" Opacity="0.75" />
|
<TextBlock Grid.Column="4" Text="Usa --multicore in CPU e --multiprocess in GPU." VerticalAlignment="Center" TextWrapping="Wrap" Opacity="0.75" />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid ColumnDefinitions="Auto,*,Auto,Auto" ColumnSpacing="6">
|
<controls:PathPickerField Label="Sorgente:"
|
||||||
<TextBlock Grid.Column="0" Text="Sorgente:" VerticalAlignment="Center" />
|
Text="{Binding DestinationPath, Mode=TwoWay}"
|
||||||
<TextBox Grid.Column="1" Name="FaceDestinationPathTextBox" Text="{Binding DestinationPath, Mode=OneWay}" IsReadOnly="True" />
|
IsTextReadOnly="True"
|
||||||
<Button Grid.Column="3" Name="FaceOpenDestinationButton" Click="OpenFaceDestinationFolder_Click" Width="72" Margin="6,0,0,0">
|
PreferenceKey="Picker.DestinationFolder.LastPath"
|
||||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
|
PickerTitle="Seleziona cartella sorgente Face Encoder"
|
||||||
<iconPacks:PackIconMaterial Kind="Folder" Width="14" Height="14" />
|
PickerMode="Folder"
|
||||||
<TextBlock Text="Apri" />
|
AppendDirectorySeparator="True" />
|
||||||
</StackPanel>
|
|
||||||
</Button>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<TextBlock Text="Output encodings" FontWeight="Bold" Margin="0,4,0,0" />
|
<TextBlock Text="Output encodings" FontWeight="Bold" Margin="0,4,0,0" />
|
||||||
<Grid ColumnDefinitions="Auto,*,Auto,Auto" ColumnSpacing="6">
|
<controls:PathPickerField Label="Cartella out:"
|
||||||
<TextBlock Grid.Column="0" Text="Cartella out:" VerticalAlignment="Center" />
|
Text="{Binding FaceOutputFolderPath, Mode=TwoWay}"
|
||||||
<TextBox Grid.Column="1" Name="FaceOutputFolderTextBox" Text="{Binding FaceOutputFolderPath, Mode=TwoWay}" Watermark="C:\output\face_encoder" />
|
Watermark="C:\output\face_encoder"
|
||||||
<Button Grid.Column="2" Name="FaceSelectOutputButton" Click="SelectFaceOutputFolder_Click" Width="104" Margin="6,0,0,0">
|
PreferenceKey="Picker.FaceOutputFolder.LastPath"
|
||||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
|
PickerTitle="Seleziona la cartella output per encodings e log"
|
||||||
<iconPacks:PackIconMaterial Kind="FolderOutline" Width="14" Height="14" />
|
PickerMode="Folder" />
|
||||||
<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>
|
|
||||||
<TextBlock Text="I file vengono creati come face_encodings_yyyyMMdd_HHmmss_nomecartella.pkl e encoder_log_yyyyMMdd_HHmmss_nomecartella.txt."
|
<TextBlock Text="I file vengono creati come face_encodings_yyyyMMdd_HHmmss_nomecartella.pkl e encoder_log_yyyyMMdd_HHmmss_nomecartella.txt."
|
||||||
TextWrapping="Wrap"
|
TextWrapping="Wrap"
|
||||||
Opacity="0.75" />
|
Opacity="0.75" />
|
||||||
|
|
||||||
<StackPanel Orientation="Horizontal" Spacing="6" Margin="0,6,0,0">
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left" Spacing="8" Margin="0,6,0,0">
|
||||||
<Button Name="FaceRunButton" Command="{Binding StartFaceEncoderCommand}">
|
<Button Name="FaceRunButton" Command="{Binding StartFaceEncoderCommand}" Width="176">
|
||||||
<StackPanel Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6" VerticalAlignment="Center">
|
||||||
|
<iconPacks:PackIconMaterial Kind="PlayCircle" Width="16" Height="16" Foreground="#2E7D32" />
|
||||||
<ProgressBar Width="18"
|
<ProgressBar Width="18"
|
||||||
Height="18"
|
Height="18"
|
||||||
IsIndeterminate="True"
|
IsIndeterminate="True"
|
||||||
|
|
@ -90,8 +70,13 @@
|
||||||
<TextBlock Text="Esegui Face Encoder" VerticalAlignment="Center" />
|
<TextBlock Text="Esegui Face Encoder" VerticalAlignment="Center" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Button>
|
</Button>
|
||||||
<Button Content="Stop" Command="{Binding StopFaceEncoderCommand}" />
|
<Button Command="{Binding StopFaceEncoderCommand}" Width="120">
|
||||||
<TextBlock VerticalAlignment="Center" Text="{Binding FaceStatusMessage}" />
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
|
||||||
|
<iconPacks:PackIconMaterial Kind="Cancel" Width="16" Height="16" Foreground="#C62828" />
|
||||||
|
<TextBlock Text="Stop" />
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
<TextBlock VerticalAlignment="Center" Text="{Binding FaceStatusMessage}" TextWrapping="Wrap" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<TextBlock Text="Output comando" FontWeight="Bold" Margin="0,6,0,0" />
|
<TextBlock Text="Output comando" FontWeight="Bold" Margin="0,6,0,0" />
|
||||||
|
|
@ -106,4 +91,201 @@
|
||||||
ScrollViewer.HorizontalScrollBarVisibility="Auto" />
|
ScrollViewer.HorizontalScrollBarVisibility="Auto" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
<TabItem Header="Impostazioni">
|
||||||
|
<ScrollViewer>
|
||||||
|
<StackPanel Margin="4" Spacing="8">
|
||||||
|
<TextBlock Text="Cartella Face Encoder" FontWeight="Bold" />
|
||||||
|
<TextBlock Text="Seleziona la cartella base di face recognition"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
Opacity="0.8" />
|
||||||
|
<TextBlock Text="L'app sceglie automaticamente face_encoder_cpu.exe o face_encoder_gpu.exe in base al checkbox GPU."
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
Opacity="0.75" />
|
||||||
|
|
||||||
|
<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>
|
||||||
|
</TabControl>
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
<TabItem Header="Matcher Test">
|
||||||
|
<ScrollViewer>
|
||||||
|
<StackPanel Margin="4" Spacing="6">
|
||||||
|
<TextBlock Text="Face Matcher Test" FontWeight="Bold" />
|
||||||
|
<TextBlock Text="Esegue face_matcher.exe su un'immagine scelta a runtime e confronta i risultati con gli encodings .pkl. I match vengono poi risolti per nome file dentro la cartella Destinazione corrente per facilitare il debug visivo."
|
||||||
|
TextWrapping="Wrap" Opacity="0.8" />
|
||||||
|
|
||||||
|
<TextBlock Text="Eseguibile matcher" FontWeight="Bold" Margin="0,4,0,0" />
|
||||||
|
<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" />
|
||||||
|
<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" />
|
||||||
|
<StackPanel Spacing="4">
|
||||||
|
<TextBlock Text="Preview immagine selezionata" FontWeight="SemiBold" />
|
||||||
|
<Border BorderThickness="1" BorderBrush="#808080" Padding="4" HorizontalAlignment="Left">
|
||||||
|
<Image Width="184"
|
||||||
|
Height="120"
|
||||||
|
Stretch="UniformToFill"
|
||||||
|
Source="{Binding FaceMatcherSelectedImagePath, Converter={StaticResource FilePathToBitmapConverter}}" />
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<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" />
|
||||||
|
<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" />
|
||||||
|
|
||||||
|
<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" />
|
||||||
|
|
||||||
|
<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" />
|
||||||
|
<TextBox Grid.Column="1" Text="{Binding FaceMatcherTolerance, Mode=TwoWay}" Watermark="0.50" />
|
||||||
|
<TextBlock Grid.Column="2" Text="Range utile: 0.35 - 0.75. Più bassa = match più restrittivo, più alta = più permissivo." VerticalAlignment="Center" TextWrapping="Wrap" Opacity="0.75" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left" Spacing="8" Margin="0,6,0,0">
|
||||||
|
<Button Command="{Binding StartFaceMatcherCommand}" Width="176">
|
||||||
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6" VerticalAlignment="Center">
|
||||||
|
<iconPacks:PackIconMaterial Kind="PlayCircle" Width="16" Height="16" Foreground="#2E7D32" />
|
||||||
|
<ProgressBar Width="18"
|
||||||
|
Height="18"
|
||||||
|
IsIndeterminate="True"
|
||||||
|
IsVisible="{Binding IsFaceMatcherRunning}"
|
||||||
|
ShowProgressText="False"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
<TextBlock Text="Avvia Face Matcher" VerticalAlignment="Center" />
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
<Button Command="{Binding StopFaceMatcherCommand}" Width="120">
|
||||||
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
|
||||||
|
<iconPacks:PackIconMaterial Kind="Cancel" Width="16" Height="16" Foreground="#C62828" />
|
||||||
|
<TextBlock Text="Stop" />
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
<TextBlock VerticalAlignment="Center" Text="{Binding FaceMatcherStatusMessage}" TextWrapping="Wrap" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<TextBlock Text="Output comando" FontWeight="Bold" Margin="0,6,0,0" />
|
||||||
|
<TextBox Name="FaceMatcherOutputTextBox"
|
||||||
|
Text="{Binding FaceMatcherCommandOutput}"
|
||||||
|
IsReadOnly="True"
|
||||||
|
AcceptsReturn="True"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
FontFamily="Cascadia Mono, Consolas, monospace"
|
||||||
|
Height="220"
|
||||||
|
ScrollViewer.VerticalScrollBarVisibility="Auto"
|
||||||
|
ScrollViewer.HorizontalScrollBarVisibility="Auto" />
|
||||||
|
|
||||||
|
<TextBlock Text="Risultati" FontWeight="Bold" Margin="0,8,0,0" />
|
||||||
|
<avaloniaDataGrid:DataGrid ItemsSource="{Binding FaceMatcherResults}"
|
||||||
|
IsReadOnly="True"
|
||||||
|
AutoGenerateColumns="False"
|
||||||
|
CanUserResizeColumns="True"
|
||||||
|
DoubleTapped="FaceMatcherResults_DoubleTapped"
|
||||||
|
MinHeight="220">
|
||||||
|
<avaloniaDataGrid:DataGrid.Columns>
|
||||||
|
<avaloniaDataGrid:DataGridTextColumn Header="File" Binding="{Binding PhotoId}" Width="2*" />
|
||||||
|
<avaloniaDataGrid:DataGridTextColumn Header="Score" Binding="{Binding ScoreDisplay}" Width="Auto" />
|
||||||
|
<avaloniaDataGrid:DataGridTextColumn Header="Trovato in destinazione" Binding="{Binding ResolvedImagePath}" Width="3*" />
|
||||||
|
<avaloniaDataGrid:DataGridTemplateColumn Header="Candidati" Width="Auto">
|
||||||
|
<avaloniaDataGrid:DataGridTemplateColumn.CellTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<TextBlock Text="{Binding CandidateCount}" Margin="10,0,0,0" />
|
||||||
|
</DataTemplate>
|
||||||
|
</avaloniaDataGrid:DataGridTemplateColumn.CellTemplate>
|
||||||
|
</avaloniaDataGrid:DataGridTemplateColumn>
|
||||||
|
<avaloniaDataGrid:DataGridTextColumn Header="Debug" Binding="{Binding DebugSummary}" Width="4*" />
|
||||||
|
</avaloniaDataGrid:DataGrid.Columns>
|
||||||
|
</avaloniaDataGrid:DataGrid>
|
||||||
|
|
||||||
|
<TextBlock Text="Gallery match risolti" FontWeight="Bold" Margin="0,8,0,0" />
|
||||||
|
<ItemsControl ItemsSource="{Binding FaceMatcherResults}">
|
||||||
|
<ItemsControl.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate>
|
||||||
|
<WrapPanel />
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</ItemsControl.ItemsPanel>
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<Button Click="OpenFaceMatcherPreview_Click"
|
||||||
|
Tag="{Binding}"
|
||||||
|
Width="184"
|
||||||
|
Margin="0,0,8,8"
|
||||||
|
HorizontalContentAlignment="Stretch"
|
||||||
|
VerticalContentAlignment="Stretch">
|
||||||
|
<StackPanel Spacing="4">
|
||||||
|
<Border BorderThickness="1" BorderBrush="#808080" Padding="4">
|
||||||
|
<Image Width="168"
|
||||||
|
Height="112"
|
||||||
|
Stretch="UniformToFill"
|
||||||
|
Source="{Binding ResolvedImagePath, Converter={StaticResource FilePathToBitmapConverter}}" />
|
||||||
|
</Border>
|
||||||
|
<TextBlock Text="{Binding PhotoId}" FontWeight="SemiBold" TextWrapping="Wrap" />
|
||||||
|
<TextBlock Text="{Binding ScoreDisplay}" Opacity="0.8" />
|
||||||
|
<TextBlock Text="{Binding ResolvedImagePath}" TextWrapping="Wrap" MaxHeight="36" Opacity="0.8" />
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
</TabItem>
|
||||||
|
</TabControl>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,17 @@
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
using Avalonia.Platform.Storage;
|
using Avalonia.Input;
|
||||||
|
using Avalonia.Layout;
|
||||||
|
using Avalonia.Media;
|
||||||
|
using Avalonia.Media.Imaging;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
|
using ImageCatalog_2.Models;
|
||||||
|
using ImageCatalog_2.Services;
|
||||||
using System;
|
using System;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace ImageCatalog_2.AvaloniaViews;
|
namespace ImageCatalog_2.AvaloniaViews;
|
||||||
|
|
||||||
|
|
@ -35,12 +41,21 @@ public partial class FaceAiTabView : Avalonia.Controls.UserControl
|
||||||
|
|
||||||
private void OnFaceAiPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
private void OnFaceAiPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||||
{
|
{
|
||||||
if (!string.Equals(e.PropertyName, nameof(DataModel.FaceCommandOutput), StringComparison.Ordinal))
|
if (string.Equals(e.PropertyName, nameof(DataModel.FaceCommandOutput), StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
|
ScrollOutputTextBoxToEnd("FaceOutputTextBox");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var outputBox = this.FindControl<Avalonia.Controls.TextBox>("FaceOutputTextBox");
|
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);
|
||||||
if (outputBox is null)
|
if (outputBox is null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
|
|
@ -53,172 +68,258 @@ public partial class FaceAiTabView : Avalonia.Controls.UserControl
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void SelectFaceExecutable_Click(object? sender, RoutedEventArgs e)
|
private async void OpenFaceMatcherPreview_Click(object? sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
var executableBox = this.FindControl<Avalonia.Controls.TextBox>("FaceExecutablePathTextBox");
|
if (sender is not Button { Tag: FaceMatcherResultItem item })
|
||||||
if (executableBox is null)
|
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var topLevel = TopLevel.GetTopLevel(this);
|
await OpenFaceMatcherPreviewAsync(item);
|
||||||
var storageProvider = topLevel?.StorageProvider;
|
}
|
||||||
if (storageProvider is null)
|
|
||||||
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var folders = await storageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
|
await OpenFaceMatcherPreviewAsync(item);
|
||||||
{
|
|
||||||
Title = "Seleziona la cartella Face Recognition Windows"
|
|
||||||
});
|
|
||||||
|
|
||||||
if (folders.Count > 0)
|
|
||||||
{
|
|
||||||
executableBox.Text = folders[0].Path.LocalPath;
|
|
||||||
if (DataContext is DataModel model)
|
|
||||||
{
|
|
||||||
model.FaceExecutablePath = executableBox.Text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void SelectFaceOutputFolder_Click(object? sender, RoutedEventArgs e)
|
private Window BuildFaceMatcherPreviewDialog(FaceMatcherResultItem item)
|
||||||
{
|
{
|
||||||
var outputBox = this.FindControl<Avalonia.Controls.TextBox>("FaceOutputFolderTextBox");
|
var dialog = new Window
|
||||||
if (outputBox is null)
|
|
||||||
{
|
{
|
||||||
return;
|
Title = $"Preview match: {item.PhotoId}",
|
||||||
}
|
Width = 1180,
|
||||||
|
Height = 900,
|
||||||
|
WindowStartupLocation = WindowStartupLocation.CenterOwner
|
||||||
|
};
|
||||||
|
|
||||||
var topLevel = TopLevel.GetTopLevel(this);
|
Bitmap? bitmap = null;
|
||||||
var storageProvider = topLevel?.StorageProvider;
|
var dimensionText = "n/d";
|
||||||
if (storageProvider is null)
|
if (!string.IsNullOrWhiteSpace(item.ResolvedImagePath) && File.Exists(item.ResolvedImagePath))
|
||||||
{
|
{
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var folders = await storageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
|
|
||||||
{
|
|
||||||
Title = "Seleziona la cartella output per encodings e log"
|
|
||||||
});
|
|
||||||
|
|
||||||
if (folders.Count > 0)
|
|
||||||
{
|
|
||||||
outputBox.Text = folders[0].Path.LocalPath;
|
|
||||||
if (DataContext is DataModel model)
|
|
||||||
{
|
|
||||||
model.FaceOutputFolderPath = outputBox.Text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OpenFaceExecutableFolder_Click(object? sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
var executableBox = this.FindControl<Avalonia.Controls.TextBox>("FaceExecutablePathTextBox");
|
|
||||||
if (executableBox is null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var path = executableBox.Text?.Trim();
|
|
||||||
if (string.IsNullOrWhiteSpace(path))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Directory.Exists(path))
|
|
||||||
{
|
|
||||||
OpenInExplorer(path);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (File.Exists(path))
|
|
||||||
{
|
|
||||||
OpenInExplorer(path);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var directory = Path.GetDirectoryName(path);
|
|
||||||
OpenInExplorer(string.IsNullOrWhiteSpace(directory) ? path : directory);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OpenFaceOutputFolder_Click(object? sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
var outputBox = this.FindControl<Avalonia.Controls.TextBox>("FaceOutputFolderTextBox");
|
|
||||||
if (outputBox is null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var outputPath = outputBox.Text?.Trim();
|
|
||||||
if (string.IsNullOrWhiteSpace(outputPath))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Directory.Exists(outputPath))
|
|
||||||
{
|
|
||||||
OpenInExplorer(outputPath);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (File.Exists(outputPath))
|
|
||||||
{
|
|
||||||
OpenInExplorer(outputPath);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var directory = Path.GetDirectoryName(outputPath);
|
|
||||||
OpenInExplorer(string.IsNullOrWhiteSpace(directory) ? outputPath : directory);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OpenFaceDestinationFolder_Click(object? sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
var destBox = this.FindControl<Avalonia.Controls.TextBox>("FaceDestinationPathTextBox");
|
|
||||||
string? path = destBox?.Text?.Trim();
|
|
||||||
if (string.IsNullOrWhiteSpace(path) && 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 static void OpenInExplorer(string? path)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(path))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var normalizedPath = path.Trim().Trim('"');
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (File.Exists(normalizedPath))
|
bitmap = new Bitmap(item.ResolvedImagePath);
|
||||||
{
|
dimensionText = $"{bitmap.PixelSize.Width} x {bitmap.PixelSize.Height}px";
|
||||||
Process.Start("explorer.exe", $"/select,\"{normalizedPath}\"");
|
|
||||||
}
|
|
||||||
else if (Directory.Exists(normalizedPath))
|
|
||||||
{
|
|
||||||
Process.Start(new ProcessStartInfo { FileName = normalizedPath, UseShellExecute = true });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
// Ignore failures when opening Explorer.
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,43 +1,24 @@
|
||||||
<UserControl xmlns="https://github.com/avaloniaui"
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
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">
|
x:Class="ImageCatalog_2.AvaloniaViews.GeneralTabView">
|
||||||
<ScrollViewer>
|
<ScrollViewer>
|
||||||
<StackPanel Margin="4" Spacing="8">
|
<StackPanel Margin="4" Spacing="8">
|
||||||
<TextBlock Text="Percorsi" FontWeight="Bold" />
|
<TextBlock Text="Percorsi" FontWeight="Bold" />
|
||||||
<StackPanel Margin="0,2,0,0" Spacing="6">
|
<StackPanel Margin="0,2,0,0" Spacing="6">
|
||||||
<Grid Margin="0,0,0,2" ColumnDefinitions="Auto,*,Auto,Auto" ColumnSpacing="6">
|
<controls:PathPickerField Margin="0,0,0,2"
|
||||||
<TextBlock Text="Sorgente:" VerticalAlignment="Center" Margin="0,0,8,0" Grid.Column="0" />
|
Label="Sorgente:"
|
||||||
<TextBox Text="{Binding SourcePath, Mode=TwoWay}" Grid.Column="1" VerticalAlignment="Center" />
|
Text="{Binding SourcePath, Mode=TwoWay}"
|
||||||
<Button Width="104" Command="{Binding SelectSourceFolderCommand}" Grid.Column="2">
|
PreferenceKey="Picker.SourceFolder.LastPath"
|
||||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
|
PickerTitle="Seleziona cartella sorgente"
|
||||||
<iconPacks:PackIconMaterial Kind="FolderOpenOutline" Width="14" Height="14" />
|
PickerMode="Folder"
|
||||||
<TextBlock Text="Scegli..." />
|
AppendDirectorySeparator="True" />
|
||||||
</StackPanel>
|
<controls:PathPickerField Label="Destinazione:"
|
||||||
</Button>
|
Text="{Binding DestinationPath, Mode=TwoWay}"
|
||||||
<Button Width="72" Grid.Column="3" Click="OpenSourceFolder_Click">
|
PreferenceKey="Picker.DestinationFolder.LastPath"
|
||||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
|
PickerTitle="Seleziona cartella destinazione"
|
||||||
<iconPacks:PackIconMaterial Kind="Folder" Width="14" Height="14" />
|
PickerMode="Folder"
|
||||||
<TextBlock Text="Apri" />
|
AppendDirectorySeparator="True" />
|
||||||
</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>
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<Grid ColumnDefinitions="*,*" ColumnSpacing="24" Margin="0,4,0,0">
|
<Grid ColumnDefinitions="*,*" ColumnSpacing="24" Margin="0,4,0,0">
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,4 @@
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Interactivity;
|
|
||||||
using System;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.IO;
|
|
||||||
|
|
||||||
namespace ImageCatalog_2.AvaloniaViews;
|
namespace ImageCatalog_2.AvaloniaViews;
|
||||||
|
|
||||||
public partial class GeneralTabView : Avalonia.Controls.UserControl
|
public partial class GeneralTabView : Avalonia.Controls.UserControl
|
||||||
|
|
@ -12,45 +7,4 @@ public partial class GeneralTabView : Avalonia.Controls.UserControl
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
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.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
<TextBlock Text="Flusso: login admin, creazione gara, creazione punti foto, upload file processati da cartella destinazione locale, indicizzazione punti foto."
|
<TextBlock Text="Flusso: login admin, creazione gara, creazione punti foto, upload file processati da cartella destinazione locale, indicizzazione punti foto."
|
||||||
TextWrapping="Wrap" Opacity="0.8" />
|
TextWrapping="Wrap" Opacity="0.8" />
|
||||||
|
|
||||||
<Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto,Auto" ColumnSpacing="6" RowSpacing="6">
|
<Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto,Auto" ColumnSpacing="6">
|
||||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="Login:" VerticalAlignment="Center" />
|
<TextBlock Grid.Row="0" Grid.Column="0" Text="Login:" VerticalAlignment="Center" />
|
||||||
<TextBox Grid.Row="0" Grid.Column="1" Name="ApiLoginTextBox" Text="{Binding ApiLogin, Mode=TwoWay}" Watermark="admin user" />
|
<TextBox Grid.Row="0" Grid.Column="1" Name="ApiLoginTextBox" Text="{Binding ApiLogin, Mode=TwoWay}" Watermark="admin user" />
|
||||||
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<TextBlock Text="Dati gara" FontWeight="Bold" Margin="0,4,0,0" />
|
<TextBlock Text="Dati gara" FontWeight="Bold" Margin="0,4,0,0" />
|
||||||
<Grid ColumnDefinitions="Auto,*,Auto,*" RowDefinitions="Auto,Auto,Auto,Auto" ColumnSpacing="6" RowSpacing="6">
|
<Grid ColumnDefinitions="Auto,*,Auto,*" RowDefinitions="Auto,Auto,Auto,Auto" ColumnSpacing="6">
|
||||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="Descrizione:" VerticalAlignment="Center" />
|
<TextBlock Grid.Row="0" Grid.Column="0" Text="Descrizione:" VerticalAlignment="Center" />
|
||||||
<TextBox Grid.Row="0" Grid.Column="1" Name="ApiRaceDescriptionTextBox" Text="{Binding ApiRaceDescription, Mode=TwoWay}" Watermark="Nome gara" />
|
<TextBox Grid.Row="0" Grid.Column="1" Name="ApiRaceDescriptionTextBox" Text="{Binding ApiRaceDescription, Mode=TwoWay}" Watermark="Nome gara" />
|
||||||
|
|
||||||
|
|
@ -49,7 +49,7 @@
|
||||||
</ComboBox>
|
</ComboBox>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid ColumnDefinitions="Auto,*,Auto,*" RowDefinitions="Auto,Auto" ColumnSpacing="6" RowSpacing="6">
|
<Grid ColumnDefinitions="Auto,*,Auto,*" RowDefinitions="Auto,Auto" ColumnSpacing="6">
|
||||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="Evento Omaggio:" VerticalAlignment="Center" />
|
<TextBlock Grid.Row="0" Grid.Column="0" Text="Evento Omaggio:" VerticalAlignment="Center" />
|
||||||
<ComboBox Grid.Row="0" Grid.Column="1" Name="ApiFreeEventComboBox" SelectedIndex="{Binding ApiFreeEventIndex, Mode=TwoWay}">
|
<ComboBox Grid.Row="0" Grid.Column="1" Name="ApiFreeEventComboBox" SelectedIndex="{Binding ApiFreeEventIndex, Mode=TwoWay}">
|
||||||
<ComboBoxItem Content="0 - No" />
|
<ComboBoxItem Content="0 - No" />
|
||||||
|
|
|
||||||
34
imagecatalog/Controls/PathPickerField.axaml
Normal file
34
imagecatalog/Controls/PathPickerField.axaml
Normal 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>
|
||||||
285
imagecatalog/Controls/PathPickerField.axaml.cs
Normal file
285
imagecatalog/Controls/PathPickerField.axaml.cs
Normal 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('.');
|
||||||
|
}
|
||||||
|
}
|
||||||
8
imagecatalog/Controls/PathPickerSelectionMode.cs
Normal file
8
imagecatalog/Controls/PathPickerSelectionMode.cs
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
namespace ImageCatalog_2.Controls;
|
||||||
|
|
||||||
|
public enum PathPickerSelectionMode
|
||||||
|
{
|
||||||
|
Folder,
|
||||||
|
OpenFile,
|
||||||
|
SaveFile
|
||||||
|
}
|
||||||
31
imagecatalog/Converters/FilePathToBitmapConverter.cs
Normal file
31
imagecatalog/Converters/FilePathToBitmapConverter.cs
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using Avalonia.Data.Converters;
|
||||||
|
using Avalonia.Media.Imaging;
|
||||||
|
|
||||||
|
namespace ImageCatalog_2.Converters;
|
||||||
|
|
||||||
|
public sealed class FilePathToBitmapConverter : IValueConverter
|
||||||
|
{
|
||||||
|
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||||
|
{
|
||||||
|
if (value is not string path || string.IsNullOrWhiteSpace(path))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return new Bitmap(path);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -3,6 +3,7 @@
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<ProduceReferenceAssembly>False</ProduceReferenceAssembly>
|
<ProduceReferenceAssembly>False</ProduceReferenceAssembly>
|
||||||
|
<AvaloniaUseCompiledBindingsByDefault>false</AvaloniaUseCompiledBindingsByDefault>
|
||||||
<!-- Default assembly name for regular builds -->
|
<!-- Default assembly name for regular builds -->
|
||||||
<AssemblyName>ImageCatalog</AssemblyName>
|
<AssemblyName>ImageCatalog</AssemblyName>
|
||||||
<LangVersion>default</LangVersion>
|
<LangVersion>default</LangVersion>
|
||||||
|
|
@ -46,12 +47,16 @@
|
||||||
<DebugType>embedded</DebugType>
|
<DebugType>embedded</DebugType>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<AvaloniaXaml Remove="AvaloniaApp.axaml" />
|
||||||
<AvaloniaXaml Remove="Sorgenti\**" />
|
<AvaloniaXaml Remove="Sorgenti\**" />
|
||||||
<Compile Remove="Sorgenti\**" />
|
<Compile Remove="Sorgenti\**" />
|
||||||
<EmbeddedResource Remove="Sorgenti\**" />
|
<EmbeddedResource Remove="Sorgenti\**" />
|
||||||
<None Remove="Sorgenti\**" />
|
<None Remove="Sorgenti\**" />
|
||||||
<Page Remove="Sorgenti\**" />
|
<Page Remove="Sorgenti\**" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<AvaloniaResource Include="AvaloniaApp.axaml" />
|
||||||
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Update="Properties\Settings.settings">
|
<None Update="Properties\Settings.settings">
|
||||||
<Generator>SettingsSingleFileGenerator</Generator>
|
<Generator>SettingsSingleFileGenerator</Generator>
|
||||||
|
|
@ -65,16 +70,16 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AIFotoONLUS.Core" Version="0.1.2" Condition="'$(UseLocalAIFotoONLUS)' != 'true'" />
|
<PackageReference Include="AIFotoONLUS.Core" Version="0.1.2" Condition="'$(UseLocalAIFotoONLUS)' != 'true'" />
|
||||||
<PackageReference Include="AutoMapper" Version="16.1.0" />
|
<PackageReference Include="AutoMapper" Version="16.1.1" />
|
||||||
<PackageReference Include="IconPacks.Avalonia" Version="1.3.1" />
|
<PackageReference Include="IconPacks.Avalonia" Version="2.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.5" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.8" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.5" />
|
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.8" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="10.0.5" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="10.0.8" />
|
||||||
<PackageReference Include="MinVer" Version="7.0.0" PrivateAssets="all" />
|
<PackageReference Include="MinVer" Version="7.0.0" PrivateAssets="all" />
|
||||||
<PackageReference Include="Avalonia" Version="11.3.12" />
|
<PackageReference Include="Avalonia" Version="11.3.13" />
|
||||||
<PackageReference Include="Avalonia.Desktop" Version="11.3.12" />
|
<PackageReference Include="Avalonia.Desktop" Version="11.3.13" />
|
||||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.12" />
|
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.13" />
|
||||||
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.3.12" />
|
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.3.13" />
|
||||||
<PackageReference Include="Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers" Version="0.4.421302">
|
<PackageReference Include="Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers" Version="0.4.421302">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
|
@ -159,4 +164,13 @@
|
||||||
|
|
||||||
<Copy SourceFiles="@(LocalCudaInferenceLibrary)" DestinationFolder="$(TargetDir)" SkipUnchangedFiles="true" Condition="'@(LocalCudaInferenceLibrary)' != ''" />
|
<Copy SourceFiles="@(LocalCudaInferenceLibrary)" DestinationFolder="$(TargetDir)" SkipUnchangedFiles="true" Condition="'@(LocalCudaInferenceLibrary)' != ''" />
|
||||||
</Target>
|
</Target>
|
||||||
|
|
||||||
|
<Target Name="CopyLocalCudaInferenceLibrariesToPublish" AfterTargets="Publish" Condition="$([MSBuild]::IsOsPlatform('Windows')) and '$(UseLocalAIFotoONLUS)' == 'true' and '$(PublishDir)' != ''">
|
||||||
|
<ItemGroup>
|
||||||
|
<LocalCudaInferenceLibraryForPublish Include="$(LocalAIFotoOutputDir)\cublasLt64_11.dll" Condition="Exists('$(LocalAIFotoOutputDir)\cublasLt64_11.dll')" />
|
||||||
|
<LocalCudaInferenceLibraryForPublish Include="$(LocalAIFotoOutputDir)\cudnn_cnn_infer64_8.dll" Condition="Exists('$(LocalAIFotoOutputDir)\cudnn_cnn_infer64_8.dll')" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<Copy SourceFiles="@(LocalCudaInferenceLibraryForPublish)" DestinationFolder="$(PublishDir)" SkipUnchangedFiles="true" Condition="'@(LocalCudaInferenceLibraryForPublish)' != ''" />
|
||||||
|
</Target>
|
||||||
</Project>
|
</Project>
|
||||||
24
imagecatalog/Models/FaceMatcherResultItem.cs
Normal file
24
imagecatalog/Models/FaceMatcherResultItem.cs
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
namespace ImageCatalog_2.Models;
|
||||||
|
|
||||||
|
public sealed class FaceMatcherResultItem
|
||||||
|
{
|
||||||
|
public string PhotoId { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public double? Score { get; init; }
|
||||||
|
|
||||||
|
public string ScoreDisplay => Score.HasValue ? Score.Value.ToString("0.###") : string.Empty;
|
||||||
|
|
||||||
|
public string ResolvedImagePath { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public int CandidateCount { get; init; }
|
||||||
|
|
||||||
|
public string RawRow { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public string DebugSummary { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public string SearchImagePath { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public string CsvPath { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public string LogPath { get; init; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
@ -314,6 +314,18 @@ namespace ImageCatalog_2.Models
|
||||||
[XmlElement("AI_FaceUpsample")]
|
[XmlElement("AI_FaceUpsample")]
|
||||||
public bool FaceUpsample { get; set; } = true;
|
public bool FaceUpsample { get; set; } = true;
|
||||||
|
|
||||||
|
[JsonPropertyName("UseFaceGpu")]
|
||||||
|
[XmlElement("AI_UsaGpuFace")]
|
||||||
|
public bool UseFaceGpu { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("FaceMatcherExecutablePath")]
|
||||||
|
[XmlElement("AI_FaceMatcherExecutablePath")]
|
||||||
|
public string FaceMatcherExecutablePath { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("FaceMatcherTolerance")]
|
||||||
|
[XmlElement("AI_FaceMatcherTolerance")]
|
||||||
|
public double FaceMatcherTolerance { get; set; } = 0.5;
|
||||||
|
|
||||||
// Race upload settings
|
// Race upload settings
|
||||||
[JsonPropertyName("ApiLogin")]
|
[JsonPropertyName("ApiLogin")]
|
||||||
[XmlElement("RaceUpload_Login")]
|
[XmlElement("RaceUpload_Login")]
|
||||||
|
|
|
||||||
|
|
@ -154,6 +154,7 @@ static class Program
|
||||||
var userPrefsPath = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
var userPrefsPath = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||||
"ImageCatalog", "userprefs.xml");
|
"ImageCatalog", "userprefs.xml");
|
||||||
services.AddSingleton(new ParametriSetup(userPrefsPath));
|
services.AddSingleton(new ParametriSetup(userPrefsPath));
|
||||||
|
services.AddSingleton<PickerPreferenceService>();
|
||||||
services.AddSingleton<PicSettings>();
|
services.AddSingleton<PicSettings>();
|
||||||
|
|
||||||
services.AddCatalogCommunication(options =>
|
services.AddCatalogCommunication(options =>
|
||||||
|
|
|
||||||
|
|
@ -198,20 +198,7 @@ public class AiExtractionService : IAiExtractionService
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var dir = Path.GetDirectoryName(request.CsvOutputPath) ?? string.Empty;
|
WriteCsvOutput(request.CsvOutputPath, extractedResults);
|
||||||
if (!string.IsNullOrWhiteSpace(dir) && !Directory.Exists(dir))
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory(dir);
|
|
||||||
}
|
|
||||||
|
|
||||||
using var sw = new StreamWriter(request.CsvOutputPath, false, Encoding.UTF8);
|
|
||||||
sw.WriteLine("Path,Text");
|
|
||||||
foreach (var r in extractedResults)
|
|
||||||
{
|
|
||||||
var csvFileName = Path.GetFileName(r.Path ?? string.Empty);
|
|
||||||
var safeText = (r.Text ?? string.Empty).Replace("\"", "\"\"");
|
|
||||||
sw.WriteLine($"\"{csvFileName}\",\"{safeText}\"");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|
@ -222,6 +209,24 @@ public class AiExtractionService : IAiExtractionService
|
||||||
return summary;
|
return summary;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static void WriteCsvOutput(string csvOutputPath, IEnumerable<AiResultItem> extractedResults)
|
||||||
|
{
|
||||||
|
var dir = Path.GetDirectoryName(csvOutputPath) ?? string.Empty;
|
||||||
|
if (!string.IsNullOrWhiteSpace(dir) && !Directory.Exists(dir))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
using var sw = new StreamWriter(csvOutputPath, false, Encoding.UTF8);
|
||||||
|
sw.WriteLine("filename,text");
|
||||||
|
foreach (var result in extractedResults)
|
||||||
|
{
|
||||||
|
var csvFileName = Path.GetFileName(result.Path ?? string.Empty);
|
||||||
|
var safeText = (result.Text ?? string.Empty).Replace("\"", "\"\"");
|
||||||
|
sw.WriteLine($"\"{csvFileName}\",\"{safeText}\"");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static double CalculateAverageImagesPerSecond(int processed, TimeSpan elapsed)
|
private static double CalculateAverageImagesPerSecond(int processed, TimeSpan elapsed)
|
||||||
{
|
{
|
||||||
return elapsed.TotalSeconds > 0 ? processed / elapsed.TotalSeconds : 0;
|
return elapsed.TotalSeconds > 0 ? processed / elapsed.TotalSeconds : 0;
|
||||||
|
|
|
||||||
50
imagecatalog/Services/PathShellService.cs
Normal file
50
imagecatalog/Services/PathShellService.cs
Normal 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.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
122
imagecatalog/Services/PickerPreferenceService.cs
Normal file
122
imagecatalog/Services/PickerPreferenceService.cs
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
using Avalonia.Platform.Storage;
|
||||||
|
using ImageCatalog;
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace ImageCatalog_2.Services;
|
||||||
|
|
||||||
|
public static class PickerPreferenceKeys
|
||||||
|
{
|
||||||
|
public const string SourceFolder = "Picker.SourceFolder.LastPath";
|
||||||
|
public const string DestinationFolder = "Picker.DestinationFolder.LastPath";
|
||||||
|
public const string LogoFile = "Picker.LogoFile.LastPath";
|
||||||
|
public const string ModelsFolder = "Picker.ModelsFolder.LastPath";
|
||||||
|
public const string CsvOutput = "Picker.CsvOutput.LastPath";
|
||||||
|
public const string SettingsFile = "Picker.SettingsFile.LastPath";
|
||||||
|
public const string LastSettingsFile = "Settings.LastFilePath";
|
||||||
|
public const string FaceExecutableFolder = "Picker.FaceExecutableFolder.LastPath";
|
||||||
|
public const string FaceOutputFolder = "Picker.FaceOutputFolder.LastPath";
|
||||||
|
public const string FaceMatcherExecutable = "Picker.FaceMatcherExecutable.LastPath";
|
||||||
|
public const string FaceMatcherImage = "Picker.FaceMatcherImage.LastPath";
|
||||||
|
public const string FaceMatcherEncodings = "Picker.FaceMatcherEncodings.LastPath";
|
||||||
|
public const string FaceMatcherOutput = "Picker.FaceMatcherOutput.LastPath";
|
||||||
|
public const string FaceMatcherLog = "Picker.FaceMatcherLog.LastPath";
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class PickerPreferenceService
|
||||||
|
{
|
||||||
|
private readonly ParametriSetup _userPreferences;
|
||||||
|
|
||||||
|
public PickerPreferenceService(ParametriSetup userPreferences)
|
||||||
|
{
|
||||||
|
_userPreferences = userPreferences;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IStorageFolder?> TryGetStartFolderAsync(IStorageProvider storageProvider, string preferenceKey, string? currentPath = null)
|
||||||
|
{
|
||||||
|
var startPath = GetPreferredStartDirectory(preferenceKey, currentPath);
|
||||||
|
if (string.IsNullOrWhiteSpace(startPath))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await storageProvider.TryGetFolderFromPathAsync(new Uri(startPath)).ConfigureAwait(true);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RememberPath(string preferenceKey, string? selectedPath)
|
||||||
|
{
|
||||||
|
var directory = TryGetExistingDirectory(selectedPath);
|
||||||
|
if (string.IsNullOrWhiteSpace(directory))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_userPreferences.AggiornaParametro(preferenceKey, directory);
|
||||||
|
_userPreferences.SalvaParametriSetup();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? GetRememberedValue(string preferenceKey)
|
||||||
|
{
|
||||||
|
var value = _userPreferences.LeggiParametroString(preferenceKey);
|
||||||
|
return string.IsNullOrWhiteSpace(value)
|
||||||
|
? null
|
||||||
|
: value.Trim().Trim('"');
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RememberValue(string preferenceKey, string? value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_userPreferences.AggiornaParametro(preferenceKey, value.Trim().Trim('"'));
|
||||||
|
_userPreferences.SalvaParametriSetup();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ForgetValue(string preferenceKey)
|
||||||
|
{
|
||||||
|
if (_userPreferences.RimuoviParametro(preferenceKey))
|
||||||
|
{
|
||||||
|
_userPreferences.SalvaParametriSetup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string? GetPreferredStartDirectory(string preferenceKey, string? currentPath)
|
||||||
|
{
|
||||||
|
var storedPath = _userPreferences.LeggiParametroString(preferenceKey);
|
||||||
|
return TryGetExistingDirectory(storedPath) ?? TryGetExistingDirectory(currentPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? TryGetExistingDirectory(string? path)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(path))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalizedPath = path.Trim().Trim('"');
|
||||||
|
if (Directory.Exists(normalizedPath))
|
||||||
|
{
|
||||||
|
return normalizedPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (File.Exists(normalizedPath))
|
||||||
|
{
|
||||||
|
return Path.GetDirectoryName(normalizedPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
var containingDirectory = Path.GetDirectoryName(normalizedPath);
|
||||||
|
return !string.IsNullOrWhiteSpace(containingDirectory) && Directory.Exists(containingDirectory)
|
||||||
|
? containingDirectory
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -225,6 +225,105 @@ public class AiSettingsViewModel : ViewModelBase
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string _faceMatcherExecutablePath = string.Empty;
|
||||||
|
public string FaceMatcherExecutablePath
|
||||||
|
{
|
||||||
|
get => _faceMatcherExecutablePath;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_faceMatcherExecutablePath = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string _faceMatcherEncodingsPath = string.Empty;
|
||||||
|
public string FaceMatcherEncodingsPath
|
||||||
|
{
|
||||||
|
get => _faceMatcherEncodingsPath;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_faceMatcherEncodingsPath = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string _faceMatcherOutputPath = string.Empty;
|
||||||
|
public string FaceMatcherOutputPath
|
||||||
|
{
|
||||||
|
get => _faceMatcherOutputPath;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_faceMatcherOutputPath = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string _faceMatcherLogPath = string.Empty;
|
||||||
|
public string FaceMatcherLogPath
|
||||||
|
{
|
||||||
|
get => _faceMatcherLogPath;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_faceMatcherLogPath = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private double _faceMatcherTolerance = 0.5;
|
||||||
|
public double FaceMatcherTolerance
|
||||||
|
{
|
||||||
|
get => _faceMatcherTolerance;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_faceMatcherTolerance = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string _faceMatcherSelectedImagePath = string.Empty;
|
||||||
|
public string FaceMatcherSelectedImagePath
|
||||||
|
{
|
||||||
|
get => _faceMatcherSelectedImagePath;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_faceMatcherSelectedImagePath = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool _isFaceMatcherRunning;
|
||||||
|
public bool IsFaceMatcherRunning
|
||||||
|
{
|
||||||
|
get => _isFaceMatcherRunning;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_isFaceMatcherRunning = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string _faceMatcherStatusMessage = string.Empty;
|
||||||
|
public string FaceMatcherStatusMessage
|
||||||
|
{
|
||||||
|
get => _faceMatcherStatusMessage;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_faceMatcherStatusMessage = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string _faceMatcherCommandOutput = string.Empty;
|
||||||
|
public string FaceMatcherCommandOutput
|
||||||
|
{
|
||||||
|
get => _faceMatcherCommandOutput;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_faceMatcherCommandOutput = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private double _aiProgress;
|
private double _aiProgress;
|
||||||
public double AiProgress
|
public double AiProgress
|
||||||
{
|
{
|
||||||
|
|
@ -237,4 +336,6 @@ public class AiSettingsViewModel : ViewModelBase
|
||||||
}
|
}
|
||||||
|
|
||||||
public ObservableCollection<AiResultItem> PreviewResults { get; } = new();
|
public ObservableCollection<AiResultItem> PreviewResults { get; } = new();
|
||||||
|
|
||||||
|
public ObservableCollection<FaceMatcherResultItem> FaceMatcherResults { get; } = new();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue