diff --git a/.forgejo/workflows/build-windows-avalonia.yml b/.forgejo/workflows/build-windows-avalonia.yml index 4666242..3d3c18e 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-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) }} + NUGET_SOURCE_NAME: Nuget-GitLab-AIFotoONLUS + NUGET_SOURCE_URL: https://gitlab.com/api/v4/projects/79509532/packages/nuget/index.json jobs: build: runs-on: docker env: - FORGEJO_PACKAGE_USERNAME: ${{ secrets.FORGEJO_PACKAGE_USERNAME }} - FORGEJO_PACKAGE_TOKEN: ${{ secrets.FORGEJO_PACKAGE_TOKEN }} + NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} + NUGET_PASSWORD: ${{ secrets.NUGET_PASSWORD }} steps: - name: Checkout @@ -36,12 +36,12 @@ jobs: - name: Validate NuGet secrets run: | set -eu - if [ -z "${FORGEJO_PACKAGE_USERNAME}" ]; then - echo "secrets.FORGEJO_PACKAGE_USERNAME is required" + if [ -z "${NUGET_USERNAME}" ]; then + echo "secrets.NUGET_USERNAME is required" exit 1 fi - if [ -z "${FORGEJO_PACKAGE_TOKEN}" ]; then - echo "secrets.FORGEJO_PACKAGE_TOKEN is required" + if [ -z "${NUGET_PASSWORD}" ]; then + echo "secrets.NUGET_PASSWORD is required" exit 1 fi @@ -53,8 +53,8 @@ jobs: dotnet nuget update source "${{ env.NUGET_SOURCE_NAME }}" \ --source "${{ env.NUGET_SOURCE_URL }}" \ - --username "${FORGEJO_PACKAGE_USERNAME}" \ - --password "${FORGEJO_PACKAGE_TOKEN}" \ + --username "${NUGET_USERNAME}" \ + --password "${NUGET_PASSWORD}" \ --store-password-in-clear-text \ --configfile "${temp_config}" diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 3ce4bbe..3734723 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -4,7 +4,7 @@ ```powershell # Build -dotnet build Catalog.slnx +dotnet build Catalog.sln # 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 an Avalonia image cataloging application targeting .NET 10.0-windows. +This is a WinForms/WPF image cataloging application targeting .NET 10.0-windows. ### Projects | Project | Purpose | |---------|---------| -| **imagecatalog** | Main desktop application — Avalonia with Fluent theme (`AvaloniaMainWindow`) | +| **imagecatalog** | Main desktop application — WinForms (default), WPF (`--wpf`), or Avalonia (`--avalonia`) | | **MaddoShared** | Shared image processing library (the core) | | **MaddoShared.Tests** | Unit tests for MaddoShared | | **MaddoShared.Benchmarks** | BenchmarkDotNet performance benchmarks | @@ -35,7 +35,12 @@ This is an Avalonia image cataloging application targeting .NET 10.0-windows. | **ImageCatalogCS / ImageCatalogParallel** | Legacy/experimental variants | | **CatalogLib / CatalogLibVb / CatalogVbLib** | Legacy VB.NET libraries | -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`). +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`). ### Core Flow diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 0af4af3..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "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 deleted file mode 100644 index 22c598b..0000000 --- a/.vscode/tasks.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "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 deleted file mode 100644 index 15ec3a0..0000000 --- a/Catalog.code-workspace +++ /dev/null @@ -1,16 +0,0 @@ -{ - "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 new file mode 100644 index 0000000..5d1d09f --- /dev/null +++ b/Catalog.sln @@ -0,0 +1,116 @@ + +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 deleted file mode 100644 index 3c8cf36..0000000 --- a/Catalog.slnx +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/MaddoShared.Tests/DataModelCharacterizationTests.cs b/MaddoShared.Tests/DataModelCharacterizationTests.cs index cba6ce0..7ce601e 100644 --- a/MaddoShared.Tests/DataModelCharacterizationTests.cs +++ b/MaddoShared.Tests/DataModelCharacterizationTests.cs @@ -1,5 +1,4 @@ using System; -using System.IO; using System.Threading.Tasks; using ImageCatalog_2; using ImageCatalog_2.Services; @@ -135,61 +134,6 @@ 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) @@ -225,33 +169,4 @@ 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 d8936a9..1d562c8 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 e0e2d2b..79cf275 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -1,25 +1,25 @@ - + - + - - + + diff --git a/imagecatalog/AvaloniaMainWindow.axaml.cs b/imagecatalog/AvaloniaMainWindow.axaml.cs index 8ebe952..2de8d3f 100644 --- a/imagecatalog/AvaloniaMainWindow.axaml.cs +++ b/imagecatalog/AvaloniaMainWindow.axaml.cs @@ -3,7 +3,6 @@ using Avalonia.Interactivity; using Avalonia.Platform.Storage; using Avalonia.Styling; using Avalonia.Threading; -using System.ComponentModel; using System.IO; namespace ImageCatalog_2; @@ -21,7 +20,6 @@ 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); @@ -137,29 +135,6 @@ 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 134621c..7f7f3ae 100644 --- a/imagecatalog/AvaloniaViews/FaceAiTabView.axaml +++ b/imagecatalog/AvaloniaViews/FaceAiTabView.axaml @@ -5,16 +5,16 @@ - - + - - + + @@ -26,26 +26,6 @@ - - - - - - - - - - - - - - - - @@ -59,11 +39,11 @@ - - - @@ -74,36 +54,18 @@ - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/imagecatalog/MainWindow.xaml.cs b/imagecatalog/MainWindow.xaml.cs new file mode 100644 index 0000000..146ccb3 --- /dev/null +++ b/imagecatalog/MainWindow.xaml.cs @@ -0,0 +1,383 @@ +#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 4891981..7d56b15 100644 --- a/imagecatalog/Models/SettingsDto.cs +++ b/imagecatalog/Models/SettingsDto.cs @@ -282,26 +282,6 @@ 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 fb38a24..99fb2da 100644 --- a/imagecatalog/Program.cs +++ b/imagecatalog/Program.cs @@ -11,6 +11,7 @@ using Microsoft.Extensions.Logging.Console; using System.IO; using Microsoft.Extensions.Options; using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; namespace ImageCatalog_2; @@ -20,9 +21,6 @@ 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); @@ -38,12 +36,6 @@ 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, @@ -56,9 +48,6 @@ 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() { @@ -69,12 +58,6 @@ 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!; @@ -88,6 +71,10 @@ 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 @@ -97,7 +84,59 @@ 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) @@ -150,6 +189,11 @@ 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 deleted file mode 100644 index d063896..0000000 --- a/imagecatalog/Properties/InternalsVisibleTo.cs +++ /dev/null @@ -1,3 +0,0 @@ -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 b999ba5..f14e44e 100644 --- a/imagecatalog/ViewModelBase.cs +++ b/imagecatalog/ViewModelBase.cs @@ -6,12 +6,18 @@ 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() { @@ -19,8 +25,19 @@ namespace ImageCatalog_2 _synchronizationContext = SynchronizationContext.Current; } - public event PropertyChangedEventHandler? PropertyChanged; + /// + /// 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. @@ -29,6 +46,22 @@ 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 461758f..18878dd 100644 --- a/imagecatalog/ViewModels/AiSettingsViewModel.cs +++ b/imagecatalog/ViewModels/AiSettingsViewModel.cs @@ -60,116 +60,6 @@ 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 {