diff --git a/.forgejo/workflows/build-windows-avalonia.yml b/.forgejo/workflows/build-windows-avalonia.yml index 3d3c18e..4666242 100644 --- a/.forgejo/workflows/build-windows-avalonia.yml +++ b/.forgejo/workflows/build-windows-avalonia.yml @@ -14,15 +14,15 @@ env: PROJECT_PATH: imagecatalog/ImageCatalog 2.csproj PUBLISH_DIR: artifacts/publish/win-x64 ARTIFACT_NAME: imagecatalog-windows-avalonia - NUGET_SOURCE_NAME: Nuget-GitLab-AIFotoONLUS - NUGET_SOURCE_URL: https://gitlab.com/api/v4/projects/79509532/packages/nuget/index.json + NUGET_SOURCE_NAME: Nuget-Forgejo-AIFotoONLUS + NUGET_SOURCE_URL: ${{ vars.AIFOTOONLUS_NUGET_SOURCE_URL || format('{0}/api/packages/{1}/nuget/index.json', github.server_url, vars.AIFOTOONLUS_PACKAGE_OWNER || github.repository_owner) }} jobs: build: runs-on: docker env: - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_PASSWORD }} + FORGEJO_PACKAGE_USERNAME: ${{ secrets.FORGEJO_PACKAGE_USERNAME }} + FORGEJO_PACKAGE_TOKEN: ${{ secrets.FORGEJO_PACKAGE_TOKEN }} steps: - name: Checkout @@ -36,12 +36,12 @@ jobs: - name: Validate NuGet secrets run: | set -eu - if [ -z "${NUGET_USERNAME}" ]; then - echo "secrets.NUGET_USERNAME is required" + if [ -z "${FORGEJO_PACKAGE_USERNAME}" ]; then + echo "secrets.FORGEJO_PACKAGE_USERNAME is required" exit 1 fi - if [ -z "${NUGET_PASSWORD}" ]; then - echo "secrets.NUGET_PASSWORD is required" + if [ -z "${FORGEJO_PACKAGE_TOKEN}" ]; then + echo "secrets.FORGEJO_PACKAGE_TOKEN is required" exit 1 fi @@ -53,8 +53,8 @@ jobs: dotnet nuget update source "${{ env.NUGET_SOURCE_NAME }}" \ --source "${{ env.NUGET_SOURCE_URL }}" \ - --username "${NUGET_USERNAME}" \ - --password "${NUGET_PASSWORD}" \ + --username "${FORGEJO_PACKAGE_USERNAME}" \ + --password "${FORGEJO_PACKAGE_TOKEN}" \ --store-password-in-clear-text \ --configfile "${temp_config}" diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 3734723..3ce4bbe 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -4,7 +4,7 @@ ```powershell # Build -dotnet build Catalog.sln +dotnet build Catalog.slnx # Run all tests dotnet test MaddoShared.Tests @@ -21,13 +21,13 @@ dotnet publish "imagecatalog\ImageCatalog 2.csproj" -c Release -r win-x64 --self ## Architecture -This is a WinForms/WPF image cataloging application targeting .NET 10.0-windows. +This is an Avalonia image cataloging application targeting .NET 10.0-windows. ### Projects | Project | Purpose | |---------|---------| -| **imagecatalog** | Main desktop application — WinForms (default), WPF (`--wpf`), or Avalonia (`--avalonia`) | +| **imagecatalog** | Main desktop application — Avalonia with Fluent theme (`AvaloniaMainWindow`) | | **MaddoShared** | Shared image processing library (the core) | | **MaddoShared.Tests** | Unit tests for MaddoShared | | **MaddoShared.Benchmarks** | BenchmarkDotNet performance benchmarks | @@ -35,12 +35,7 @@ This is a WinForms/WPF image cataloging application targeting .NET 10.0-windows. | **ImageCatalogCS / ImageCatalogParallel** | Legacy/experimental variants | | **CatalogLib / CatalogLibVb / CatalogVbLib** | Legacy VB.NET libraries | -The main app selects its UI at startup via command-line flag: -- *(default)* — WinForms (`MainForm`) -- `--wpf` — WPF with MahApps.Metro (`MainWindow`) -- `--avalonia` — Avalonia with Fluent theme (`AvaloniaMainWindow`) - -All three UIs bind to the same `DataModel`. Dialog events (`SelectSourceFolderRequested`, etc.) are subscribed in each window's code-behind. `DataModel.UiInvoker` must be set by the active UI to enable cross-thread UI updates (Avalonia sets this to `Dispatcher.UIThread.Invoke`; WPF uses `Application.Current.Dispatcher`). +The main app launches Avalonia directly. Dialog events (`SelectSourceFolderRequested`, etc.) are subscribed in `AvaloniaMainWindow` code-behind. `DataModel.UiInvoker` must be set by the active UI to enable cross-thread UI updates (Avalonia sets this to `Dispatcher.UIThread.Invoke`). ### Core Flow diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..0af4af3 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,16 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "ImageCatalog Avalonia", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build ImageCatalog Avalonia", + "program": "${workspaceFolder:Catalog}/imagecatalog/bin/Debug/net10.0-windows/win-x64/ImageCatalog.exe", + "args": [], + "cwd": "${workspaceFolder:Catalog}/imagecatalog", + "stopAtEntry": false, + "console": "internalConsole" + } + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..22c598b --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,18 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build ImageCatalog Avalonia", + "type": "process", + "command": "dotnet", + "args": [ + "build", + "${workspaceFolder:Catalog}/imagecatalog/ImageCatalog 2.csproj", + "--configuration", + "Debug" + ], + "problemMatcher": "$msCompile", + "group": "build" + } + ] +} \ No newline at end of file diff --git a/Catalog.code-workspace b/Catalog.code-workspace new file mode 100644 index 0000000..15ec3a0 --- /dev/null +++ b/Catalog.code-workspace @@ -0,0 +1,16 @@ +{ + "folders": [ + { + "path": "." + }, + { + "path": "../AIFotoONLUS" + }, + { + "path": "../../various/regalamiunsorriso" + } + ], + "settings": { + "commentTranslate.hover.enabled": false + } +} \ No newline at end of file diff --git a/Catalog.sln b/Catalog.sln deleted file mode 100644 index 5d1d09f..0000000 --- a/Catalog.sln +++ /dev/null @@ -1,116 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 18 -VisualStudioVersion = 18.2.11415.280 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImageCatalog 2", "imagecatalog\ImageCatalog 2.csproj", "{3F1E23DB-435E-0590-1EF5-735E898DBA3C}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Libraries", "Libraries", "{A3D50937-74F6-4DC8-8D89-B534B484C0F9}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MaddoShared", "MaddoShared\MaddoShared.csproj", "{AEBFE9E3-277C-4A7B-8448-145D1B11998B}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{5F0BEF23-B1EA-4100-A772-DC455D40B1C1}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MaddoShared.Tests", "MaddoShared.Tests\MaddoShared.Tests.csproj", "{59952BE8-20B4-4BF2-9367-705F41395265}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MaddoShared.Benchmarks", "MaddoShared.Benchmarks\MaddoShared.Benchmarks.csproj", "{07499348-8C15-4DCC-8316-4AD121A43C38}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Catalog.Communication", "Catalog.Communication\Catalog.Communication.csproj", "{EF5D3B7E-F380-4976-A0A9-085FEA157F79}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MaddoShared.ImageSharpTests", "MaddoShared.ImageSharpTests\MaddoShared.ImageSharpTests.csproj", "{1528903F-3BF9-599C-2DD0-0AF7B5706675}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Debug|x64 = Debug|x64 - Debug|x86 = Debug|x86 - Release|Any CPU = Release|Any CPU - Release|x64 = Release|x64 - Release|x86 = Release|x86 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {3F1E23DB-435E-0590-1EF5-735E898DBA3C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3F1E23DB-435E-0590-1EF5-735E898DBA3C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3F1E23DB-435E-0590-1EF5-735E898DBA3C}.Debug|x64.ActiveCfg = Debug|Any CPU - {3F1E23DB-435E-0590-1EF5-735E898DBA3C}.Debug|x64.Build.0 = Debug|Any CPU - {3F1E23DB-435E-0590-1EF5-735E898DBA3C}.Debug|x86.ActiveCfg = Debug|x86 - {3F1E23DB-435E-0590-1EF5-735E898DBA3C}.Debug|x86.Build.0 = Debug|x86 - {3F1E23DB-435E-0590-1EF5-735E898DBA3C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3F1E23DB-435E-0590-1EF5-735E898DBA3C}.Release|Any CPU.Build.0 = Release|Any CPU - {3F1E23DB-435E-0590-1EF5-735E898DBA3C}.Release|x64.ActiveCfg = Release|x64 - {3F1E23DB-435E-0590-1EF5-735E898DBA3C}.Release|x64.Build.0 = Release|x64 - {3F1E23DB-435E-0590-1EF5-735E898DBA3C}.Release|x86.ActiveCfg = Release|x86 - {3F1E23DB-435E-0590-1EF5-735E898DBA3C}.Release|x86.Build.0 = Release|x86 - {AEBFE9E3-277C-4A7B-8448-145D1B11998B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AEBFE9E3-277C-4A7B-8448-145D1B11998B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AEBFE9E3-277C-4A7B-8448-145D1B11998B}.Debug|x64.ActiveCfg = Debug|Any CPU - {AEBFE9E3-277C-4A7B-8448-145D1B11998B}.Debug|x64.Build.0 = Debug|Any CPU - {AEBFE9E3-277C-4A7B-8448-145D1B11998B}.Debug|x86.ActiveCfg = Debug|Any CPU - {AEBFE9E3-277C-4A7B-8448-145D1B11998B}.Debug|x86.Build.0 = Debug|Any CPU - {AEBFE9E3-277C-4A7B-8448-145D1B11998B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AEBFE9E3-277C-4A7B-8448-145D1B11998B}.Release|Any CPU.Build.0 = Release|Any CPU - {AEBFE9E3-277C-4A7B-8448-145D1B11998B}.Release|x64.ActiveCfg = Release|Any CPU - {AEBFE9E3-277C-4A7B-8448-145D1B11998B}.Release|x64.Build.0 = Release|Any CPU - {AEBFE9E3-277C-4A7B-8448-145D1B11998B}.Release|x86.ActiveCfg = Release|Any CPU - {AEBFE9E3-277C-4A7B-8448-145D1B11998B}.Release|x86.Build.0 = Release|Any CPU - {59952BE8-20B4-4BF2-9367-705F41395265}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {59952BE8-20B4-4BF2-9367-705F41395265}.Debug|Any CPU.Build.0 = Debug|Any CPU - {59952BE8-20B4-4BF2-9367-705F41395265}.Debug|x64.ActiveCfg = Debug|Any CPU - {59952BE8-20B4-4BF2-9367-705F41395265}.Debug|x64.Build.0 = Debug|Any CPU - {59952BE8-20B4-4BF2-9367-705F41395265}.Debug|x86.ActiveCfg = Debug|Any CPU - {59952BE8-20B4-4BF2-9367-705F41395265}.Debug|x86.Build.0 = Debug|Any CPU - {59952BE8-20B4-4BF2-9367-705F41395265}.Release|Any CPU.ActiveCfg = Release|Any CPU - {59952BE8-20B4-4BF2-9367-705F41395265}.Release|Any CPU.Build.0 = Release|Any CPU - {59952BE8-20B4-4BF2-9367-705F41395265}.Release|x64.ActiveCfg = Release|Any CPU - {59952BE8-20B4-4BF2-9367-705F41395265}.Release|x64.Build.0 = Release|Any CPU - {59952BE8-20B4-4BF2-9367-705F41395265}.Release|x86.ActiveCfg = Release|Any CPU - {59952BE8-20B4-4BF2-9367-705F41395265}.Release|x86.Build.0 = Release|Any CPU - {07499348-8C15-4DCC-8316-4AD121A43C38}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {07499348-8C15-4DCC-8316-4AD121A43C38}.Debug|Any CPU.Build.0 = Debug|Any CPU - {07499348-8C15-4DCC-8316-4AD121A43C38}.Debug|x64.ActiveCfg = Debug|Any CPU - {07499348-8C15-4DCC-8316-4AD121A43C38}.Debug|x64.Build.0 = Debug|Any CPU - {07499348-8C15-4DCC-8316-4AD121A43C38}.Debug|x86.ActiveCfg = Debug|Any CPU - {07499348-8C15-4DCC-8316-4AD121A43C38}.Debug|x86.Build.0 = Debug|Any CPU - {07499348-8C15-4DCC-8316-4AD121A43C38}.Release|Any CPU.ActiveCfg = Release|Any CPU - {07499348-8C15-4DCC-8316-4AD121A43C38}.Release|Any CPU.Build.0 = Release|Any CPU - {07499348-8C15-4DCC-8316-4AD121A43C38}.Release|x64.ActiveCfg = Release|Any CPU - {07499348-8C15-4DCC-8316-4AD121A43C38}.Release|x64.Build.0 = Release|Any CPU - {07499348-8C15-4DCC-8316-4AD121A43C38}.Release|x86.ActiveCfg = Release|Any CPU - {07499348-8C15-4DCC-8316-4AD121A43C38}.Release|x86.Build.0 = Release|Any CPU - {EF5D3B7E-F380-4976-A0A9-085FEA157F79}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EF5D3B7E-F380-4976-A0A9-085FEA157F79}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EF5D3B7E-F380-4976-A0A9-085FEA157F79}.Debug|x64.ActiveCfg = Debug|Any CPU - {EF5D3B7E-F380-4976-A0A9-085FEA157F79}.Debug|x64.Build.0 = Debug|Any CPU - {EF5D3B7E-F380-4976-A0A9-085FEA157F79}.Debug|x86.ActiveCfg = Debug|Any CPU - {EF5D3B7E-F380-4976-A0A9-085FEA157F79}.Debug|x86.Build.0 = Debug|Any CPU - {EF5D3B7E-F380-4976-A0A9-085FEA157F79}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EF5D3B7E-F380-4976-A0A9-085FEA157F79}.Release|Any CPU.Build.0 = Release|Any CPU - {EF5D3B7E-F380-4976-A0A9-085FEA157F79}.Release|x64.ActiveCfg = Release|Any CPU - {EF5D3B7E-F380-4976-A0A9-085FEA157F79}.Release|x64.Build.0 = Release|Any CPU - {EF5D3B7E-F380-4976-A0A9-085FEA157F79}.Release|x86.ActiveCfg = Release|Any CPU - {EF5D3B7E-F380-4976-A0A9-085FEA157F79}.Release|x86.Build.0 = Release|Any CPU - {1528903F-3BF9-599C-2DD0-0AF7B5706675}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1528903F-3BF9-599C-2DD0-0AF7B5706675}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1528903F-3BF9-599C-2DD0-0AF7B5706675}.Debug|x64.ActiveCfg = Debug|Any CPU - {1528903F-3BF9-599C-2DD0-0AF7B5706675}.Debug|x64.Build.0 = Debug|Any CPU - {1528903F-3BF9-599C-2DD0-0AF7B5706675}.Debug|x86.ActiveCfg = Debug|Any CPU - {1528903F-3BF9-599C-2DD0-0AF7B5706675}.Debug|x86.Build.0 = Debug|Any CPU - {1528903F-3BF9-599C-2DD0-0AF7B5706675}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1528903F-3BF9-599C-2DD0-0AF7B5706675}.Release|Any CPU.Build.0 = Release|Any CPU - {1528903F-3BF9-599C-2DD0-0AF7B5706675}.Release|x64.ActiveCfg = Release|Any CPU - {1528903F-3BF9-599C-2DD0-0AF7B5706675}.Release|x64.Build.0 = Release|Any CPU - {1528903F-3BF9-599C-2DD0-0AF7B5706675}.Release|x86.ActiveCfg = Release|Any CPU - {1528903F-3BF9-599C-2DD0-0AF7B5706675}.Release|x86.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {AEBFE9E3-277C-4A7B-8448-145D1B11998B} = {A3D50937-74F6-4DC8-8D89-B534B484C0F9} - {59952BE8-20B4-4BF2-9367-705F41395265} = {5F0BEF23-B1EA-4100-A772-DC455D40B1C1} - {EF5D3B7E-F380-4976-A0A9-085FEA157F79} = {A3D50937-74F6-4DC8-8D89-B534B484C0F9} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {0E3ABC63-8601-4DAC-AFEA-33F3E8E36757} - EndGlobalSection -EndGlobal diff --git a/Catalog.slnx b/Catalog.slnx new file mode 100644 index 0000000..3c8cf36 --- /dev/null +++ b/Catalog.slnx @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/MaddoShared.Tests/DataModelCharacterizationTests.cs b/MaddoShared.Tests/DataModelCharacterizationTests.cs index 7ce601e..cba6ce0 100644 --- a/MaddoShared.Tests/DataModelCharacterizationTests.cs +++ b/MaddoShared.Tests/DataModelCharacterizationTests.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using System.Threading.Tasks; using ImageCatalog_2; using ImageCatalog_2.Services; @@ -134,6 +135,61 @@ public class DataModelCharacterizationTests model.FontSize.ShouldBe(42); } + [TestMethod] + public void FaceExecutableFolder_EnablesGpuToggleWhenBothVariantsExist() + { + using var root = new TemporaryDirectory(); + CreateFaceEncoderExecutable(root.Path, "cpu"); + CreateFaceEncoderExecutable(root.Path, "gpu"); + + var model = CreateModel(); + + model.FaceExecutablePath = root.Path; + + model.FaceGpuOptionEnabled.ShouldBeTrue(); + model.UseFaceGpu.ShouldBeFalse(); + } + + [TestMethod] + public void UseFaceGpu_UpdatesUpsampleWhenUsingRecommendedDefault() + { + using var root = new TemporaryDirectory(); + CreateFaceEncoderExecutable(root.Path, "cpu"); + CreateFaceEncoderExecutable(root.Path, "gpu"); + + var model = CreateModel(); + model.FaceExecutablePath = root.Path; + model.FaceUpsample.ShouldBeTrue(); + + model.UseFaceGpu = true; + + model.FaceUpsample.ShouldBeFalse(); + } + + [TestMethod] + public void ResolveConfiguredFaceEncoderExecutablePath_UsesFolderLayoutFromPowerShellScript() + { + using var root = new TemporaryDirectory(); + var cpuExecutable = CreateFaceEncoderExecutable(root.Path, "cpu"); + var gpuExecutable = CreateFaceEncoderExecutable(root.Path, "gpu"); + + DataModel.ResolveConfiguredFaceEncoderExecutablePath(root.Path, useGpu: false).ShouldBe(cpuExecutable); + DataModel.ResolveConfiguredFaceEncoderExecutablePath(root.Path, useGpu: true).ShouldBe(gpuExecutable); + } + + [TestMethod] + public void BuildFaceEncoderOutputPaths_UsesTimestampAndSanitizedFolderName() + { + var timestamp = new DateTime(2026, 5, 9, 14, 30, 45); + var output = DataModel.BuildFaceEncoderOutputPaths( + @"C:\out", + @"C:\images\04 APRILE: gara?", + timestamp); + + output.OutputFilePath.ShouldBe(@"C:\out\face_encodings_20260509_143045_04_APRILE_gara.pkl"); + output.LogFilePath.ShouldBe(@"C:\out\encoder_log_20260509_143045_04_APRILE_gara.txt"); + } + private static DataModel CreateModel( ISettingsService? settingsService = null, ITestService? testService = null) @@ -169,4 +225,33 @@ public class DataModelCharacterizationTests Substitute.For>(), versionProvider: null); } + + private static string CreateFaceEncoderExecutable(string rootPath, string variant) + { + var variantDirectory = Path.Combine(rootPath, $"face_encoder_{variant}"); + Directory.CreateDirectory(variantDirectory); + + var executablePath = Path.Combine(variantDirectory, $"face_encoder_{variant}.exe"); + File.WriteAllText(executablePath, "stub"); + return executablePath; + } + + 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); + } + } + } } diff --git a/MaddoShared.Tests/MaddoShared.Tests.csproj b/MaddoShared.Tests/MaddoShared.Tests.csproj index 1d562c8..d8936a9 100644 --- a/MaddoShared.Tests/MaddoShared.Tests.csproj +++ b/MaddoShared.Tests/MaddoShared.Tests.csproj @@ -17,7 +17,7 @@ - + diff --git a/NuGet.Config b/NuGet.Config index 79cf275..e0e2d2b 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -1,25 +1,25 @@ - + - + - - + + diff --git a/imagecatalog/AvaloniaMainWindow.axaml.cs b/imagecatalog/AvaloniaMainWindow.axaml.cs index 2de8d3f..8ebe952 100644 --- a/imagecatalog/AvaloniaMainWindow.axaml.cs +++ b/imagecatalog/AvaloniaMainWindow.axaml.cs @@ -3,6 +3,7 @@ using Avalonia.Interactivity; using Avalonia.Platform.Storage; using Avalonia.Styling; using Avalonia.Threading; +using System.ComponentModel; using System.IO; namespace ImageCatalog_2; @@ -20,6 +21,7 @@ public partial class AvaloniaMainWindow : Window DataContext = _model; Opened += (_, _) => SyncThemeStateFromCurrentTheme(); + Closing += AvaloniaMainWindow_Closing; // Let DataModel marshal callbacks onto Avalonia UI thread. _model.UiInvoker = action => Dispatcher.UIThread.Invoke(action); @@ -135,6 +137,29 @@ public partial class AvaloniaMainWindow : Window }; } + private bool _isStoppingFaceEncoderForClose; + + private async void AvaloniaMainWindow_Closing(object? sender, CancelEventArgs e) + { + if (_isStoppingFaceEncoderForClose || !_model.IsFaceEncoderRunning) + { + return; + } + + e.Cancel = true; + _isStoppingFaceEncoderForClose = true; + + try + { + await _model.StopFaceEncoderAsync("Arresto face encoder in chiusura...", waitForExit: true); + } + finally + { + _isStoppingFaceEncoderForClose = false; + Close(); + } + } + private void ToggleTheme_Click(object? sender, RoutedEventArgs e) { _isDarkTheme = !_isDarkTheme; diff --git a/imagecatalog/AvaloniaViews/FaceAiTabView.axaml b/imagecatalog/AvaloniaViews/FaceAiTabView.axaml index 7f7f3ae..134621c 100644 --- a/imagecatalog/AvaloniaViews/FaceAiTabView.axaml +++ b/imagecatalog/AvaloniaViews/FaceAiTabView.axaml @@ -5,16 +5,16 @@ - - + - - + + @@ -26,6 +26,26 @@ + + + + + + + + + + + + + + + + @@ -39,11 +59,11 @@ - - - @@ -54,18 +74,36 @@ + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/imagecatalog/MainWindow.xaml.cs b/imagecatalog/MainWindow.xaml.cs deleted file mode 100644 index 146ccb3..0000000 --- a/imagecatalog/MainWindow.xaml.cs +++ /dev/null @@ -1,383 +0,0 @@ -#if WINDOWS -using System.Windows; -using MahApps.Metro.Controls; -using ControlzEx.Theming; -using Microsoft.Extensions.DependencyInjection; -using System; -using System.IO; -using System.Windows.Media.Imaging; -using System.Diagnostics; -using Microsoft.Win32; -using System.Windows.Forms; - -namespace ImageCatalog_2 -{ - public partial class MainWindow : MetroWindow - { - private readonly DataModel _model; - private bool _isDarkTheme = false; - public MainWindow(DataModel model) - { - InitializeComponent(); - _model = model; - DataContext = _model; - // Set product version in status bar (use ProductVersion rather than AssemblyVersion) - try - { - var entry = System.Reflection.Assembly.GetEntryAssembly(); - string version = string.Empty; - if (entry is not null && !string.IsNullOrEmpty(entry.Location)) - { - try - { - version = FileVersionInfo.GetVersionInfo(entry.Location).ProductVersion ?? string.Empty; - } - catch { } - } - if (string.IsNullOrWhiteSpace(version)) - { - // fallback to assembly version - version = entry?.GetName().Version?.ToString() ?? string.Empty; - } - VersionTextBlock.Text = string.IsNullOrWhiteSpace(version) ? string.Empty : $"v{version}"; - } - catch { } - // Ensure MahApps resource dictionaries are loaded so chrome/styles are available - EnsureMahAppsResourcesLoaded(); - - // Apply theme based on user preference or system setting (default to light) - ApplyTheme(isDark: false); - // Subscribe to DataModel events that require UI dialogs - _model.SelectSourceFolderRequested += Model_SelectSourceFolderRequested; - _model.SelectDestinationFolderRequested += Model_SelectDestinationFolderRequested; - _model.SelectLogoFileRequested += Model_SelectLogoFileRequested; - _model.SaveSettingsRequested += Model_SaveSettingsRequested; - _model.LoadSettingsRequested += Model_LoadSettingsRequested; - _model.SelectColorRequested += Model_SelectColorRequested; - _model.SelectTransparentColorRequested += Model_SelectTransparentColorRequested; - _model.SelectModelsFolderRequested += Model_SelectModelsFolderRequested; - _model.SelectCsvOutputRequested += Model_SelectCsvOutputRequested; - - // Watch for logo changes to update preview - _model.PropertyChanged += Model_PropertyChanged; - } - - private void ApplyTheme(bool isDark) - { - try - { - var rd = isDark ? (ResourceDictionary)Resources["DarkTheme"] : (ResourceDictionary)Resources["LightTheme"]; - foreach (var key in rd.Keys) - { - // If the theme dictionary uses suffixed keys (e.g. "WindowBackgroundBrush.Dark"), - // map them to the base key ("WindowBackgroundBrush") so existing DynamicResource lookups update. - string outKey = key?.ToString() ?? string.Empty; - if (outKey.EndsWith(".Light", StringComparison.OrdinalIgnoreCase)) - outKey = outKey.Substring(0, outKey.Length - ".Light".Length); - else if (outKey.EndsWith(".Dark", StringComparison.OrdinalIgnoreCase)) - outKey = outKey.Substring(0, outKey.Length - ".Dark".Length); - - Resources[outKey] = rd[key]; - } - } - catch - { - // ignore theme failures - } - } - - private void Model_SelectModelsFolderRequested(object? sender, EventArgs e) - { - var dlg = new System.Windows.Forms.FolderBrowserDialog(); - var starting = string.IsNullOrWhiteSpace(_model.ModelsFolderPath) ? Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments) : _model.ModelsFolderPath; - dlg.SelectedPath = starting; - if (dlg.ShowDialog() == System.Windows.Forms.DialogResult.OK) - { - _model.ModelsFolderPath = dlg.SelectedPath + Path.DirectorySeparatorChar; - } - } - - private void OpenModelsFolder_Click(object sender, RoutedEventArgs e) - { - try - { - var path = _model.ModelsFolderPath; - if (string.IsNullOrWhiteSpace(path)) return; - path = path.Trim().Trim('"'); - if (File.Exists(path)) - { - System.Diagnostics.Process.Start("explorer.exe", $"/select,\"{path}\""); - return; - } - if (Directory.Exists(path)) - { - System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = path, UseShellExecute = true }); - return; - } - } - catch { } - } - - private void Model_SelectCsvOutputRequested(object? sender, EventArgs e) - { - var dlg = new Microsoft.Win32.SaveFileDialog(); - dlg.Filter = "CSV file (*.csv)|*.csv|All files (*.*)|*.*"; - if (!string.IsNullOrWhiteSpace(_model.CsvOutputPath)) dlg.FileName = _model.CsvOutputPath; - var result = dlg.ShowDialog(this); - if (result == true) - { - _model.CsvOutputPath = dlg.FileName; - } - } - - private void OpenCsvOutputFolder_Click(object sender, RoutedEventArgs e) - { - try - { - var path = _model.CsvOutputPath; - if (string.IsNullOrWhiteSpace(path)) return; - path = path.Trim().Trim('"'); - if (File.Exists(path)) - { - System.Diagnostics.Process.Start("explorer.exe", $"/select,\"{path}\""); - return; - } - var dir = Path.GetDirectoryName(path); - if (!string.IsNullOrWhiteSpace(dir) && Directory.Exists(dir)) - { - System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = dir, UseShellExecute = true }); - return; - } - } - catch { } - } - - private void Model_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) - { - if (e is null || string.IsNullOrWhiteSpace(e.PropertyName)) return; - if (e.PropertyName == nameof(_model.LogoFile)) - { - UpdateLogoPreview(_model.LogoFile); - } - } - - private void Model_SelectSourceFolderRequested(object? sender, EventArgs e) - { - var dlg = new System.Windows.Forms.FolderBrowserDialog(); - var starting = string.IsNullOrWhiteSpace(_model.SourcePath) ? Environment.GetFolderPath(Environment.SpecialFolder.MyPictures) : _model.SourcePath; - dlg.SelectedPath = starting; - if (dlg.ShowDialog() == System.Windows.Forms.DialogResult.OK) - { - _model.SourcePath = dlg.SelectedPath + Path.DirectorySeparatorChar; - } - } - - private void OpenSourceFolder_Click(object sender, RoutedEventArgs e) - { - try - { - var path = _model.SourcePath; - if (string.IsNullOrWhiteSpace(path)) return; - path = path.Trim().Trim('"'); - if (File.Exists(path)) - { - System.Diagnostics.Process.Start("explorer.exe", $"/select,\"{path}\""); - return; - } - if (Directory.Exists(path)) - { - System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = path, UseShellExecute = true }); - return; - } - } - catch (Exception ex) - { - // ignore for now, or could show a message - } - } - - private void Model_SelectDestinationFolderRequested(object? sender, EventArgs e) - { - var dlg = new System.Windows.Forms.FolderBrowserDialog(); - var starting = string.IsNullOrWhiteSpace(_model.DestinationPath) ? Environment.GetFolderPath(Environment.SpecialFolder.MyPictures) : _model.DestinationPath; - dlg.SelectedPath = starting; - if (dlg.ShowDialog() == System.Windows.Forms.DialogResult.OK) - { - _model.DestinationPath = dlg.SelectedPath + Path.DirectorySeparatorChar; - } - - } - - private void OpenDestinationFolder_Click(object sender, RoutedEventArgs e) - { - try - { - var path = _model.DestinationPath; - if (string.IsNullOrWhiteSpace(path)) return; - path = path.Trim().Trim('"'); - if (File.Exists(path)) - { - System.Diagnostics.Process.Start("explorer.exe", $"/select,\"{path}\""); - return; - } - if (Directory.Exists(path)) - { - System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = path, UseShellExecute = true }); - return; - } - } - catch (Exception ex) - { - // ignore for now - } - } - - private void Model_SelectLogoFileRequested(object? sender, EventArgs e) - { - var dlg = new Microsoft.Win32.OpenFileDialog(); - dlg.Filter = "Image Files|*.jpg;*.jpeg;*.png;*.bmp;*.gif"; - if (!string.IsNullOrWhiteSpace(_model.LogoFile)) dlg.FileName = _model.LogoFile; - var result = dlg.ShowDialog(this); - if (result == true) - { - _model.LogoFile = dlg.FileName; - } - } - - private async void Model_SaveSettingsRequested(object? sender, string filePath) - { - var dlg = new Microsoft.Win32.SaveFileDialog(); - dlg.Filter = "Setup (*.xml)|*.xml|All valid files (*.*)|*.*"; - var result = dlg.ShowDialog(this); - if (result == true) - { - await _model.SaveSettingsToFileAsync(dlg.FileName); - } - } - - private async void Model_LoadSettingsRequested(object? sender, string filePath) - { - var dlg = new Microsoft.Win32.OpenFileDialog(); - dlg.Filter = "Setup (*.xml)|*.xml|All valid files (*.*)|*.*"; - var result = dlg.ShowDialog(this); - if (result == true) - { - await _model.LoadSettingsFromFileAsync(dlg.FileName); - } - } - - private void Model_SelectColorRequested(object? sender, EventArgs e) - { - var dlg = new System.Windows.Forms.ColorDialog { AllowFullOpen = true }; - if (!string.IsNullOrWhiteSpace(_model.TextColorRGB)) - { - try { dlg.Color = System.Drawing.ColorTranslator.FromHtml(_model.TextColorRGB); } catch { } - } - if (dlg.ShowDialog() == System.Windows.Forms.DialogResult.OK) - { - _model.TextColorRGB = System.Drawing.ColorTranslator.ToHtml(dlg.Color); - } - } - - private void Model_SelectTransparentColorRequested(object? sender, EventArgs e) - { - var dlg = new System.Windows.Forms.ColorDialog { AllowFullOpen = true }; - try { dlg.Color = System.Drawing.ColorTranslator.FromHtml(_model.TransparentColor); } catch { } - if (dlg.ShowDialog() == System.Windows.Forms.DialogResult.OK) - { - _model.TransparentColor = System.Drawing.ColorTranslator.ToHtml(dlg.Color); - } - } - - private void ToggleTheme_Click(object? sender, RoutedEventArgs e) - { - ToggleTheme(); - } - - private void ToggleTheme() - { - try - { - _isDarkTheme = !_isDarkTheme; - - // Use MahApps ThemeManager to change the application theme (handles chrome and brushes) - try - { - var themeName = _isDarkTheme ? "Dark.Blue" : "Light.Blue"; - ThemeManager.Current.ChangeTheme(System.Windows.Application.Current, themeName); - } - catch - { - // Fall back silently if ThemeManager isn't available - } - - // Still apply local resource overrides so any app-specific keys update - ApplyTheme(_isDarkTheme); - } - catch - { - // ignore toggle failures - } - } - - private void EnsureMahAppsResourcesLoaded() - { - try - { - var app = System.Windows.Application.Current; - if (app is null) - return; - - var mds = app.Resources.MergedDictionaries; - - // Helper to add if missing - void AddIfMissing(string uriString) - { - if (!mds.Any(d => d.Source is not null && d.Source.OriginalString.Equals(uriString, StringComparison.OrdinalIgnoreCase))) - { - mds.Add(new ResourceDictionary { Source = new Uri(uriString) }); - } - } - - AddIfMissing("pack://application:,,,/MahApps.Metro;component/Styles/Controls.xaml"); - AddIfMissing("pack://application:,,,/MahApps.Metro;component/Styles/Fonts.xaml"); - // Ensure a default theme is present - if (!mds.Any(d => d.Source is not null && d.Source.OriginalString.IndexOf("/MahApps.Metro;component/Styles/Themes/", StringComparison.OrdinalIgnoreCase) >= 0)) - { - AddIfMissing("pack://application:,,,/MahApps.Metro;component/Styles/Themes/Light.Blue.xaml"); - _isDarkTheme = false; - } - } - catch - { - // ignore; styling will fallback to local resources - } - } - - private void UpdateLogoPreview(string? path) - { - if (string.IsNullOrWhiteSpace(path) || !File.Exists(path)) - { - LogoPreview.Source = null; - return; - } - - try - { - var bitmap = new BitmapImage(); - bitmap.BeginInit(); - bitmap.CacheOption = BitmapCacheOption.OnLoad; - bitmap.UriSource = new Uri(path); - bitmap.EndInit(); - LogoPreview.Source = bitmap; - } - catch - { - LogoPreview.Source = null; - } - } - } -} - - -#endif diff --git a/imagecatalog/Models/SettingsDto.cs b/imagecatalog/Models/SettingsDto.cs index 7d56b15..4891981 100644 --- a/imagecatalog/Models/SettingsDto.cs +++ b/imagecatalog/Models/SettingsDto.cs @@ -282,6 +282,26 @@ namespace ImageCatalog_2.Models [XmlElement("AI_FaceOutputFolderPath")] public string FaceOutputFolderPath { get; set; } = string.Empty; + [JsonPropertyName("FaceRecursive")] + [XmlElement("AI_FaceRecursive")] + public bool FaceRecursive { get; set; } + + [JsonPropertyName("FaceIncludeThumbnails")] + [XmlElement("AI_FaceIncludeThumbnails")] + public bool FaceIncludeThumbnails { get; set; } + + [JsonPropertyName("FaceParallelism")] + [XmlElement("AI_FaceParallelism")] + public int FaceParallelism { get; set; } = 3; + + [JsonPropertyName("FaceMinSize")] + [XmlElement("AI_FaceMinSize")] + public int FaceMinSize { get; set; } = 35; + + [JsonPropertyName("FaceUpsample")] + [XmlElement("AI_FaceUpsample")] + public bool FaceUpsample { get; set; } = true; + // Race upload settings [JsonPropertyName("ApiLogin")] [XmlElement("RaceUpload_Login")] diff --git a/imagecatalog/Program.cs b/imagecatalog/Program.cs index 99fb2da..fb38a24 100644 --- a/imagecatalog/Program.cs +++ b/imagecatalog/Program.cs @@ -11,7 +11,6 @@ using Microsoft.Extensions.Logging.Console; using System.IO; using Microsoft.Extensions.Options; using Avalonia; -using Avalonia.Controls.ApplicationLifetimes; namespace ImageCatalog_2; @@ -21,6 +20,9 @@ static class Program [DllImport("kernel32.dll", SetLastError = true)] private static extern bool AllocConsole(); + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool FreeConsole(); + [DllImport("kernel32.dll", SetLastError = true)] static extern IntPtr GetStdHandle(int nStdHandle); @@ -36,6 +38,12 @@ static class Program [DllImport("kernel32.dll", SetLastError = true)] static extern bool AttachConsole(int dwProcessId); + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool GenerateConsoleCtrlEvent(uint dwCtrlEvent, uint dwProcessGroupId); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool SetConsoleCtrlHandler(ConsoleCtrlDelegate? handlerRoutine, bool add); + [DllImport("kernel32.dll", SetLastError = true)] static extern IntPtr CreateFile( string lpFileName, @@ -48,6 +56,9 @@ static class Program private const uint GENERIC_WRITE = 0x40000000; private const uint OPEN_EXISTING = 3; + private const uint CTRL_C_EVENT = 0; + + private delegate bool ConsoleCtrlDelegate(uint ctrlType); private static void RedirectConsoleOutput() { @@ -58,6 +69,12 @@ static class Program Console.SetOut(standardOutput); Console.SetError(standardOutput); } + + internal static bool TrySendConsoleInterrupt(int processId) + { + _ = processId; + return false; + } #endif public static IServiceProvider ServiceProvider { get; private set; } = default!; @@ -71,10 +88,6 @@ static class Program static void Main(string[] args) { #if WINDOWS - System.Windows.Forms.Application.SetHighDpiMode(System.Windows.Forms.HighDpiMode.SystemAware); - System.Windows.Forms.Application.EnableVisualStyles(); - System.Windows.Forms.Application.SetCompatibleTextRenderingDefault(false); - AllocConsole(); RedirectConsoleOutput(); #endif @@ -84,59 +97,7 @@ static class Program ServiceProvider = serviceCollection.BuildServiceProvider(); - var serviceProvider = ServiceProvider; - - // Determine UI based on command line. Default: WinForms. Use --wpf for WPF, --avalonia for Avalonia. - bool useWpf = args is not null && Array.Exists(args, a => string.Equals(a, "--wpf", StringComparison.OrdinalIgnoreCase)); - bool useAvalonia = args is not null && Array.Exists(args, a => string.Equals(a, "--avalonia", StringComparison.OrdinalIgnoreCase)); - - if (useAvalonia) - { - BuildAvaloniaApp().StartWithClassicDesktopLifetime(args ?? Array.Empty()); - return; - } - -#if WINDOWS - if (useWpf) - { - var wpfApp = new System.Windows.Application(); - try - { - wpfApp.Resources.MergedDictionaries.Add(new System.Windows.ResourceDictionary { Source = new Uri("pack://application:,,,/MahApps.Metro;component/Styles/Controls.xaml") }); - wpfApp.Resources.MergedDictionaries.Add(new System.Windows.ResourceDictionary { Source = new Uri("pack://application:,,,/MahApps.Metro;component/Styles/Fonts.xaml") }); - wpfApp.Resources.MergedDictionaries.Add(new System.Windows.ResourceDictionary { Source = new Uri("pack://application:,,,/MahApps.Metro;component/Styles/Themes/Light.Blue.xaml") }); - - try - { - ControlzEx.Theming.ThemeManager.Current.ChangeTheme(wpfApp, "Light.Blue"); - } - catch - { - // ignore if ThemeManager API isn't present - } - } - catch - { - // If resources fail to load, continue silently - } - - var wpfMain = serviceProvider.GetService(typeof(ImageCatalog_2.MainWindow)) as ImageCatalog_2.MainWindow; - if (wpfMain is not null) - { - wpfApp.Run(wpfMain); - return; - } - - // If WPF was requested but not available, fall through to WinForms. - } - - // Default / fallback to WinForms UI - var mainForm = serviceProvider.GetRequiredService(); - System.Windows.Forms.Application.Run(mainForm); -#else - // On non-Windows, Avalonia is the only available UI BuildAvaloniaApp().StartWithClassicDesktopLifetime(args ?? Array.Empty()); -#endif } private static void ConfigureServices(ServiceCollection services) @@ -189,11 +150,6 @@ static class Program services.AddTransient(); -#if WINDOWS - services.AddTransient(); - services.AddTransient(); -#endif - services.AddSingleton(); services.AddLogging(configure => diff --git a/imagecatalog/Properties/InternalsVisibleTo.cs b/imagecatalog/Properties/InternalsVisibleTo.cs new file mode 100644 index 0000000..d063896 --- /dev/null +++ b/imagecatalog/Properties/InternalsVisibleTo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("MaddoShared.Tests")] \ No newline at end of file diff --git a/imagecatalog/ViewModelBase.cs b/imagecatalog/ViewModelBase.cs index f14e44e..b999ba5 100644 --- a/imagecatalog/ViewModelBase.cs +++ b/imagecatalog/ViewModelBase.cs @@ -6,18 +6,12 @@ using System.Runtime.CompilerServices; using System.Text; using System.Threading; using System.Threading.Tasks; -#if WINDOWS -using System.Windows.Forms; -#endif namespace ImageCatalog_2 { public class ViewModelBase : INotifyPropertyChanged { private readonly SynchronizationContext? _synchronizationContext; -#if WINDOWS - private Control? _control; -#endif protected ViewModelBase() { @@ -25,19 +19,8 @@ namespace ImageCatalog_2 _synchronizationContext = SynchronizationContext.Current; } - /// - /// Set a Control to use for thread marshalling in WinForms applications. - /// This is required for proper cross-thread handling with data binding. - /// -#if WINDOWS - public void SetControl(Control control) - { - _control = control; - } -#endif - public event PropertyChangedEventHandler? PropertyChanged; - + // This method is called by the Set accessor of each property. // The CallerMemberName attribute that is applied to the optional propertyName // parameter causes the property name of the caller to be substituted as an argument. @@ -46,22 +29,6 @@ namespace ImageCatalog_2 if (PropertyChanged == null) return; -#if WINDOWS - // If we have a Control reference (WinForms), use Control.Invoke for proper marshalling - if (_control != null) - { - if (_control.InvokeRequired) - { - _control.Invoke(() => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName))); - } - else - { - PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); - } - } - // Fallback to SynchronizationContext if available - else -#endif if (_synchronizationContext != null && SynchronizationContext.Current != _synchronizationContext) { // We're on a different thread, marshal to the UI thread diff --git a/imagecatalog/ViewModels/AiSettingsViewModel.cs b/imagecatalog/ViewModels/AiSettingsViewModel.cs index 18878dd..461758f 100644 --- a/imagecatalog/ViewModels/AiSettingsViewModel.cs +++ b/imagecatalog/ViewModels/AiSettingsViewModel.cs @@ -60,6 +60,116 @@ public class AiSettingsViewModel : ViewModelBase } } + private bool _faceRecursive; + public bool FaceRecursive + { + get => _faceRecursive; + set + { + _faceRecursive = value; + NotifyPropertyChanged(); + } + } + + private bool _faceIncludeThumbnails; + public bool FaceIncludeThumbnails + { + get => _faceIncludeThumbnails; + set + { + _faceIncludeThumbnails = value; + NotifyPropertyChanged(); + } + } + + private int _faceParallelism = 3; + public int FaceParallelism + { + get => _faceParallelism; + set + { + _faceParallelism = value; + NotifyPropertyChanged(); + } + } + + private int _faceMinSize = 35; + public int FaceMinSize + { + get => _faceMinSize; + set + { + _faceMinSize = value; + NotifyPropertyChanged(); + } + } + + private bool _faceUpsample = true; + public bool FaceUpsample + { + get => _faceUpsample; + set + { + _faceUpsample = value; + NotifyPropertyChanged(); + } + } + + private bool _faceGpuOptionEnabled; + public bool FaceGpuOptionEnabled + { + get => _faceGpuOptionEnabled; + set + { + _faceGpuOptionEnabled = value; + NotifyPropertyChanged(); + } + } + + private bool _useFaceGpu; + public bool UseFaceGpu + { + get => _useFaceGpu; + set + { + _useFaceGpu = value; + NotifyPropertyChanged(); + } + } + + private bool _isFaceEncoderRunning; + public bool IsFaceEncoderRunning + { + get => _isFaceEncoderRunning; + set + { + _isFaceEncoderRunning = value; + NotifyPropertyChanged(); + } + } + + private string _faceStatusMessage = string.Empty; + public string FaceStatusMessage + { + get => _faceStatusMessage; + set + { + _faceStatusMessage = value; + NotifyPropertyChanged(); + } + } + + private string _faceCommandOutput = string.Empty; + public string FaceCommandOutput + { + get => _faceCommandOutput; + set + { + _faceCommandOutput = value; + NotifyPropertyChanged(); + } + } + private double _aiProgress; public double AiProgress {