Compare commits
3 commits
daf3b5ad2c
...
25fdb82d2f
| Author | SHA1 | Date | |
|---|---|---|---|
| 25fdb82d2f | |||
| d6b778a648 | |||
| 988a3d94e1 |
26 changed files with 1338 additions and 4659 deletions
|
|
@ -14,15 +14,15 @@ env:
|
||||||
PROJECT_PATH: imagecatalog/ImageCatalog 2.csproj
|
PROJECT_PATH: imagecatalog/ImageCatalog 2.csproj
|
||||||
PUBLISH_DIR: artifacts/publish/win-x64
|
PUBLISH_DIR: artifacts/publish/win-x64
|
||||||
ARTIFACT_NAME: imagecatalog-windows-avalonia
|
ARTIFACT_NAME: imagecatalog-windows-avalonia
|
||||||
NUGET_SOURCE_NAME: Nuget-GitLab-AIFotoONLUS
|
NUGET_SOURCE_NAME: Nuget-Forgejo-AIFotoONLUS
|
||||||
NUGET_SOURCE_URL: https://gitlab.com/api/v4/projects/79509532/packages/nuget/index.json
|
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:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: docker
|
runs-on: docker
|
||||||
env:
|
env:
|
||||||
NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }}
|
FORGEJO_PACKAGE_USERNAME: ${{ secrets.FORGEJO_PACKAGE_USERNAME }}
|
||||||
NUGET_PASSWORD: ${{ secrets.NUGET_PASSWORD }}
|
FORGEJO_PACKAGE_TOKEN: ${{ secrets.FORGEJO_PACKAGE_TOKEN }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
|
|
@ -36,12 +36,12 @@ jobs:
|
||||||
- name: Validate NuGet secrets
|
- name: Validate NuGet secrets
|
||||||
run: |
|
run: |
|
||||||
set -eu
|
set -eu
|
||||||
if [ -z "${NUGET_USERNAME}" ]; then
|
if [ -z "${FORGEJO_PACKAGE_USERNAME}" ]; then
|
||||||
echo "secrets.NUGET_USERNAME is required"
|
echo "secrets.FORGEJO_PACKAGE_USERNAME is required"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
if [ -z "${NUGET_PASSWORD}" ]; then
|
if [ -z "${FORGEJO_PACKAGE_TOKEN}" ]; then
|
||||||
echo "secrets.NUGET_PASSWORD is required"
|
echo "secrets.FORGEJO_PACKAGE_TOKEN is required"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -53,8 +53,8 @@ jobs:
|
||||||
|
|
||||||
dotnet nuget update source "${{ env.NUGET_SOURCE_NAME }}" \
|
dotnet nuget update source "${{ env.NUGET_SOURCE_NAME }}" \
|
||||||
--source "${{ env.NUGET_SOURCE_URL }}" \
|
--source "${{ env.NUGET_SOURCE_URL }}" \
|
||||||
--username "${NUGET_USERNAME}" \
|
--username "${FORGEJO_PACKAGE_USERNAME}" \
|
||||||
--password "${NUGET_PASSWORD}" \
|
--password "${FORGEJO_PACKAGE_TOKEN}" \
|
||||||
--store-password-in-clear-text \
|
--store-password-in-clear-text \
|
||||||
--configfile "${temp_config}"
|
--configfile "${temp_config}"
|
||||||
|
|
||||||
|
|
|
||||||
13
.github/copilot-instructions.md
vendored
13
.github/copilot-instructions.md
vendored
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Build
|
# Build
|
||||||
dotnet build Catalog.sln
|
dotnet build Catalog.slnx
|
||||||
|
|
||||||
# Run all tests
|
# Run all tests
|
||||||
dotnet test MaddoShared.Tests
|
dotnet test MaddoShared.Tests
|
||||||
|
|
@ -21,13 +21,13 @@ dotnet publish "imagecatalog\ImageCatalog 2.csproj" -c Release -r win-x64 --self
|
||||||
|
|
||||||
## Architecture
|
## 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
|
### Projects
|
||||||
|
|
||||||
| Project | Purpose |
|
| 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** | Shared image processing library (the core) |
|
||||||
| **MaddoShared.Tests** | Unit tests for MaddoShared |
|
| **MaddoShared.Tests** | Unit tests for MaddoShared |
|
||||||
| **MaddoShared.Benchmarks** | BenchmarkDotNet performance benchmarks |
|
| **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 |
|
| **ImageCatalogCS / ImageCatalogParallel** | Legacy/experimental variants |
|
||||||
| **CatalogLib / CatalogLibVb / CatalogVbLib** | Legacy VB.NET libraries |
|
| **CatalogLib / CatalogLibVb / CatalogVbLib** | Legacy VB.NET libraries |
|
||||||
|
|
||||||
The main app selects its UI at startup via command-line flag:
|
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`).
|
||||||
- *(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
|
### Core Flow
|
||||||
|
|
||||||
|
|
|
||||||
16
.vscode/launch.json
vendored
Normal file
16
.vscode/launch.json
vendored
Normal file
|
|
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
18
.vscode/tasks.json
vendored
Normal file
18
.vscode/tasks.json
vendored
Normal file
|
|
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
16
Catalog.code-workspace
Normal file
16
Catalog.code-workspace
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"path": "."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../AIFotoONLUS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../../various/regalamiunsorriso"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {
|
||||||
|
"commentTranslate.hover.enabled": false
|
||||||
|
}
|
||||||
|
}
|
||||||
116
Catalog.sln
116
Catalog.sln
|
|
@ -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
|
|
||||||
20
Catalog.slnx
Normal file
20
Catalog.slnx
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
<Solution>
|
||||||
|
<Configurations>
|
||||||
|
<Platform Name="Any CPU" />
|
||||||
|
<Platform Name="x64" />
|
||||||
|
<Platform Name="x86" />
|
||||||
|
</Configurations>
|
||||||
|
<Folder Name="/Libraries/">
|
||||||
|
<Project Path="Catalog.Communication/Catalog.Communication.csproj" />
|
||||||
|
<Project Path="MaddoShared/MaddoShared.csproj" />
|
||||||
|
</Folder>
|
||||||
|
<Folder Name="/Tests/">
|
||||||
|
<Project Path="MaddoShared.Tests/MaddoShared.Tests.csproj" />
|
||||||
|
</Folder>
|
||||||
|
<Project Path="imagecatalog/ImageCatalog 2.csproj">
|
||||||
|
<Platform Solution="*|x86" Project="x86" />
|
||||||
|
<Platform Solution="Release|x64" Project="x64" />
|
||||||
|
</Project>
|
||||||
|
<Project Path="MaddoShared.Benchmarks/MaddoShared.Benchmarks.csproj" />
|
||||||
|
<Project Path="MaddoShared.ImageSharpTests/MaddoShared.ImageSharpTests.csproj" />
|
||||||
|
</Solution>
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.IO;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using ImageCatalog_2;
|
using ImageCatalog_2;
|
||||||
using ImageCatalog_2.Services;
|
using ImageCatalog_2.Services;
|
||||||
|
|
@ -134,6 +135,61 @@ public class DataModelCharacterizationTests
|
||||||
model.FontSize.ShouldBe(42);
|
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(
|
private static DataModel CreateModel(
|
||||||
ISettingsService? settingsService = null,
|
ISettingsService? settingsService = null,
|
||||||
ITestService? testService = null)
|
ITestService? testService = null)
|
||||||
|
|
@ -169,4 +225,33 @@ public class DataModelCharacterizationTests
|
||||||
Substitute.For<ILogger<DataModel>>(),
|
Substitute.For<ILogger<DataModel>>(),
|
||||||
versionProvider: null);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.3" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.5" />
|
||||||
<PackageReference Include="System.Drawing.Common" Version="10.0.3" />
|
<PackageReference Include="System.Drawing.Common" Version="10.0.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
|
||||||
14
NuGet.Config
14
NuGet.Config
|
|
@ -1,25 +1,25 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!--
|
<!--
|
||||||
Repository-level NuGet.Config to control which sources are queried for which
|
Repository-level NuGet.Config to control which sources are queried for which
|
||||||
packages. This prevents the GitLab feed from being queried for all package
|
packages. This prevents the Forgejo feed from being queried for all package
|
||||||
IDs (which causes 401 errors for packages hosted on nuget.org).
|
IDs (which causes 401 errors for packages hosted on nuget.org).
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
- Keep this file in the repository root so `dotnet restore` picks it up by default.
|
- Keep this file in the repository root so `dotnet restore` picks it up by default.
|
||||||
- CI still needs to add the `GitLab` source credentials at runtime (we do this
|
- CI still needs to add the `Forgejo` source credentials at runtime (we do this
|
||||||
from the pipeline using `dotnet nuget add source ...`), but the mapping below
|
from the pipeline using `dotnet nuget add source ...`), but the mapping below
|
||||||
ensures only the listed package ID patterns are requested from the GitLab feed.
|
ensures only the listed package ID patterns are requested from the Forgejo feed.
|
||||||
-->
|
-->
|
||||||
<configuration>
|
<configuration>
|
||||||
<packageSources>
|
<packageSources>
|
||||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
||||||
<add key="Nuget-GitLab-AIFotoONLUS" value="https://gitlab.com/api/v4/projects/79509532/packages/nuget/index.json" />
|
<add key="Nuget-Forgejo-AIFotoONLUS" value="https://forgejo.maddoscientisto.net/api/packages/maddo/nuget/index.json" />
|
||||||
</packageSources>
|
</packageSources>
|
||||||
|
|
||||||
<!-- Map private package IDs to the GitLab source; everything else uses nuget.org -->
|
<!-- Map private package IDs to the Forgejo source; everything else uses nuget.org -->
|
||||||
<packageSourceMapping>
|
<packageSourceMapping>
|
||||||
<packageSource key="Nuget-GitLab-AIFotoONLUS">
|
<packageSource key="Nuget-Forgejo-AIFotoONLUS">
|
||||||
<!-- Add patterns for your private packages hosted in GitLab -->
|
<!-- Add patterns for your private packages hosted in Forgejo -->
|
||||||
<package pattern="AIFotoONLUS.*" />
|
<package pattern="AIFotoONLUS.*" />
|
||||||
<package pattern="AIFotoONLUS.Core" />
|
<package pattern="AIFotoONLUS.Core" />
|
||||||
</packageSource>
|
</packageSource>
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ using Avalonia.Interactivity;
|
||||||
using Avalonia.Platform.Storage;
|
using Avalonia.Platform.Storage;
|
||||||
using Avalonia.Styling;
|
using Avalonia.Styling;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
|
using System.ComponentModel;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
|
||||||
namespace ImageCatalog_2;
|
namespace ImageCatalog_2;
|
||||||
|
|
@ -20,6 +21,7 @@ public partial class AvaloniaMainWindow : Window
|
||||||
DataContext = _model;
|
DataContext = _model;
|
||||||
|
|
||||||
Opened += (_, _) => SyncThemeStateFromCurrentTheme();
|
Opened += (_, _) => SyncThemeStateFromCurrentTheme();
|
||||||
|
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);
|
||||||
|
|
@ -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)
|
private void ToggleTheme_Click(object? sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
_isDarkTheme = !_isDarkTheme;
|
_isDarkTheme = !_isDarkTheme;
|
||||||
|
|
|
||||||
|
|
@ -5,16 +5,16 @@
|
||||||
<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 face_encoder.exe usando la cartella Destinazione corrente come --images e un file .pkl come --out."
|
<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="Eseguibile" FontWeight="Bold" Margin="0,4,0,0" />
|
<TextBlock Text="Cartella Face Encoder" FontWeight="Bold" Margin="0,4,0,0" />
|
||||||
<Grid ColumnDefinitions="Auto,*,Auto,Auto" ColumnSpacing="6">
|
<Grid ColumnDefinitions="Auto,*,Auto,Auto" ColumnSpacing="6">
|
||||||
<TextBlock Grid.Column="0" Text="face_encoder:" VerticalAlignment="Center" />
|
<TextBlock Grid.Column="0" Text="Percorso:" VerticalAlignment="Center" />
|
||||||
<TextBox Grid.Column="1" Name="FaceExecutablePathTextBox" Text="{Binding FaceExecutablePath, Mode=TwoWay}" Watermark="C:\\tools\\face_encoder.exe" />
|
<TextBox Grid.Column="1" Name="FaceExecutablePathTextBox" Text="{Binding FaceExecutablePath, Mode=TwoWay}" Watermark="C:\tools\Face_Recognition_Windows" />
|
||||||
<Button Grid.Column="2" Name="FaceSelectExecutableButton" Click="SelectFaceExecutable_Click" Width="104" Margin="6,0,0,0">
|
<Button Grid.Column="2" Name="FaceSelectExecutableButton" Click="SelectFaceExecutable_Click" Width="104" Margin="6,0,0,0">
|
||||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
|
||||||
<iconPacks:PackIconMaterial Kind="FileOutline" Width="14" Height="14" />
|
<iconPacks:PackIconMaterial Kind="FolderOutline" Width="14" Height="14" />
|
||||||
<TextBlock Text="Scegli..." />
|
<TextBlock Text="Scegli..." />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -26,6 +26,26 @@
|
||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</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"
|
||||||
|
IsChecked="{Binding UseFaceGpu, Mode=TwoWay}"
|
||||||
|
IsEnabled="{Binding FaceGpuOptionEnabled}" />
|
||||||
|
</StackPanel>
|
||||||
|
<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."
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
Opacity="0.75" />
|
||||||
|
|
||||||
|
<Grid ColumnDefinitions="Auto,120,Auto,120,*" ColumnSpacing="6">
|
||||||
|
<TextBlock Grid.Column="0" Text="Parallelismo:" VerticalAlignment="Center" />
|
||||||
|
<ComboBox Grid.Column="1" ItemsSource="{Binding FaceParallelismOptions}" SelectedItem="{Binding FaceParallelism, Mode=TwoWay}" />
|
||||||
|
<TextBlock Grid.Column="2" Text="Min size:" VerticalAlignment="Center" />
|
||||||
|
<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" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
<Grid ColumnDefinitions="Auto,*,Auto,Auto" ColumnSpacing="6">
|
<Grid ColumnDefinitions="Auto,*,Auto,Auto" ColumnSpacing="6">
|
||||||
<TextBlock Grid.Column="0" Text="Sorgente:" VerticalAlignment="Center" />
|
<TextBlock Grid.Column="0" Text="Sorgente:" VerticalAlignment="Center" />
|
||||||
<TextBox Grid.Column="1" Name="FaceDestinationPathTextBox" Text="{Binding DestinationPath, Mode=OneWay}" IsReadOnly="True" />
|
<TextBox Grid.Column="1" Name="FaceDestinationPathTextBox" Text="{Binding DestinationPath, Mode=OneWay}" IsReadOnly="True" />
|
||||||
|
|
@ -39,11 +59,11 @@
|
||||||
|
|
||||||
<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">
|
<Grid ColumnDefinitions="Auto,*,Auto,Auto" ColumnSpacing="6">
|
||||||
<TextBlock Grid.Column="0" Text="File out (.pkl):" VerticalAlignment="Center" />
|
<TextBlock Grid.Column="0" Text="Cartella out:" VerticalAlignment="Center" />
|
||||||
<TextBox Grid.Column="1" Name="FaceOutputFolderTextBox" Text="{Binding FaceOutputFolderPath, Mode=TwoWay}" Watermark="C:\\output\\encodings.pkl" />
|
<TextBox Grid.Column="1" Name="FaceOutputFolderTextBox" Text="{Binding FaceOutputFolderPath, Mode=TwoWay}" Watermark="C:\output\face_encoder" />
|
||||||
<Button Grid.Column="2" Name="FaceSelectOutputButton" Click="SelectFaceOutputFile_Click" Width="104" Margin="6,0,0,0">
|
<Button Grid.Column="2" Name="FaceSelectOutputButton" Click="SelectFaceOutputFolder_Click" Width="104" Margin="6,0,0,0">
|
||||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
|
||||||
<iconPacks:PackIconMaterial Kind="FileOutline" Width="14" Height="14" />
|
<iconPacks:PackIconMaterial Kind="FolderOutline" Width="14" Height="14" />
|
||||||
<TextBlock Text="Scegli..." />
|
<TextBlock Text="Scegli..." />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -54,18 +74,36 @@
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
<TextBlock Text="I file vengono creati come face_encodings_yyyyMMdd_HHmmss_nomecartella.pkl e encoder_log_yyyyMMdd_HHmmss_nomecartella.txt."
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
Opacity="0.75" />
|
||||||
|
|
||||||
<StackPanel Orientation="Horizontal" Spacing="6" Margin="0,6,0,0">
|
<StackPanel Orientation="Horizontal" Spacing="6" Margin="0,6,0,0">
|
||||||
<Button Name="FaceRunButton" Content="Esegui Face Encoder" Click="RunFaceEncoder_Click" />
|
<Button Name="FaceRunButton" Command="{Binding StartFaceEncoderCommand}">
|
||||||
<TextBlock Name="FaceStatusTextBlock" VerticalAlignment="Center" />
|
<StackPanel Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
|
||||||
|
<ProgressBar Width="18"
|
||||||
|
Height="18"
|
||||||
|
IsIndeterminate="True"
|
||||||
|
IsVisible="{Binding IsFaceEncoderRunning}"
|
||||||
|
ShowProgressText="False"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
<TextBlock Text="Esegui Face Encoder" VerticalAlignment="Center" />
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
<Button Content="Stop" Command="{Binding StopFaceEncoderCommand}" />
|
||||||
|
<TextBlock VerticalAlignment="Center" Text="{Binding FaceStatusMessage}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<TextBlock Text="Output comando" FontWeight="Bold" Margin="0,6,0,0" />
|
<TextBlock Text="Output comando" FontWeight="Bold" Margin="0,6,0,0" />
|
||||||
<TextBox Name="FaceOutputTextBox"
|
<TextBox Name="FaceOutputTextBox"
|
||||||
|
Text="{Binding FaceCommandOutput}"
|
||||||
IsReadOnly="True"
|
IsReadOnly="True"
|
||||||
AcceptsReturn="True"
|
AcceptsReturn="True"
|
||||||
TextWrapping="Wrap"
|
TextWrapping="Wrap"
|
||||||
MinHeight="180" />
|
FontFamily="Cascadia Mono, Consolas, monospace"
|
||||||
|
Height="220"
|
||||||
|
ScrollViewer.VerticalScrollBarVisibility="Auto"
|
||||||
|
ScrollViewer.HorizontalScrollBarVisibility="Auto" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,56 @@
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
using Avalonia.Platform.Storage;
|
using Avalonia.Platform.Storage;
|
||||||
using Microsoft.Extensions.Logging;
|
using Avalonia.Threading;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.ComponentModel;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace ImageCatalog_2.AvaloniaViews;
|
namespace ImageCatalog_2.AvaloniaViews;
|
||||||
|
|
||||||
public partial class FaceAiTabView : Avalonia.Controls.UserControl
|
public partial class FaceAiTabView : Avalonia.Controls.UserControl
|
||||||
{
|
{
|
||||||
private readonly ILogger<FaceAiTabView> _logger;
|
private INotifyPropertyChanged? _faceAiPropertySource;
|
||||||
|
|
||||||
public FaceAiTabView()
|
public FaceAiTabView()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
_logger = Program.ServiceProvider.GetService(typeof(ILogger<FaceAiTabView>)) as ILogger<FaceAiTabView>
|
DataContextChanged += OnDataContextChanged;
|
||||||
?? NullLogger<FaceAiTabView>.Instance;
|
}
|
||||||
|
|
||||||
|
private void OnDataContextChanged(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (_faceAiPropertySource is not null)
|
||||||
|
{
|
||||||
|
_faceAiPropertySource.PropertyChanged -= OnFaceAiPropertyChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
_faceAiPropertySource = DataContext as INotifyPropertyChanged;
|
||||||
|
if (_faceAiPropertySource is not null)
|
||||||
|
{
|
||||||
|
_faceAiPropertySource.PropertyChanged += OnFaceAiPropertyChanged;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnFaceAiPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (!string.Equals(e.PropertyName, nameof(DataModel.FaceCommandOutput), StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var outputBox = this.FindControl<Avalonia.Controls.TextBox>("FaceOutputTextBox");
|
||||||
|
if (outputBox is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Dispatcher.UIThread.Post(() =>
|
||||||
|
{
|
||||||
|
var textLength = outputBox.Text?.Length ?? 0;
|
||||||
|
outputBox.CaretIndex = textLength;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void SelectFaceExecutable_Click(object? sender, RoutedEventArgs e)
|
private async void SelectFaceExecutable_Click(object? sender, RoutedEventArgs e)
|
||||||
|
|
@ -37,19 +68,14 @@ public partial class FaceAiTabView : Avalonia.Controls.UserControl
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var files = await storageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
|
var folders = await storageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
|
||||||
{
|
{
|
||||||
Title = "Seleziona face_encoder.exe",
|
Title = "Seleziona la cartella Face Recognition Windows"
|
||||||
FileTypeFilter =
|
|
||||||
[
|
|
||||||
new FilePickerFileType("Eseguibile") { Patterns = ["*.exe"] },
|
|
||||||
new FilePickerFileType("Tutti i file") { Patterns = ["*.*"] }
|
|
||||||
]
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (files.Count > 0)
|
if (folders.Count > 0)
|
||||||
{
|
{
|
||||||
executableBox.Text = files[0].Path.LocalPath;
|
executableBox.Text = folders[0].Path.LocalPath;
|
||||||
if (DataContext is DataModel model)
|
if (DataContext is DataModel model)
|
||||||
{
|
{
|
||||||
model.FaceExecutablePath = executableBox.Text;
|
model.FaceExecutablePath = executableBox.Text;
|
||||||
|
|
@ -57,7 +83,7 @@ public partial class FaceAiTabView : Avalonia.Controls.UserControl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void SelectFaceOutputFile_Click(object? sender, RoutedEventArgs e)
|
private async void SelectFaceOutputFolder_Click(object? sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
var outputBox = this.FindControl<Avalonia.Controls.TextBox>("FaceOutputFolderTextBox");
|
var outputBox = this.FindControl<Avalonia.Controls.TextBox>("FaceOutputFolderTextBox");
|
||||||
if (outputBox is null)
|
if (outputBox is null)
|
||||||
|
|
@ -72,21 +98,14 @@ public partial class FaceAiTabView : Avalonia.Controls.UserControl
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var files = await storageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
|
var folders = await storageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
|
||||||
{
|
{
|
||||||
Title = "Seleziona file output encodings (.pkl)",
|
Title = "Seleziona la cartella output per encodings e log"
|
||||||
SuggestedFileName = "encodings.pkl",
|
|
||||||
DefaultExtension = "pkl",
|
|
||||||
FileTypeChoices =
|
|
||||||
[
|
|
||||||
new FilePickerFileType("Pickle file") { Patterns = ["*.pkl"] }
|
|
||||||
],
|
|
||||||
ShowOverwritePrompt = true
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (files is not null)
|
if (folders.Count > 0)
|
||||||
{
|
{
|
||||||
outputBox.Text = files.Path.LocalPath;
|
outputBox.Text = folders[0].Path.LocalPath;
|
||||||
if (DataContext is DataModel model)
|
if (DataContext is DataModel model)
|
||||||
{
|
{
|
||||||
model.FaceOutputFolderPath = outputBox.Text;
|
model.FaceOutputFolderPath = outputBox.Text;
|
||||||
|
|
@ -108,6 +127,12 @@ public partial class FaceAiTabView : Avalonia.Controls.UserControl
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Directory.Exists(path))
|
||||||
|
{
|
||||||
|
OpenInExplorer(path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (File.Exists(path))
|
if (File.Exists(path))
|
||||||
{
|
{
|
||||||
OpenInExplorer(path);
|
OpenInExplorer(path);
|
||||||
|
|
@ -132,6 +157,12 @@ public partial class FaceAiTabView : Avalonia.Controls.UserControl
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Directory.Exists(outputPath))
|
||||||
|
{
|
||||||
|
OpenInExplorer(outputPath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (File.Exists(outputPath))
|
if (File.Exists(outputPath))
|
||||||
{
|
{
|
||||||
OpenInExplorer(outputPath);
|
OpenInExplorer(outputPath);
|
||||||
|
|
@ -156,171 +187,16 @@ public partial class FaceAiTabView : Avalonia.Controls.UserControl
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Directory.Exists(path))
|
||||||
|
{
|
||||||
|
OpenInExplorer(path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var directory = Path.GetDirectoryName(path);
|
var directory = Path.GetDirectoryName(path);
|
||||||
OpenInExplorer(string.IsNullOrWhiteSpace(directory) ? path : directory);
|
OpenInExplorer(string.IsNullOrWhiteSpace(directory) ? path : directory);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void RunFaceEncoder_Click(object? sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
var executableBox = this.FindControl<Avalonia.Controls.TextBox>("FaceExecutablePathTextBox");
|
|
||||||
var outputFolderBox = this.FindControl<Avalonia.Controls.TextBox>("FaceOutputFolderTextBox");
|
|
||||||
var outputLogBox = this.FindControl<Avalonia.Controls.TextBox>("FaceOutputTextBox");
|
|
||||||
var statusBlock = this.FindControl<TextBlock>("FaceStatusTextBlock");
|
|
||||||
var runButton = this.FindControl<Avalonia.Controls.Button>("FaceRunButton");
|
|
||||||
|
|
||||||
if (executableBox is null || outputFolderBox is null || outputLogBox is null || statusBlock is null || runButton is null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (DataContext is not DataModel model)
|
|
||||||
{
|
|
||||||
statusBlock.Text = "DataContext non valido.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var executablePath = executableBox.Text?.Trim().Trim('"') ?? string.Empty;
|
|
||||||
var outputFilePath = outputFolderBox.Text?.Trim().Trim('"') ?? string.Empty;
|
|
||||||
var imagesFolder = (model.DestinationPath ?? string.Empty).Trim().Trim('"');
|
|
||||||
|
|
||||||
model.FaceExecutablePath = executablePath;
|
|
||||||
model.FaceOutputFolderPath = outputFilePath;
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(executablePath) || !File.Exists(executablePath))
|
|
||||||
{
|
|
||||||
statusBlock.Text = "Percorso eseguibile non valido.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(imagesFolder) || !Directory.Exists(imagesFolder))
|
|
||||||
{
|
|
||||||
statusBlock.Text = "Cartella Destinazione non valida.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(outputFilePath))
|
|
||||||
{
|
|
||||||
statusBlock.Text = "Inserisci il file di output .pkl.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.Equals(Path.GetExtension(outputFilePath), ".pkl", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
statusBlock.Text = "Il file di output deve avere estensione .pkl.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var outputDirectory = Path.GetDirectoryName(outputFilePath);
|
|
||||||
if (!string.IsNullOrWhiteSpace(outputDirectory))
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory(outputDirectory);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Unable to create face output directory for file: {OutputFilePath}", outputFilePath);
|
|
||||||
statusBlock.Text = "Impossibile creare la cartella del file di output.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
runButton.IsEnabled = false;
|
|
||||||
statusBlock.Text = "Esecuzione face encoder in corso...";
|
|
||||||
outputLogBox.Text = string.Empty;
|
|
||||||
|
|
||||||
var outputLines = new StringBuilder();
|
|
||||||
var errorLines = new StringBuilder();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var imagesFolderArg = NormalizeDirectoryPathArgument(imagesFolder);
|
|
||||||
var outputFileArg = NormalizeFilePathArgument(outputFilePath);
|
|
||||||
|
|
||||||
var processStartInfo = new ProcessStartInfo
|
|
||||||
{
|
|
||||||
FileName = executablePath,
|
|
||||||
WorkingDirectory = Path.GetDirectoryName(executablePath) ?? Environment.CurrentDirectory,
|
|
||||||
UseShellExecute = false,
|
|
||||||
RedirectStandardOutput = true,
|
|
||||||
RedirectStandardError = true,
|
|
||||||
CreateNoWindow = true,
|
|
||||||
};
|
|
||||||
processStartInfo.ArgumentList.Add("--images");
|
|
||||||
processStartInfo.ArgumentList.Add(imagesFolderArg);
|
|
||||||
processStartInfo.ArgumentList.Add("--out");
|
|
||||||
processStartInfo.ArgumentList.Add(outputFileArg);
|
|
||||||
|
|
||||||
using var process = new Process { StartInfo = processStartInfo, EnableRaisingEvents = true };
|
|
||||||
process.OutputDataReceived += (_, args) =>
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(args.Data))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (outputLines)
|
|
||||||
{
|
|
||||||
outputLines.AppendLine(args.Data);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
process.ErrorDataReceived += (_, args) =>
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(args.Data))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (errorLines)
|
|
||||||
{
|
|
||||||
errorLines.AppendLine(args.Data);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!process.Start())
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Avvio face_encoder.exe fallito.");
|
|
||||||
}
|
|
||||||
|
|
||||||
process.BeginOutputReadLine();
|
|
||||||
process.BeginErrorReadLine();
|
|
||||||
await process.WaitForExitAsync().ConfigureAwait(true);
|
|
||||||
|
|
||||||
var summary = new StringBuilder();
|
|
||||||
summary.AppendLine($"Exit code: {process.ExitCode}");
|
|
||||||
|
|
||||||
if (outputLines.Length > 0)
|
|
||||||
{
|
|
||||||
summary.AppendLine();
|
|
||||||
summary.AppendLine("STDOUT:");
|
|
||||||
summary.Append(outputLines);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (errorLines.Length > 0)
|
|
||||||
{
|
|
||||||
summary.AppendLine();
|
|
||||||
summary.AppendLine("STDERR:");
|
|
||||||
summary.Append(errorLines);
|
|
||||||
}
|
|
||||||
|
|
||||||
outputLogBox.Text = summary.ToString();
|
|
||||||
statusBlock.Text = process.ExitCode == 0
|
|
||||||
? "Face encoder completato."
|
|
||||||
: $"Face encoder terminato con errore (code {process.ExitCode}).";
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Face encoder execution failed.");
|
|
||||||
outputLogBox.Text = ex.ToString();
|
|
||||||
statusBlock.Text = "Errore durante esecuzione face encoder.";
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
runButton.IsEnabled = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void OpenInExplorer(string? path)
|
private static void OpenInExplorer(string? path)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(path))
|
if (string.IsNullOrWhiteSpace(path))
|
||||||
|
|
@ -345,31 +221,4 @@ public partial class FaceAiTabView : Avalonia.Controls.UserControl
|
||||||
// Ignore failures when opening Explorer.
|
// Ignore failures when opening Explorer.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string NormalizeDirectoryPathArgument(string value)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(value))
|
|
||||||
{
|
|
||||||
return string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
var normalized = value.Trim().Trim('"');
|
|
||||||
var root = Path.GetPathRoot(normalized);
|
|
||||||
if (!string.IsNullOrEmpty(root) && normalized.Length > root.Length)
|
|
||||||
{
|
|
||||||
normalized = normalized.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
|
||||||
}
|
|
||||||
|
|
||||||
return normalized;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string NormalizeFilePathArgument(string value)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(value))
|
|
||||||
{
|
|
||||||
return string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
return value.Trim().Trim('"');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Windows.Forms;
|
|
||||||
using System.Windows.Input;
|
|
||||||
|
|
||||||
namespace ImageCatalog_2.Commands
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Helper class to bind WinForms Button controls to ICommand implementations
|
|
||||||
/// </summary>
|
|
||||||
public static class ButtonCommandBinder
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Binds a Button's Click event to an ICommand
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="button">The button to bind</param>
|
|
||||||
/// <param name="command">The command to execute</param>
|
|
||||||
/// <param name="commandParameter">Optional parameter to pass to the command</param>
|
|
||||||
public static void BindCommand(this Button button, ICommand command, object commandParameter = null)
|
|
||||||
{
|
|
||||||
if (button == null) throw new ArgumentNullException(nameof(button));
|
|
||||||
if (command == null) throw new ArgumentNullException(nameof(command));
|
|
||||||
|
|
||||||
// Wire up the Click event to execute the command
|
|
||||||
button.Click += (sender, e) =>
|
|
||||||
{
|
|
||||||
if (command.CanExecute(commandParameter))
|
|
||||||
{
|
|
||||||
command.Execute(commandParameter);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Wire up CanExecuteChanged to enable/disable the button
|
|
||||||
command.CanExecuteChanged += (sender, e) =>
|
|
||||||
{
|
|
||||||
button.Enabled = command.CanExecute(commandParameter);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Set initial enabled state
|
|
||||||
button.Enabled = command.CanExecute(commandParameter);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Binds multiple buttons to commands at once
|
|
||||||
/// </summary>
|
|
||||||
public static void BindCommands(params (Button button, ICommand command, object parameter)[] bindings)
|
|
||||||
{
|
|
||||||
foreach (var (button, command, parameter) in bindings)
|
|
||||||
{
|
|
||||||
button.BindCommand(command, parameter);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -9,14 +9,12 @@ using System.Diagnostics;
|
||||||
#if WINDOWS
|
#if WINDOWS
|
||||||
using System.Drawing.Text;
|
using System.Drawing.Text;
|
||||||
#endif
|
#endif
|
||||||
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
#if WINDOWS
|
|
||||||
using System.Windows.Forms;
|
|
||||||
#endif
|
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using MaddoShared;
|
using MaddoShared;
|
||||||
|
|
@ -40,6 +38,8 @@ namespace ImageCatalog_2
|
||||||
public ICommand SelectModelsFolderCommand { get; }
|
public ICommand SelectModelsFolderCommand { get; }
|
||||||
public ICommand SelectCsvOutputCommand { get; }
|
public ICommand SelectCsvOutputCommand { get; }
|
||||||
public ICommand StartAiCommand { get; }
|
public ICommand StartAiCommand { get; }
|
||||||
|
public ICommand StartFaceEncoderCommand { get; }
|
||||||
|
public ICommand StopFaceEncoderCommand { get; }
|
||||||
|
|
||||||
private readonly ITestService _service;
|
private readonly ITestService _service;
|
||||||
private readonly ILogger<DataModel> _logger;
|
private readonly ILogger<DataModel> _logger;
|
||||||
|
|
@ -54,6 +54,15 @@ namespace ImageCatalog_2
|
||||||
private readonly VisualSettingsViewModel _visual;
|
private readonly VisualSettingsViewModel _visual;
|
||||||
private readonly PicSettings _picSettings;
|
private readonly PicSettings _picSettings;
|
||||||
private readonly IMapper _mapper;
|
private readonly IMapper _mapper;
|
||||||
|
private readonly AsyncCommand _startFaceEncoderCommand;
|
||||||
|
private readonly AsyncCommand _stopFaceEncoderCommand;
|
||||||
|
private readonly object _faceEncoderProcessLock = new();
|
||||||
|
private Process? _faceEncoderProcess;
|
||||||
|
private CancellationTokenSource? _faceEncoderWatcherTokenSource;
|
||||||
|
private Task? _faceEncoderWatcherTask;
|
||||||
|
private CancellationTokenSource? _faceEncoderLogWatcherTokenSource;
|
||||||
|
private Task? _faceEncoderLogWatcherTask;
|
||||||
|
private bool _hasStartedFaceEncoderInSession;
|
||||||
|
|
||||||
// ComboBox collections
|
// ComboBox collections
|
||||||
public List<string> AvailableFonts { get; }
|
public List<string> AvailableFonts { get; }
|
||||||
|
|
@ -93,6 +102,10 @@ namespace ImageCatalog_2
|
||||||
SelectModelsFolderCommand = new RelayCommand(SelectModelsFolder);
|
SelectModelsFolderCommand = new RelayCommand(SelectModelsFolder);
|
||||||
SelectCsvOutputCommand = new RelayCommand(SelectCsvOutput);
|
SelectCsvOutputCommand = new RelayCommand(SelectCsvOutput);
|
||||||
StartAiCommand = new AsyncCommand(StartAiAsync);
|
StartAiCommand = new AsyncCommand(StartAiAsync);
|
||||||
|
_startFaceEncoderCommand = new AsyncCommand(RunFaceEncoderAsync, CanRunFaceEncoder);
|
||||||
|
_stopFaceEncoderCommand = new AsyncCommand(() => StopFaceEncoderAsync("Arresto richiesto dall'utente."), CanStopFaceEncoder);
|
||||||
|
StartFaceEncoderCommand = _startFaceEncoderCommand;
|
||||||
|
StopFaceEncoderCommand = _stopFaceEncoderCommand;
|
||||||
|
|
||||||
SelectSourceFolderCommand = new RelayCommand(SelectSourceFolder);
|
SelectSourceFolderCommand = new RelayCommand(SelectSourceFolder);
|
||||||
SelectDestinationFolderCommand = new RelayCommand(SelectDestinationFolder);
|
SelectDestinationFolderCommand = new RelayCommand(SelectDestinationFolder);
|
||||||
|
|
@ -104,6 +117,7 @@ namespace ImageCatalog_2
|
||||||
|
|
||||||
// Load available fonts
|
// Load available fonts
|
||||||
AvailableFonts = LoadAvailableFonts();
|
AvailableFonts = LoadAvailableFonts();
|
||||||
|
RefreshFaceExecutableCapabilities();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task StartAiAsync()
|
private async Task StartAiAsync()
|
||||||
|
|
@ -151,8 +165,7 @@ namespace ImageCatalog_2
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Optional UI-thread invoker set by the active UI layer (WPF, Avalonia, etc.).
|
/// Optional UI-thread invoker set by the active UI layer.
|
||||||
/// When set, <see cref="InvokeOnUiThreadAsync"/> uses this delegate instead of the WPF dispatcher.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Action<Action>? UiInvoker { get; set; }
|
public Action<Action>? UiInvoker { get; set; }
|
||||||
|
|
||||||
|
|
@ -163,7 +176,7 @@ namespace ImageCatalog_2
|
||||||
if (UiInvoker != null)
|
if (UiInvoker != null)
|
||||||
UiInvoker(action);
|
UiInvoker(action);
|
||||||
else
|
else
|
||||||
System.Windows.Application.Current?.Dispatcher.Invoke(action);
|
action();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -192,7 +205,18 @@ namespace ImageCatalog_2
|
||||||
public string FaceExecutablePath
|
public string FaceExecutablePath
|
||||||
{
|
{
|
||||||
get => _ai.FaceExecutablePath;
|
get => _ai.FaceExecutablePath;
|
||||||
set => _ai.FaceExecutablePath = value;
|
set
|
||||||
|
{
|
||||||
|
var normalizedValue = value ?? string.Empty;
|
||||||
|
if (string.Equals(_ai.FaceExecutablePath, normalizedValue, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
RefreshFaceExecutableCapabilities();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ai.FaceExecutablePath = normalizedValue;
|
||||||
|
RefreshFaceExecutableCapabilities();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public string FaceOutputFolderPath
|
public string FaceOutputFolderPath
|
||||||
|
|
@ -201,6 +225,64 @@ namespace ImageCatalog_2
|
||||||
set => _ai.FaceOutputFolderPath = value;
|
set => _ai.FaceOutputFolderPath = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool FaceRecursive
|
||||||
|
{
|
||||||
|
get => _ai.FaceRecursive;
|
||||||
|
set => _ai.FaceRecursive = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool FaceIncludeThumbnails
|
||||||
|
{
|
||||||
|
get => _ai.FaceIncludeThumbnails;
|
||||||
|
set => _ai.FaceIncludeThumbnails = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<int> FaceParallelismOptions { get; } = [1, 2, 3, 4, 5];
|
||||||
|
|
||||||
|
public int FaceParallelism
|
||||||
|
{
|
||||||
|
get => _ai.FaceParallelism;
|
||||||
|
set => _ai.FaceParallelism = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int FaceMinSize
|
||||||
|
{
|
||||||
|
get => _ai.FaceMinSize;
|
||||||
|
set => _ai.FaceMinSize = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool FaceUpsample
|
||||||
|
{
|
||||||
|
get => _ai.FaceUpsample;
|
||||||
|
set => _ai.FaceUpsample = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool FaceGpuOptionEnabled => _ai.FaceGpuOptionEnabled;
|
||||||
|
|
||||||
|
public bool UseFaceGpu
|
||||||
|
{
|
||||||
|
get => _ai.UseFaceGpu;
|
||||||
|
set => SetUseFaceGpu(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsFaceEncoderRunning
|
||||||
|
{
|
||||||
|
get => _ai.IsFaceEncoderRunning;
|
||||||
|
private set => _ai.IsFaceEncoderRunning = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string FaceStatusMessage
|
||||||
|
{
|
||||||
|
get => _ai.FaceStatusMessage;
|
||||||
|
private set => _ai.FaceStatusMessage = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string FaceCommandOutput
|
||||||
|
{
|
||||||
|
get => _ai.FaceCommandOutput;
|
||||||
|
private set => _ai.FaceCommandOutput = value;
|
||||||
|
}
|
||||||
|
|
||||||
// Race upload settings
|
// Race upload settings
|
||||||
public string ApiLogin
|
public string ApiLogin
|
||||||
{
|
{
|
||||||
|
|
@ -393,6 +475,7 @@ namespace ImageCatalog_2
|
||||||
}
|
}
|
||||||
|
|
||||||
NotifyPropertyChanged(e.PropertyName);
|
NotifyPropertyChanged(e.PropertyName);
|
||||||
|
UpdateFaceEncoderCommandStates();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnRaceUploadPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
private void OnRaceUploadPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||||
|
|
@ -1272,6 +1355,785 @@ namespace ImageCatalog_2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task StopFaceEncoderAsync(string reason, bool waitForExit = true)
|
||||||
|
{
|
||||||
|
var trackedProcess = GetTrackedFaceEncoderProcess();
|
||||||
|
var ownsProcess = false;
|
||||||
|
var process = trackedProcess;
|
||||||
|
|
||||||
|
if (process is null)
|
||||||
|
{
|
||||||
|
process = FindConfiguredFaceEncoderProcess();
|
||||||
|
ownsProcess = process is not null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process is null)
|
||||||
|
{
|
||||||
|
await InvokeOnUiThreadAsync(() =>
|
||||||
|
{
|
||||||
|
IsFaceEncoderRunning = false;
|
||||||
|
FaceStatusMessage = "Face encoder non in esecuzione.";
|
||||||
|
}).ConfigureAwait(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await InvokeOnUiThreadAsync(() => FaceStatusMessage = reason).ConfigureAwait(false);
|
||||||
|
|
||||||
|
var gracefulStopRequested = TryRequestFaceEncoderStop(process);
|
||||||
|
if (waitForExit)
|
||||||
|
{
|
||||||
|
var exited = await WaitForProcessExitAsync(process, TimeSpan.FromSeconds(5)).ConfigureAwait(false);
|
||||||
|
if (!exited)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
process.Kill(entireProcessTree: true);
|
||||||
|
exited = await WaitForProcessExitAsync(process, TimeSpan.FromSeconds(5)).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Unable to terminate face encoder process {ProcessId}", process.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await InvokeOnUiThreadAsync(() =>
|
||||||
|
{
|
||||||
|
IsFaceEncoderRunning = !exited && IsProcessAlive(process);
|
||||||
|
FaceStatusMessage = exited
|
||||||
|
? "Face encoder arrestato."
|
||||||
|
: gracefulStopRequested
|
||||||
|
? "Segnale di arresto inviato al face encoder."
|
||||||
|
: "Arresto forzato del face encoder richiesto.";
|
||||||
|
}).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (ownsProcess)
|
||||||
|
{
|
||||||
|
process.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool CanRunFaceEncoder()
|
||||||
|
{
|
||||||
|
return !IsFaceEncoderRunning;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool CanStopFaceEncoder()
|
||||||
|
{
|
||||||
|
return IsFaceEncoderRunning;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateFaceEncoderCommandStates()
|
||||||
|
{
|
||||||
|
_startFaceEncoderCommand?.RaiseCanExecuteChanged();
|
||||||
|
_stopFaceEncoderCommand?.RaiseCanExecuteChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RunFaceEncoderAsync()
|
||||||
|
{
|
||||||
|
if (IsFaceEncoderRunning)
|
||||||
|
{
|
||||||
|
FaceStatusMessage = "Face encoder gia in esecuzione.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var executableRootPath = NormalizeFilePathArgument(FaceExecutablePath);
|
||||||
|
var outputFolderPath = NormalizeDirectoryPathArgument(FaceOutputFolderPath);
|
||||||
|
var imagesFolder = NormalizeDirectoryPathArgument(DestinationPath);
|
||||||
|
var executablePath = ResolveConfiguredFaceEncoderExecutablePath(executableRootPath, UseFaceGpu);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(executableRootPath))
|
||||||
|
{
|
||||||
|
FaceStatusMessage = "Percorso cartella face encoder non valido.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(executablePath) || !File.Exists(executablePath))
|
||||||
|
{
|
||||||
|
FaceStatusMessage = UseFaceGpu
|
||||||
|
? "Impossibile trovare face_encoder_gpu.exe nella cartella selezionata."
|
||||||
|
: "Impossibile trovare face_encoder_cpu.exe nella cartella selezionata.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(imagesFolder) || !Directory.Exists(imagesFolder))
|
||||||
|
{
|
||||||
|
FaceStatusMessage = "Cartella Destinazione non valida.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(outputFolderPath))
|
||||||
|
{
|
||||||
|
FaceStatusMessage = "Inserisci la cartella di output per encodings e log.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(outputFolderPath);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Unable to create face output directory: {OutputFolderPath}", outputFolderPath);
|
||||||
|
FaceStatusMessage = "Impossibile creare la cartella di output.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var parallelism = NormalizeFaceParallelism(FaceParallelism);
|
||||||
|
var minSize = NormalizeFaceMinSize(FaceMinSize);
|
||||||
|
var outputFiles = BuildFaceEncoderOutputPaths(outputFolderPath, imagesFolder, DateTime.Now);
|
||||||
|
|
||||||
|
FaceExecutablePath = executableRootPath;
|
||||||
|
FaceOutputFolderPath = outputFolderPath;
|
||||||
|
FaceCommandOutput = string.Empty;
|
||||||
|
FaceStatusMessage = "Esecuzione face encoder in corso...";
|
||||||
|
|
||||||
|
var transcriptLines = new StringBuilder();
|
||||||
|
var outputLines = new StringBuilder();
|
||||||
|
var errorLines = new StringBuilder();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var processStartInfo = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = executablePath,
|
||||||
|
WorkingDirectory = Path.GetDirectoryName(executablePath) ?? Environment.CurrentDirectory,
|
||||||
|
UseShellExecute = false,
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true,
|
||||||
|
RedirectStandardInput = false,
|
||||||
|
CreateNoWindow = false,
|
||||||
|
};
|
||||||
|
|
||||||
|
processStartInfo.ArgumentList.Add("--images");
|
||||||
|
processStartInfo.ArgumentList.Add(imagesFolder);
|
||||||
|
processStartInfo.ArgumentList.Add("--out");
|
||||||
|
processStartInfo.ArgumentList.Add(outputFiles.OutputFilePath);
|
||||||
|
processStartInfo.ArgumentList.Add("--log");
|
||||||
|
processStartInfo.ArgumentList.Add(outputFiles.LogFilePath);
|
||||||
|
|
||||||
|
if (FaceRecursive)
|
||||||
|
{
|
||||||
|
processStartInfo.ArgumentList.Add("--recursive");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (FaceIncludeThumbnails)
|
||||||
|
{
|
||||||
|
processStartInfo.ArgumentList.Add("--include-tn");
|
||||||
|
}
|
||||||
|
|
||||||
|
processStartInfo.ArgumentList.Add(UseFaceGpu ? "--multiprocess" : "--multicore");
|
||||||
|
processStartInfo.ArgumentList.Add(parallelism.ToString());
|
||||||
|
processStartInfo.ArgumentList.Add("--min-size");
|
||||||
|
processStartInfo.ArgumentList.Add(minSize.ToString());
|
||||||
|
|
||||||
|
if (FaceUpsample)
|
||||||
|
{
|
||||||
|
processStartInfo.ArgumentList.Add("--upsample");
|
||||||
|
}
|
||||||
|
|
||||||
|
using var process = new Process { StartInfo = processStartInfo, EnableRaisingEvents = true };
|
||||||
|
process.OutputDataReceived += (_, args) => AppendFaceProcessOutput(outputLines, transcriptLines, args.Data, isError: false);
|
||||||
|
process.ErrorDataReceived += (_, args) => AppendFaceProcessOutput(errorLines, transcriptLines, args.Data, isError: true);
|
||||||
|
process.Exited += (_, _) =>
|
||||||
|
{
|
||||||
|
_ = InvokeOnUiThreadAsync(() =>
|
||||||
|
{
|
||||||
|
if (!ComputeIsFaceEncoderRunning())
|
||||||
|
{
|
||||||
|
IsFaceEncoderRunning = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!process.Start())
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Avvio face encoder fallito.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_hasStartedFaceEncoderInSession = true;
|
||||||
|
EnsureFaceEncoderWatcherStarted();
|
||||||
|
TrackFaceEncoderProcess(process);
|
||||||
|
await InvokeOnUiThreadAsync(() => IsFaceEncoderRunning = true).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (UseFaceGpu)
|
||||||
|
{
|
||||||
|
StartFaceEncoderLogWatcher(outputFiles.LogFilePath, outputLines, transcriptLines);
|
||||||
|
}
|
||||||
|
|
||||||
|
process.BeginOutputReadLine();
|
||||||
|
process.BeginErrorReadLine();
|
||||||
|
await process.WaitForExitAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
var summary = BuildFaceEncoderSummary(process.ExitCode, processStartInfo, outputFiles.OutputFilePath, outputFiles.LogFilePath, outputLines, errorLines);
|
||||||
|
await InvokeOnUiThreadAsync(() =>
|
||||||
|
{
|
||||||
|
FaceCommandOutput = string.IsNullOrWhiteSpace(FaceCommandOutput)
|
||||||
|
? summary
|
||||||
|
: $"{FaceCommandOutput.TrimEnd()}\n\n{summary}";
|
||||||
|
FaceStatusMessage = process.ExitCode == 0
|
||||||
|
? "Face encoder completato."
|
||||||
|
: $"Face encoder terminato con errore (code {process.ExitCode}).";
|
||||||
|
}).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(ex);
|
||||||
|
_logger.LogError(ex, "Face encoder execution failed.");
|
||||||
|
await InvokeOnUiThreadAsync(() =>
|
||||||
|
{
|
||||||
|
FaceCommandOutput = ex.ToString();
|
||||||
|
FaceStatusMessage = "Errore durante esecuzione face encoder.";
|
||||||
|
}).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await StopFaceEncoderLogWatcherAsync().ConfigureAwait(false);
|
||||||
|
ClearTrackedFaceEncoderProcess();
|
||||||
|
await InvokeOnUiThreadAsync(() => IsFaceEncoderRunning = ComputeIsFaceEncoderRunning()).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task WatchFaceEncoderProcessAsync(CancellationToken token)
|
||||||
|
{
|
||||||
|
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(1));
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (await timer.WaitForNextTickAsync(token).ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
if (!_hasStartedFaceEncoderInSession)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var isRunning = ComputeIsFaceEncoderRunning();
|
||||||
|
if (isRunning != IsFaceEncoderRunning)
|
||||||
|
{
|
||||||
|
await InvokeOnUiThreadAsync(() => IsFaceEncoderRunning = isRunning).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// App shutdown.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EnsureFaceEncoderWatcherStarted()
|
||||||
|
{
|
||||||
|
if (_faceEncoderWatcherTask is not null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_faceEncoderWatcherTokenSource = new CancellationTokenSource();
|
||||||
|
_faceEncoderWatcherTask = WatchFaceEncoderProcessAsync(_faceEncoderWatcherTokenSource.Token);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StartFaceEncoderLogWatcher(string logFilePath, StringBuilder outputLines, StringBuilder transcriptLines)
|
||||||
|
{
|
||||||
|
_faceEncoderLogWatcherTokenSource?.Cancel();
|
||||||
|
_faceEncoderLogWatcherTokenSource?.Dispose();
|
||||||
|
|
||||||
|
_faceEncoderLogWatcherTokenSource = new CancellationTokenSource();
|
||||||
|
_faceEncoderLogWatcherTask = WatchFaceEncoderLogFileAsync(logFilePath, outputLines, transcriptLines, _faceEncoderLogWatcherTokenSource.Token);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task StopFaceEncoderLogWatcherAsync()
|
||||||
|
{
|
||||||
|
var tokenSource = _faceEncoderLogWatcherTokenSource;
|
||||||
|
var task = _faceEncoderLogWatcherTask;
|
||||||
|
|
||||||
|
_faceEncoderLogWatcherTokenSource = null;
|
||||||
|
_faceEncoderLogWatcherTask = null;
|
||||||
|
|
||||||
|
if (tokenSource is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await tokenSource.CancelAsync().ConfigureAwait(false);
|
||||||
|
if (task is not null)
|
||||||
|
{
|
||||||
|
await task.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// Expected when shutting down the watcher.
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
tokenSource.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task WatchFaceEncoderLogFileAsync(string logFilePath, StringBuilder outputLines, StringBuilder transcriptLines, CancellationToken token)
|
||||||
|
{
|
||||||
|
long filePosition = 0;
|
||||||
|
|
||||||
|
while (!token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (File.Exists(logFilePath))
|
||||||
|
{
|
||||||
|
using var stream = new FileStream(logFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete);
|
||||||
|
if (filePosition > stream.Length)
|
||||||
|
{
|
||||||
|
filePosition = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
stream.Seek(filePosition, SeekOrigin.Begin);
|
||||||
|
using var reader = new StreamReader(stream);
|
||||||
|
while (!reader.EndOfStream)
|
||||||
|
{
|
||||||
|
var line = await reader.ReadLineAsync(token).ConfigureAwait(false);
|
||||||
|
AppendFaceProcessOutput(outputLines, transcriptLines, line, isError: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
filePosition = stream.Position;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
// Retry on the next polling interval while the encoder is still writing.
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException)
|
||||||
|
{
|
||||||
|
// Retry on the next polling interval if the log file is transiently locked.
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.Delay(TimeSpan.FromMilliseconds(250), token).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshFaceExecutableCapabilities()
|
||||||
|
{
|
||||||
|
var executableRoot = NormalizeFilePathArgument(_ai.FaceExecutablePath);
|
||||||
|
var hasCpu = !string.IsNullOrWhiteSpace(ResolveConfiguredFaceEncoderExecutablePath(executableRoot, useGpu: false));
|
||||||
|
var hasGpu = !string.IsNullOrWhiteSpace(ResolveConfiguredFaceEncoderExecutablePath(executableRoot, useGpu: true));
|
||||||
|
|
||||||
|
_ai.FaceGpuOptionEnabled = hasCpu && hasGpu;
|
||||||
|
|
||||||
|
if (hasGpu && !hasCpu)
|
||||||
|
{
|
||||||
|
_ai.UseFaceGpu = true;
|
||||||
|
}
|
||||||
|
else if (!hasGpu)
|
||||||
|
{
|
||||||
|
_ai.UseFaceGpu = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetUseFaceGpu(bool value)
|
||||||
|
{
|
||||||
|
var currentValue = _ai.UseFaceGpu;
|
||||||
|
if (!FaceGpuOptionEnabled)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentValue == value)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ai.UseFaceGpu = value;
|
||||||
|
|
||||||
|
var previousRecommendedUpsample = GetRecommendedFaceUpsample(currentValue);
|
||||||
|
if (_ai.FaceUpsample == previousRecommendedUpsample)
|
||||||
|
{
|
||||||
|
_ai.FaceUpsample = GetRecommendedFaceUpsample(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TrackFaceEncoderProcess(Process process)
|
||||||
|
{
|
||||||
|
lock (_faceEncoderProcessLock)
|
||||||
|
{
|
||||||
|
_faceEncoderProcess = process;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClearTrackedFaceEncoderProcess()
|
||||||
|
{
|
||||||
|
lock (_faceEncoderProcessLock)
|
||||||
|
{
|
||||||
|
if (_faceEncoderProcess is not null && _faceEncoderProcess.HasExited)
|
||||||
|
{
|
||||||
|
_faceEncoderProcess = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_faceEncoderProcess = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Process? GetTrackedFaceEncoderProcess()
|
||||||
|
{
|
||||||
|
lock (_faceEncoderProcessLock)
|
||||||
|
{
|
||||||
|
if (_faceEncoderProcess is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_faceEncoderProcess.HasExited)
|
||||||
|
{
|
||||||
|
_faceEncoderProcess = null;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _faceEncoderProcess;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Process? FindConfiguredFaceEncoderProcess()
|
||||||
|
{
|
||||||
|
var configuredExecutablePath = ResolveConfiguredFaceEncoderExecutablePath(FaceExecutablePath, UseFaceGpu);
|
||||||
|
if (string.IsNullOrWhiteSpace(configuredExecutablePath))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var processName = Path.GetFileNameWithoutExtension(configuredExecutablePath);
|
||||||
|
foreach (var process in Process.GetProcessesByName(processName))
|
||||||
|
{
|
||||||
|
if (!IsProcessAlive(process))
|
||||||
|
{
|
||||||
|
process.Dispose();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsMatchingProcessPath(process, configuredExecutablePath))
|
||||||
|
{
|
||||||
|
return process;
|
||||||
|
}
|
||||||
|
|
||||||
|
process.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool ComputeIsFaceEncoderRunning()
|
||||||
|
{
|
||||||
|
var trackedProcess = GetTrackedFaceEncoderProcess();
|
||||||
|
if (trackedProcess is not null)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_hasStartedFaceEncoderInSession)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var discoveredProcess = FindConfiguredFaceEncoderProcess();
|
||||||
|
return discoveredProcess is not null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsProcessAlive(Process process)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return !process.HasExited;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsMatchingProcessPath(Process process, string configuredExecutablePath)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var processPath = process.MainModule?.FileName;
|
||||||
|
return !string.IsNullOrWhiteSpace(processPath)
|
||||||
|
&& string.Equals(Path.GetFullPath(processPath), Path.GetFullPath(configuredExecutablePath), StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryRequestFaceEncoderStop(Process process)
|
||||||
|
{
|
||||||
|
if (!IsProcessAlive(process))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if WINDOWS
|
||||||
|
if (Program.TrySendConsoleInterrupt(process.Id))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return process.CloseMainWindow();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Unable to request graceful stop for face encoder process {ProcessId}", process.Id);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<bool> WaitForProcessExitAsync(Process process, TimeSpan timeout)
|
||||||
|
{
|
||||||
|
if (!IsProcessAlive(process))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var cancellationTokenSource = new CancellationTokenSource(timeout);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await process.WaitForExitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
return !IsProcessAlive(process);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AppendFaceProcessOutput(StringBuilder builder, StringBuilder transcriptBuilder, string? line, bool isError)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(line))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (builder)
|
||||||
|
{
|
||||||
|
builder.AppendLine(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isError)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(line);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
string transcript;
|
||||||
|
lock (transcriptBuilder)
|
||||||
|
{
|
||||||
|
if (isError)
|
||||||
|
{
|
||||||
|
transcriptBuilder.Append("[stderr] ");
|
||||||
|
}
|
||||||
|
|
||||||
|
transcriptBuilder.AppendLine(line);
|
||||||
|
transcript = transcriptBuilder.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = InvokeOnUiThreadAsync(() => FaceCommandOutput = transcript);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string? ResolveConfiguredFaceEncoderExecutablePath(string configuredPath, bool useGpu)
|
||||||
|
{
|
||||||
|
var variant = useGpu ? "gpu" : "cpu";
|
||||||
|
foreach (var candidate in EnumerateFaceEncoderExecutableCandidates(configuredPath, variant))
|
||||||
|
{
|
||||||
|
if (File.Exists(candidate))
|
||||||
|
{
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static (string OutputFilePath, string LogFilePath) BuildFaceEncoderOutputPaths(string outputFolderPath, string imagesFolderPath, DateTime timestamp)
|
||||||
|
{
|
||||||
|
var safeRaceName = BuildSafeFaceEncoderRaceName(imagesFolderPath);
|
||||||
|
var timestampToken = timestamp.ToString("yyyyMMdd_HHmmss");
|
||||||
|
|
||||||
|
return (
|
||||||
|
Path.Combine(outputFolderPath, $"face_encodings_{timestampToken}_{safeRaceName}.pkl"),
|
||||||
|
Path.Combine(outputFolderPath, $"encoder_log_{timestampToken}_{safeRaceName}.txt"));
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string BuildSafeFaceEncoderRaceName(string imagesFolderPath)
|
||||||
|
{
|
||||||
|
var raceName = new DirectoryInfo(imagesFolderPath).Name;
|
||||||
|
if (string.IsNullOrWhiteSpace(raceName))
|
||||||
|
{
|
||||||
|
return "race";
|
||||||
|
}
|
||||||
|
|
||||||
|
var invalidChars = Path.GetInvalidFileNameChars();
|
||||||
|
var builder = new StringBuilder(raceName.Length);
|
||||||
|
var previousWasSeparator = false;
|
||||||
|
|
||||||
|
foreach (var currentChar in raceName)
|
||||||
|
{
|
||||||
|
if (char.IsWhiteSpace(currentChar) || invalidChars.Contains(currentChar))
|
||||||
|
{
|
||||||
|
if (!previousWasSeparator)
|
||||||
|
{
|
||||||
|
builder.Append('_');
|
||||||
|
previousWasSeparator = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.Append(currentChar);
|
||||||
|
previousWasSeparator = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.ToString().Trim('_') switch
|
||||||
|
{
|
||||||
|
"" => "race",
|
||||||
|
var sanitized => sanitized
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<string> EnumerateFaceEncoderExecutableCandidates(string configuredPath, string variant)
|
||||||
|
{
|
||||||
|
var normalizedPath = NormalizeFilePathArgument(configuredPath);
|
||||||
|
if (string.IsNullOrWhiteSpace(normalizedPath))
|
||||||
|
{
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var executableName = $"face_encoder_{variant}.exe";
|
||||||
|
|
||||||
|
if (File.Exists(normalizedPath))
|
||||||
|
{
|
||||||
|
var fileDirectory = Path.GetDirectoryName(normalizedPath);
|
||||||
|
if (!string.IsNullOrWhiteSpace(fileDirectory))
|
||||||
|
{
|
||||||
|
yield return Path.Combine(fileDirectory, executableName);
|
||||||
|
|
||||||
|
var parentDirectory = Directory.GetParent(fileDirectory)?.FullName;
|
||||||
|
if (!string.IsNullOrWhiteSpace(parentDirectory))
|
||||||
|
{
|
||||||
|
yield return Path.Combine(parentDirectory, $"face_encoder_{variant}", executableName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
yield return normalizedPath;
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
yield return Path.Combine(normalizedPath, executableName);
|
||||||
|
yield return Path.Combine(normalizedPath, $"face_encoder_{variant}", executableName);
|
||||||
|
|
||||||
|
var leafName = Path.GetFileName(normalizedPath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar));
|
||||||
|
if (leafName.Equals("face_encoder_cpu", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| leafName.Equals("face_encoder_gpu", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var parentDirectory = Directory.GetParent(normalizedPath)?.FullName;
|
||||||
|
if (!string.IsNullOrWhiteSpace(parentDirectory))
|
||||||
|
{
|
||||||
|
yield return Path.Combine(parentDirectory, $"face_encoder_{variant}", executableName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int NormalizeFaceParallelism(int value)
|
||||||
|
{
|
||||||
|
return value is >= 1 and <= 5 ? value : 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int NormalizeFaceMinSize(int value)
|
||||||
|
{
|
||||||
|
return value > 0 ? value : 35;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool GetRecommendedFaceUpsample(bool useGpu)
|
||||||
|
{
|
||||||
|
return !useGpu;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildFaceEncoderSummary(
|
||||||
|
int exitCode,
|
||||||
|
ProcessStartInfo processStartInfo,
|
||||||
|
string outputFilePath,
|
||||||
|
string logFilePath,
|
||||||
|
StringBuilder outputLines,
|
||||||
|
StringBuilder errorLines)
|
||||||
|
{
|
||||||
|
var summary = new StringBuilder();
|
||||||
|
summary.AppendLine($"Exit code: {exitCode}");
|
||||||
|
summary.AppendLine($"Command: {processStartInfo.FileName} {string.Join(" ", processStartInfo.ArgumentList)}");
|
||||||
|
summary.AppendLine($"Output file: {outputFilePath}");
|
||||||
|
summary.AppendLine($"Log file: {logFilePath}");
|
||||||
|
|
||||||
|
lock (outputLines)
|
||||||
|
{
|
||||||
|
if (outputLines.Length > 0)
|
||||||
|
{
|
||||||
|
summary.AppendLine();
|
||||||
|
summary.AppendLine("STDOUT:");
|
||||||
|
summary.Append(outputLines);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (errorLines)
|
||||||
|
{
|
||||||
|
if (errorLines.Length > 0)
|
||||||
|
{
|
||||||
|
summary.AppendLine();
|
||||||
|
summary.AppendLine("STDERR:");
|
||||||
|
summary.Append(errorLines);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return summary.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizeDirectoryPathArgument(string value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalized = value.Trim().Trim('"');
|
||||||
|
var root = Path.GetPathRoot(normalized);
|
||||||
|
if (!string.IsNullOrEmpty(root) && normalized.Length > root.Length)
|
||||||
|
{
|
||||||
|
normalized = normalized.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizeFilePathArgument(string value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.Trim().Trim('"');
|
||||||
|
}
|
||||||
|
|
||||||
// Note: These commands will trigger events that the View will handle to show dialogs
|
// Note: These commands will trigger events that the View will handle to show dialogs
|
||||||
// since dialogs require UI context
|
// since dialogs require UI context
|
||||||
public event EventHandler? SelectSourceFolderRequested;
|
public event EventHandler? SelectSourceFolderRequested;
|
||||||
|
|
@ -1283,11 +2145,7 @@ namespace ImageCatalog_2
|
||||||
public event EventHandler<string?>? LoadSettingsRequested;
|
public event EventHandler<string?>? LoadSettingsRequested;
|
||||||
public event EventHandler? SelectColorRequested;
|
public event EventHandler? SelectColorRequested;
|
||||||
// Request that the View shows a message to the user (message, caption, icon)
|
// Request that the View shows a message to the user (message, caption, icon)
|
||||||
#if WINDOWS
|
|
||||||
public event EventHandler<Tuple<string, string, MessageBoxIcon>>? ShowMessageRequested;
|
|
||||||
#else
|
|
||||||
public event EventHandler<Tuple<string, string, int>>? ShowMessageRequested;
|
public event EventHandler<Tuple<string, string, int>>? ShowMessageRequested;
|
||||||
#endif
|
|
||||||
public event EventHandler? SelectTransparentColorRequested;
|
public event EventHandler? SelectTransparentColorRequested;
|
||||||
|
|
||||||
private void SelectSourceFolder(object parameter)
|
private void SelectSourceFolder(object parameter)
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,6 @@
|
||||||
<PropertyGroup Condition="$([MSBuild]::IsOsPlatform('Windows'))">
|
<PropertyGroup Condition="$([MSBuild]::IsOsPlatform('Windows'))">
|
||||||
<TargetFramework>net10.0-windows</TargetFramework>
|
<TargetFramework>net10.0-windows</TargetFramework>
|
||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
<UseWindowsForms>true</UseWindowsForms>
|
|
||||||
<UseWPF>true</UseWPF>
|
|
||||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||||
<ApplicationIcon>Logo.ico</ApplicationIcon>
|
<ApplicationIcon>Logo.ico</ApplicationIcon>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
@ -31,9 +29,7 @@
|
||||||
<PublishReadyToRun Condition="'$(PublishReadyToRun)' == ''">false</PublishReadyToRun>
|
<PublishReadyToRun Condition="'$(PublishReadyToRun)' == ''">false</PublishReadyToRun>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<!-- Keep MinVer package enabled but do NOT let it overwrite AssemblyVersion/FileVersion used at build-time.
|
<!-- Keep MinVer package enabled but do NOT let it overwrite AssemblyVersion/FileVersion used at build-time. -->
|
||||||
This prevents MinVer from injecting a computed version into generated BAML/pack URIs which can cause
|
|
||||||
WPF to try loading a mismatched assembly identity at runtime. Do a full clean rebuild after this change. -->
|
|
||||||
<UpdateVersionProperties>true</UpdateVersionProperties>
|
<UpdateVersionProperties>true</UpdateVersionProperties>
|
||||||
<!-- Skip MinVer execution during local builds to avoid environment/runtime-specific failures. -->
|
<!-- Skip MinVer execution during local builds to avoid environment/runtime-specific failures. -->
|
||||||
<MinVerSkip>true</MinVerSkip>
|
<MinVerSkip>true</MinVerSkip>
|
||||||
|
|
@ -65,11 +61,9 @@
|
||||||
<PackageReference Include="AIFotoONLUS.Core" Version="0.1.1" />
|
<PackageReference Include="AIFotoONLUS.Core" Version="0.1.1" />
|
||||||
<PackageReference Include="AutoMapper" Version="16.1.0" />
|
<PackageReference Include="AutoMapper" Version="16.1.0" />
|
||||||
<PackageReference Include="IconPacks.Avalonia" Version="1.3.1" />
|
<PackageReference Include="IconPacks.Avalonia" Version="1.3.1" />
|
||||||
<PackageReference Include="MahApps.Metro" Version="2.4.11" Condition="$([MSBuild]::IsOsPlatform('Windows'))" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.5" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.5" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.5" />
|
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.5" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="10.0.5" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="10.0.5" />
|
||||||
<PackageReference Include="MahApps.Metro.IconPacks" Version="6.2.1" Condition="$([MSBuild]::IsOsPlatform('Windows'))" />
|
|
||||||
<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.12" />
|
||||||
<PackageReference Include="Avalonia.Desktop" Version="11.3.12" />
|
<PackageReference Include="Avalonia.Desktop" Version="11.3.12" />
|
||||||
|
|
@ -81,8 +75,7 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<!-- MinVer provides a computed 'Version' property. Do not automatically override AssemblyVersion/FileVersion
|
<!-- MinVer provides a computed 'Version' property. Do not automatically override AssemblyVersion/FileVersion
|
||||||
with MinVer's computed Version to avoid mismatches embedded into generated XAML/BAML. The explicit
|
with MinVer's computed Version. The explicit AssemblyVersion/FileVersion at the top of this file will be used for runtime identity. -->
|
||||||
AssemblyVersion/FileVersion at the top of this file will be used for runtime identity. -->
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Update="Properties\Settings.Designer.cs">
|
<Compile Update="Properties\Settings.Designer.cs">
|
||||||
<DesignTimeSharedInput>True</DesignTimeSharedInput>
|
<DesignTimeSharedInput>True</DesignTimeSharedInput>
|
||||||
|
|
@ -90,11 +83,6 @@
|
||||||
<DependentUpon>Settings.settings</DependentUpon>
|
<DependentUpon>Settings.settings</DependentUpon>
|
||||||
</Compile>
|
</Compile>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
|
||||||
<EmbeddedResource Update="MainForm.resx">
|
|
||||||
<DependentUpon>MainForm.cs</DependentUpon>
|
|
||||||
</EmbeddedResource>
|
|
||||||
</ItemGroup>
|
|
||||||
<PropertyGroup />
|
<PropertyGroup />
|
||||||
|
|
||||||
<!-- No Visual Studio fallback required for MinVer; MinVer integrates with MSBuild during build. -->
|
<!-- No Visual Studio fallback required for MinVer; MinVer integrates with MSBuild during build. -->
|
||||||
|
|
|
||||||
2246
imagecatalog/MainForm.Designer.cs
generated
2246
imagecatalog/MainForm.Designer.cs
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,842 +0,0 @@
|
||||||
#if WINDOWS
|
|
||||||
using System;
|
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.ComponentModel;
|
|
||||||
using System.Drawing;
|
|
||||||
using System.Drawing.Text;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Reflection;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Windows.Forms;
|
|
||||||
using ImageCatalog_2;
|
|
||||||
using ImageCatalog_2.Commands;
|
|
||||||
using ImageCatalog_2.Services;
|
|
||||||
using MaddoShared;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace ImageCatalog;
|
|
||||||
|
|
||||||
public partial class MainForm
|
|
||||||
{
|
|
||||||
private readonly DataModel Model;
|
|
||||||
|
|
||||||
private readonly ILogger<MainForm> _logger;
|
|
||||||
|
|
||||||
private readonly ParametriSetup _parametriSetup;
|
|
||||||
private readonly PicSettings _picSettings;
|
|
||||||
// Prevent re-entrant updates between UI events and model PropertyChanged handling
|
|
||||||
private bool _suppressRadioUpdates = false;
|
|
||||||
private bool _transparentDialogOpen = false;
|
|
||||||
|
|
||||||
public MainForm(DataModel model, ImageCreationService imageCreationStuff, PicSettings picSettings,
|
|
||||||
ParametriSetup parametriSetup, ILogger<MainForm> logger)
|
|
||||||
{
|
|
||||||
Model = model;
|
|
||||||
_parametriSetup = parametriSetup;
|
|
||||||
_picSettings = picSettings;
|
|
||||||
_logger = logger;
|
|
||||||
|
|
||||||
_logger.LogDebug("Start");
|
|
||||||
|
|
||||||
InitializeComponent();
|
|
||||||
// Set this form as the control for thread marshalling in the DataModel
|
|
||||||
Model.SetControl(this);
|
|
||||||
|
|
||||||
// Ensure the designer data bindings have a concrete DataSource immediately so
|
|
||||||
// that UI controls (radio buttons) reflect the ViewModel state and propagate
|
|
||||||
// user changes back to the ViewModel.
|
|
||||||
bindingSource1.DataSource = Model;
|
|
||||||
|
|
||||||
BindControls();
|
|
||||||
|
|
||||||
// The designer originally bound the radio buttons to boolean helpers on the ViewModel.
|
|
||||||
// Those two separate bindings can fight with each other. Clear designer bindings and
|
|
||||||
// wire explicit Click handlers that update the single authoritative property
|
|
||||||
// `Model.ImageLibrary`. Also keep a PropertyChanged listener to reflect external
|
|
||||||
// changes back into the radio buttons.
|
|
||||||
rdbLibrary1.DataBindings.Clear();
|
|
||||||
rdbLibrary2.DataBindings.Clear();
|
|
||||||
|
|
||||||
// Initialize radio state from model
|
|
||||||
rdbLibrary1.Checked = Model.UseSystemGraphics;
|
|
||||||
rdbLibrary2.Checked = Model.UseImageSharp;
|
|
||||||
|
|
||||||
// Use Click handlers (not CheckedChanged) to avoid competing binding updates
|
|
||||||
rdbLibrary1.Click += (_, _) =>
|
|
||||||
{
|
|
||||||
if (_suppressRadioUpdates) return;
|
|
||||||
if (Model.ImageLibrary != "System.Graphics")
|
|
||||||
Model.ImageLibrary = "System.Graphics";
|
|
||||||
};
|
|
||||||
rdbLibrary2.Click += (_, _) =>
|
|
||||||
{
|
|
||||||
if (_suppressRadioUpdates) return;
|
|
||||||
if (Model.ImageLibrary != "ImageSharp")
|
|
||||||
Model.ImageLibrary = "ImageSharp";
|
|
||||||
};
|
|
||||||
|
|
||||||
// Watch for model changes so we can reflect external updates
|
|
||||||
Model.PropertyChanged += Model_PropertyChanged;
|
|
||||||
|
|
||||||
// Thumbnail options moved to ComboBox to avoid conflicting bindings with multiple radio buttons
|
|
||||||
|
|
||||||
// Initialize ComboBox with Italian descriptions
|
|
||||||
comboThumbnailOption.Items.Clear();
|
|
||||||
comboThumbnailOption.Items.AddRange(new object[] {
|
|
||||||
"Nessuna",
|
|
||||||
"Aggiungi scritta",
|
|
||||||
"Nome file",
|
|
||||||
"Aggiungi orario",
|
|
||||||
"Nome+Orario",
|
|
||||||
"Tempo gara"
|
|
||||||
});
|
|
||||||
|
|
||||||
// Bind to model via helper index property ThumbnailOptionIndex
|
|
||||||
comboThumbnailOption.DataBindings.Add(new Binding("SelectedIndex", bindingSource1, "ThumbnailOptionIndex", true, DataSourceUpdateMode.OnPropertyChanged));
|
|
||||||
|
|
||||||
// Save user preferences on form close instead of immediately when dialogs are used
|
|
||||||
this.FormClosing += MainForm_FormClosing;
|
|
||||||
|
|
||||||
// Wire up 'Open folder in Explorer' buttons
|
|
||||||
btnOpenSourceFolder.Click += BtnOpenSourceFolder_Click;
|
|
||||||
btnOpenDestFolder.Click += BtnOpenDestFolder_Click;
|
|
||||||
|
|
||||||
// Show currently selected color in small PictureBox3
|
|
||||||
PictureBox3.BackColor = ColorTranslator.FromHtml(Model.TransparentColor);
|
|
||||||
|
|
||||||
// Version label is data-bound to DataModel.AppVersion; DataModel is populated with the version via DI
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private void RdbLibrary_CheckedChanged(object? sender, EventArgs e)
|
|
||||||
{
|
|
||||||
// Keep behavior simple: when a radio button becomes checked, update the ViewModel
|
|
||||||
// so that the designer binding and PicSettings stay in sync.
|
|
||||||
if (sender is RadioButton rb && rb.Checked)
|
|
||||||
{
|
|
||||||
_logger?.LogDebug("Radio library changed: {RadioName}", rb.Name);
|
|
||||||
if (rb == rdbLibrary2)
|
|
||||||
{
|
|
||||||
Model.ImageLibrary = "ImageSharp";
|
|
||||||
}
|
|
||||||
else if (rb == rdbLibrary1)
|
|
||||||
{
|
|
||||||
Model.ImageLibrary = "System.Graphics";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Model_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
|
||||||
{
|
|
||||||
if (e.PropertyName is null) return;
|
|
||||||
if (e.PropertyName == nameof(Model.ImageLibrary) || e.PropertyName == nameof(Model.UseImageSharp) || e.PropertyName == nameof(Model.UseSystemGraphics))
|
|
||||||
{
|
|
||||||
_logger?.LogDebug("Model property changed: {Property} => ImageLibrary={ImageLibrary}, PicSettings.Provider={Provider}", e.PropertyName, Model.ImageLibrary, _picSettings.ImageCreatorProvider);
|
|
||||||
|
|
||||||
// Reflect authoritative model value into the radio buttons in a thread-safe, re-entrancy-safe way
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_suppressRadioUpdates = true;
|
|
||||||
rdbLibrary1.Checked = Model.UseSystemGraphics;
|
|
||||||
rdbLibrary2.Checked = Model.UseImageSharp;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_suppressRadioUpdates = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Thumbnail mode changes - reflect back to combo box index
|
|
||||||
if (e.PropertyName == nameof(Model.ThumbnailMode) ||
|
|
||||||
e.PropertyName == nameof(Model.ThumbnailOption) ||
|
|
||||||
e.PropertyName == nameof(Model.ThumbnailOptionIndex))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_suppressRadioUpdates = true;
|
|
||||||
comboThumbnailOption.SelectedIndex = Model.ThumbnailOptionIndex;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_suppressRadioUpdates = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void BindControls()
|
|
||||||
{
|
|
||||||
// Bind buttons to ViewModel commands using command binding
|
|
||||||
_btnCreaCatalogoAsync.BindCommand(Model.ProcessImagesCommand);
|
|
||||||
// Note: `button1` control does not exist in the designer. Use the primary create button only.
|
|
||||||
_Button2.BindCommand(Model.SelectSourceFolderCommand);
|
|
||||||
_Button3.BindCommand(Model.SelectDestinationFolderCommand);
|
|
||||||
_Button4.BindCommand(Model.SelectLogoFileCommand);
|
|
||||||
_Button5.BindCommand(Model.SaveSettingsCommand);
|
|
||||||
_Button6.BindCommand(Model.LoadSettingsCommand);
|
|
||||||
_Button8.BindCommand(Model.SelectColorCommand);
|
|
||||||
// Bind the transparency chooser button/command
|
|
||||||
btnSetTransparency.BindCommand(Model.SelectTransparentColorCommand);
|
|
||||||
|
|
||||||
// Subscribe to ViewModel events for UI dialogs (these need UI context)
|
|
||||||
Model.SelectSourceFolderRequested += OnSelectSourceFolderRequested;
|
|
||||||
Model.SelectDestinationFolderRequested += OnSelectDestinationFolderRequested;
|
|
||||||
Model.SelectLogoFileRequested += OnSelectLogoFileRequested;
|
|
||||||
Model.SaveSettingsRequested += OnSaveSettingsRequested;
|
|
||||||
Model.LoadSettingsRequested += OnLoadSettingsRequested;
|
|
||||||
Model.SelectColorRequested += OnSelectColorRequested;
|
|
||||||
Model.SelectTransparentColorRequested += OnSelectTransparentColorRequested;
|
|
||||||
// Show message requests (from ViewModel validation)
|
|
||||||
Model.ShowMessageRequested += OnShowMessageRequested;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnSelectTransparentColorRequested(object? sender, EventArgs e)
|
|
||||||
{
|
|
||||||
// Ensure UI thread
|
|
||||||
if (InvokeRequired)
|
|
||||||
{
|
|
||||||
Invoke(new Action<object, EventArgs>(OnSelectTransparentColorRequested), sender, e as EventArgs ?? EventArgs.Empty);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Prevent re-entrancy: if the dialog is already open, ignore subsequent requests
|
|
||||||
if (_transparentDialogOpen) return;
|
|
||||||
|
|
||||||
_transparentDialogOpen = true;
|
|
||||||
var dlg = new ColorDialog { AllowFullOpen = true };
|
|
||||||
try
|
|
||||||
{
|
|
||||||
dlg.Color = ColorTranslator.FromHtml(Model.TransparentColor);
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (dlg.ShowDialog() == DialogResult.OK)
|
|
||||||
{
|
|
||||||
Model.TransparentColor = ColorTranslator.ToHtml(dlg.Color);
|
|
||||||
PictureBox3.BackColor = dlg.Color;
|
|
||||||
|
|
||||||
// Update preview if logo exists
|
|
||||||
if (!string.IsNullOrWhiteSpace(Model.LogoFile) && File.Exists(Model.LogoFile))
|
|
||||||
{
|
|
||||||
UpdateLogoPictureBox(Model.LogoFile);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_transparentDialogOpen = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void BtnSetTransparency_Click(object? sender, EventArgs e)
|
|
||||||
{
|
|
||||||
Model.SelectTransparentColorCommand.Execute(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnShowMessageRequested(object? sender, Tuple<string, string, MessageBoxIcon> args)
|
|
||||||
{
|
|
||||||
if (args is null) return;
|
|
||||||
// Ensure call on UI thread
|
|
||||||
if (InvokeRequired)
|
|
||||||
{
|
|
||||||
Invoke(new Action(() => OnShowMessageRequested(sender, args)));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
MessageBox.Show(this, args.Item1, args.Item2, MessageBoxButtons.OK, args.Item3);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SetDefaults()
|
|
||||||
{
|
|
||||||
// Bind ComboBoxes to Model using proper data binding
|
|
||||||
ComboBox1.DataSource = new List<string>(Model.VerticalPositions);
|
|
||||||
ComboBox1.DataBindings.Add(new Binding("SelectedItem", bindingSource1, nameof(Model.VerticalPosition),
|
|
||||||
false, DataSourceUpdateMode.OnPropertyChanged));
|
|
||||||
|
|
||||||
ComboBox2.DataSource = new List<string>(Model.HorizontalAlignments);
|
|
||||||
ComboBox2.DataBindings.Add(new Binding("SelectedItem", bindingSource1, nameof(Model.HorizontalAlignment),
|
|
||||||
false, DataSourceUpdateMode.OnPropertyChanged));
|
|
||||||
|
|
||||||
ComboBox3.DataSource = new List<string>(Model.AvailableFonts);
|
|
||||||
ComboBox3.DataBindings.Add(new Binding("SelectedItem", bindingSource1, nameof(Model.FontName),
|
|
||||||
false, DataSourceUpdateMode.OnPropertyChanged));
|
|
||||||
|
|
||||||
ComboBox4.DataSource = new List<string>(Model.HorizontalAlignments);
|
|
||||||
ComboBox4.DataBindings.Add(new Binding("SelectedItem", bindingSource1, nameof(Model.LogoHorizontalPosition),
|
|
||||||
false, DataSourceUpdateMode.OnPropertyChanged));
|
|
||||||
|
|
||||||
ComboBox5.DataSource = new List<string> { "Alto", "Centro", "Basso" };
|
|
||||||
ComboBox5.DataBindings.Add(new Binding("SelectedItem", bindingSource1, nameof(Model.LogoVerticalPosition),
|
|
||||||
false, DataSourceUpdateMode.OnPropertyChanged));
|
|
||||||
|
|
||||||
// Bind progress bar and status labels
|
|
||||||
ProgressBar1.DataBindings.Add(new Binding("Maximum", bindingSource1, nameof(Model.ProgressBarMaximum),
|
|
||||||
false, DataSourceUpdateMode.OnPropertyChanged));
|
|
||||||
ProgressBar1.DataBindings.Add(new Binding("Value", bindingSource1, nameof(Model.ProgressBarValue),
|
|
||||||
false, DataSourceUpdateMode.OnPropertyChanged));
|
|
||||||
|
|
||||||
Label18.DataBindings.Add(new Binding("Text", bindingSource1, nameof(Model.ProcessedImagesCount),
|
|
||||||
false, DataSourceUpdateMode.OnPropertyChanged));
|
|
||||||
lblFotoTotaliNum.DataBindings.Add(new Binding("Text", bindingSource1, nameof(Model.TotalImagesCount),
|
|
||||||
false, DataSourceUpdateMode.OnPropertyChanged));
|
|
||||||
Label10.DataBindings.Add(new Binding("Text", bindingSource1, nameof(Model.ProcessingStatus),
|
|
||||||
false, DataSourceUpdateMode.OnPropertyChanged));
|
|
||||||
|
|
||||||
// Bind transparency model properties to UI
|
|
||||||
chkUseTransparentColor.DataBindings.Add(new Binding("Checked", bindingSource1, nameof(Model.UseTransparentColor), false, DataSourceUpdateMode.OnPropertyChanged));
|
|
||||||
// Show currently selected color in PictureBox3
|
|
||||||
PictureBox3.Visible = false;
|
|
||||||
if (!string.IsNullOrWhiteSpace(Model.TransparentColor))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
PictureBox3.BackColor = ColorTranslator.FromHtml(Model.TransparentColor);
|
|
||||||
PictureBox3.Visible = true;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
PictureBox3.Visible = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// When logo file changes, update preview
|
|
||||||
Model.PropertyChanged += (s, e) =>
|
|
||||||
{
|
|
||||||
if (e.PropertyName == nameof(Model.LogoFile) || e.PropertyName == nameof(Model.UseTransparentColor) || e.PropertyName == nameof(Model.TransparentColor))
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrWhiteSpace(Model.LogoFile) && System.IO.File.Exists(Model.LogoFile))
|
|
||||||
{
|
|
||||||
UpdateLogoPictureBox(Model.LogoFile);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Bind transparent color hex and show color in PictureBox3
|
|
||||||
// Bind UseTransparentColor checkbox (designer control named CheckBox5 used for AddLogo earlier, add new binding control exists in designer)
|
|
||||||
// Use PictureBox3 to display color value
|
|
||||||
var colorBinding = new Binding("BackColor", bindingSource1, nameof(Model.TransparentColor), true, DataSourceUpdateMode.OnPropertyChanged);
|
|
||||||
colorBinding.Format += (s, e) =>
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
e.Value = ColorTranslator.FromHtml(e.Value?.ToString() ?? "#FFFFFF");
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
e.Value = Color.White;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
PictureBox3.DataBindings.Add(colorBinding);
|
|
||||||
|
|
||||||
// Bind checkbox for using color key transparency if such control exists (CheckBox5 was repurposed as AddLogo); create binding if available
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// The designer has CheckBox5 for 'AddLogo'. We'll add a separate binding to a new control named CheckBoxUseTransparentColor if present.
|
|
||||||
var chk = this.Controls.Find("chkUseTransparentColor", true).FirstOrDefault() as CheckBox;
|
|
||||||
if (chk != null)
|
|
||||||
{
|
|
||||||
chk.DataBindings.Add(new Binding("Checked", bindingSource1, nameof(Model.UseTransparentColor), false, DataSourceUpdateMode.OnPropertyChanged));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void Form1_Load(object sender, EventArgs e)
|
|
||||||
{
|
|
||||||
bindingSource1.DataSource = Model;
|
|
||||||
Application.EnableVisualStyles();
|
|
||||||
SetDefaults();
|
|
||||||
|
|
||||||
_logger.LogInformation("Programma Avviato");
|
|
||||||
// If settings were loaded before the form was shown, ensure the logo preview is updated
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrWhiteSpace(Model.LogoFile) && File.Exists(Model.LogoFile))
|
|
||||||
{
|
|
||||||
UpdateLogoPictureBox(Model.LogoFile);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogDebug(ex, "Failed to load logo during form load");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private string CalcTime(DateTime timeStart, DateTime timeStop, int numFoto)
|
|
||||||
{
|
|
||||||
long timediffH, timediffS;
|
|
||||||
long timediffM;
|
|
||||||
|
|
||||||
TimeSpan timeDiff = timeStop - timeStart;
|
|
||||||
|
|
||||||
timediffM = (int)timeDiff.TotalMinutes;
|
|
||||||
timediffS = (int)timeDiff.TotalSeconds;
|
|
||||||
timediffH = (int)timeDiff.TotalHours;
|
|
||||||
|
|
||||||
// timediffM = DateAndTime.DateDiff(DateInterval.Minute, timeStart, timeStop);
|
|
||||||
// timediffS = DateAndTime.DateDiff(DateInterval.Second, timeStart, timeStop);
|
|
||||||
// timediffH = DateAndTime.DateDiff(DateInterval.Hour, timeStart, timeStop);
|
|
||||||
|
|
||||||
double fotoSec = numFoto / (double)timediffS;
|
|
||||||
double fotoMin = numFoto / (double)timediffM;
|
|
||||||
double fotoOra = numFoto / (double)timediffH;
|
|
||||||
string s = "S: " + timediffS.ToString() + "; F/s: " +
|
|
||||||
fotoSec.ToString(
|
|
||||||
"0.000"); // + " F/m: " + fotoMin.ToString("0.00") + " F/h: " + fotoOra.ToString("0.00")
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
private string SelectFolder(string startingFolder)
|
|
||||||
{
|
|
||||||
var dialog = new FolderBrowserDialog();
|
|
||||||
dialog.InitialDirectory = startingFolder;
|
|
||||||
if (dialog.ShowDialog() != DialogResult.OK) return string.Empty;
|
|
||||||
|
|
||||||
return FixPath(dialog.SelectedPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
private string FixPath(string path)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(path))
|
|
||||||
{
|
|
||||||
return string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trim leading/trailing quotes
|
|
||||||
path = path.Trim().Trim('"');
|
|
||||||
|
|
||||||
// Normalize directory separators
|
|
||||||
path = path.Replace('/', Path.DirectorySeparatorChar)
|
|
||||||
.Replace('\\', Path.DirectorySeparatorChar);
|
|
||||||
|
|
||||||
// Remove trailing separators then add one back
|
|
||||||
path = path.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;
|
|
||||||
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnSelectSourceFolderRequested(object sender, EventArgs e)
|
|
||||||
{
|
|
||||||
// Prefer model value; if empty fall back to last-used value stored in user prefs
|
|
||||||
var starting = !string.IsNullOrWhiteSpace(Model.SourcePath)
|
|
||||||
? Model.SourcePath
|
|
||||||
: _parametriSetup.LeggiParametroString("LastSourceFolder");
|
|
||||||
|
|
||||||
var dialogResult = SelectFolder(starting);
|
|
||||||
if (!string.IsNullOrWhiteSpace(dialogResult))
|
|
||||||
{
|
|
||||||
Model.SourcePath = dialogResult;
|
|
||||||
_parametriSetup.AggiornaParametro("LastSourceFolder", dialogResult);
|
|
||||||
_parametriSetup.SalvaParametriSetup();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void BtnOpenSourceFolder_Click(object? sender, EventArgs e)
|
|
||||||
{
|
|
||||||
// Prefer the model value but fall back to the textbox if needed
|
|
||||||
var path = string.IsNullOrWhiteSpace(Model.SourcePath) ? txtSorgente.Text : Model.SourcePath;
|
|
||||||
OpenFolder(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void BtnOpenDestFolder_Click(object? sender, EventArgs e)
|
|
||||||
{
|
|
||||||
var path = string.IsNullOrWhiteSpace(Model.DestinationPath) ? txtDestinazione.Text : Model.DestinationPath;
|
|
||||||
OpenFolder(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OpenFolder(string? path)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(path))
|
|
||||||
{
|
|
||||||
MessageBox.Show(this, "Folder path is empty.", "Open Folder", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
path = path.Trim().Trim('"');
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (File.Exists(path))
|
|
||||||
{
|
|
||||||
// If a file was provided, open its folder and select it
|
|
||||||
Process.Start("explorer.exe", $"/select,\"{path}\"");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Directory.Exists(path))
|
|
||||||
{
|
|
||||||
Process.Start(new ProcessStartInfo
|
|
||||||
{
|
|
||||||
FileName = path,
|
|
||||||
UseShellExecute = true
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
MessageBox.Show(this, $"Folder does not exist: {path}", "Open Folder", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger?.LogError(ex, "Failed to open folder {Path}", path);
|
|
||||||
MessageBox.Show(this, $"Failed to open folder: {ex.Message}", "Open Folder", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnSelectDestinationFolderRequested(object sender, EventArgs e)
|
|
||||||
{
|
|
||||||
var starting = !string.IsNullOrWhiteSpace(Model.DestinationPath)
|
|
||||||
? Model.DestinationPath
|
|
||||||
: _parametriSetup.LeggiParametroString("LastDestinationFolder");
|
|
||||||
|
|
||||||
var dialogResult = SelectFolder(starting);
|
|
||||||
if (!string.IsNullOrWhiteSpace(dialogResult))
|
|
||||||
{
|
|
||||||
Model.DestinationPath = dialogResult;
|
|
||||||
_parametriSetup.AggiornaParametro("LastDestinationFolder", dialogResult);
|
|
||||||
_parametriSetup.SalvaParametriSetup();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnSelectLogoFileRequested(object sender, EventArgs e)
|
|
||||||
{
|
|
||||||
var dialog = new OpenFileDialog();
|
|
||||||
dialog.Filter = "Image Files|*.jpg;*.jpeg;*.png;*.bmp;*.gif";
|
|
||||||
if (!string.IsNullOrWhiteSpace(Model.LogoFile))
|
|
||||||
{
|
|
||||||
dialog.FileName = Model.LogoFile;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var lastLogoFolder = _parametriSetup.LeggiParametroString("LastLogoFolder");
|
|
||||||
if (!string.IsNullOrWhiteSpace(lastLogoFolder) && Directory.Exists(lastLogoFolder))
|
|
||||||
{
|
|
||||||
dialog.InitialDirectory = lastLogoFolder;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dialog.ShowDialog() == DialogResult.OK)
|
|
||||||
{
|
|
||||||
Model.LogoFile = dialog.FileName;
|
|
||||||
UpdateLogoPictureBox(Model.LogoFile);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var folder = Path.GetDirectoryName(dialog.FileName) ?? string.Empty;
|
|
||||||
if (!string.IsNullOrWhiteSpace(folder))
|
|
||||||
{
|
|
||||||
_parametriSetup.AggiornaParametro("LastLogoFolder", folder);
|
|
||||||
_parametriSetup.SalvaParametriSetup();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// ignore preferences save failures
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void OnSaveSettingsRequested(object sender, string e)
|
|
||||||
{
|
|
||||||
var saveDialog = new SaveFileDialog
|
|
||||||
{
|
|
||||||
Filter = "Setup (*.xml)|*.xml|All valid files (*.*)|*.*",
|
|
||||||
FilterIndex = 0,
|
|
||||||
RestoreDirectory = true
|
|
||||||
};
|
|
||||||
var lastSettings = _parametriSetup.LeggiParametroString("LastSettingsFolder");
|
|
||||||
if (!string.IsNullOrWhiteSpace(lastSettings) && Directory.Exists(lastSettings))
|
|
||||||
saveDialog.InitialDirectory = lastSettings;
|
|
||||||
|
|
||||||
if (saveDialog.ShowDialog() != DialogResult.OK) return;
|
|
||||||
|
|
||||||
await Model.SaveSettingsToFileAsync(saveDialog.FileName);
|
|
||||||
Text = "Image Catalog - " + Path.GetFileName(saveDialog.FileName);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var folder = Path.GetDirectoryName(saveDialog.FileName) ?? string.Empty;
|
|
||||||
if (!string.IsNullOrWhiteSpace(folder))
|
|
||||||
{
|
|
||||||
_parametriSetup.AggiornaParametro("LastSettingsFolder", folder);
|
|
||||||
_parametriSetup.SalvaParametriSetup();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void OnLoadSettingsRequested(object sender, string e)
|
|
||||||
{
|
|
||||||
var openDialog = new OpenFileDialog
|
|
||||||
{
|
|
||||||
Filter = "Setup (*.xml)|*.xml|All valid files (*.*)|*.*",
|
|
||||||
FilterIndex = 0,
|
|
||||||
RestoreDirectory = true
|
|
||||||
};
|
|
||||||
|
|
||||||
var lastSettings = _parametriSetup.LeggiParametroString("LastSettingsFolder");
|
|
||||||
if (!string.IsNullOrWhiteSpace(lastSettings) && Directory.Exists(lastSettings))
|
|
||||||
openDialog.InitialDirectory = lastSettings;
|
|
||||||
|
|
||||||
if (openDialog.ShowDialog() != DialogResult.OK) return;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await Model.LoadSettingsFromFileAsync(openDialog.FileName);
|
|
||||||
|
|
||||||
// Explicitly ensure UI is enabled after loading
|
|
||||||
Model.UiEnabled = true;
|
|
||||||
|
|
||||||
// If a logo path was stored in the settings, try to resolve and display it.
|
|
||||||
// The stored path may be absolute or relative to the settings file location.
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var storedLogo = Model.LogoFile;
|
|
||||||
// Trim whitespace and surrounding quotes which may be present in saved settings
|
|
||||||
if (!string.IsNullOrWhiteSpace(storedLogo))
|
|
||||||
{
|
|
||||||
storedLogo = storedLogo.Trim();
|
|
||||||
storedLogo = storedLogo.Trim('"');
|
|
||||||
}
|
|
||||||
if (!string.IsNullOrWhiteSpace(storedLogo))
|
|
||||||
{
|
|
||||||
string resolved = storedLogo;
|
|
||||||
// If not rooted, try to resolve relative to the settings file folder
|
|
||||||
var settingsFolder = Path.GetDirectoryName(openDialog.FileName) ?? string.Empty;
|
|
||||||
if (!Path.IsPathRooted(resolved) && !string.IsNullOrWhiteSpace(settingsFolder))
|
|
||||||
{
|
|
||||||
var candidate = Path.Combine(settingsFolder, resolved);
|
|
||||||
if (File.Exists(candidate)) resolved = candidate;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If rooted but file doesn't exist, try filename near settings file
|
|
||||||
if (!File.Exists(resolved) && !string.IsNullOrWhiteSpace(settingsFolder))
|
|
||||||
{
|
|
||||||
var candidate2 = Path.Combine(settingsFolder, Path.GetFileName(resolved));
|
|
||||||
if (File.Exists(candidate2)) resolved = candidate2;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (File.Exists(resolved))
|
|
||||||
{
|
|
||||||
// Update the model so data-bound controls reflect the resolved absolute path
|
|
||||||
Model.LogoFile = resolved;
|
|
||||||
UpdateLogoPictureBox(resolved);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogDebug(ex, "Error resolving logo path after loading settings");
|
|
||||||
}
|
|
||||||
|
|
||||||
Text = "Image Catalog - " + Path.GetFileName(openDialog.FileName);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var folder = Path.GetDirectoryName(openDialog.FileName) ?? string.Empty;
|
|
||||||
if (!string.IsNullOrWhiteSpace(folder))
|
|
||||||
{
|
|
||||||
_parametriSetup.AggiornaParametro("LastSettingsFolder", folder);
|
|
||||||
_parametriSetup.SalvaParametriSetup();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// ignore preferences save failures
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogInformation($"Settings loaded successfully from {openDialog.FileName}");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error loading settings");
|
|
||||||
MessageBox.Show($"Error loading settings: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void MainForm_FormClosing(object? sender, FormClosingEventArgs e)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Persist last-used dialogs paths (user preferences)
|
|
||||||
// These keys are managed independently from settings files
|
|
||||||
// and must be saved when the form closes.
|
|
||||||
_parametriSetup.AggiornaParametro("LastSourceFolder", Model.SourcePath ?? string.Empty);
|
|
||||||
_parametriSetup.AggiornaParametro("LastDestinationFolder", Model.DestinationPath ?? string.Empty);
|
|
||||||
_parametriSetup.AggiornaParametro("LastLogoFolder", Path.GetDirectoryName(Model.LogoFile ?? string.Empty) ?? string.Empty);
|
|
||||||
_parametriSetup.SalvaParametriSetup();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger?.LogWarning(ex, "Failed to save user preferences on exit");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnSelectColorRequested(object sender, EventArgs e)
|
|
||||||
{
|
|
||||||
var colorDialog = new ColorDialog
|
|
||||||
{
|
|
||||||
AllowFullOpen = true
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(Model.TextColorRGB))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
colorDialog.Color = ColorTranslator.FromHtml(Model.TextColorRGB);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// Invalid color, use default
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (colorDialog.ShowDialog() == DialogResult.OK)
|
|
||||||
{
|
|
||||||
Model.TextColorRGB = ColorTranslator.ToHtml(colorDialog.Color);
|
|
||||||
TextBox34.BackColor = colorDialog.Color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateLogoPictureBox(string logoPath)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Load image via System.Drawing for preview so we can use MakeTransparent when requested
|
|
||||||
using var img = System.Drawing.Image.FromFile(logoPath);
|
|
||||||
|
|
||||||
System.Drawing.Bitmap previewBmp;
|
|
||||||
// If using color-key transparency and a color is selected, apply MakeTransparent for preview
|
|
||||||
if (Model.UseTransparentColor && !string.IsNullOrWhiteSpace(Model.TransparentColor))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var key = ColorTranslator.FromHtml(Model.TransparentColor);
|
|
||||||
previewBmp = new System.Drawing.Bitmap(img.Width, img.Height, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
|
|
||||||
using (var g = System.Drawing.Graphics.FromImage(previewBmp))
|
|
||||||
{
|
|
||||||
g.Clear(System.Drawing.Color.Transparent);
|
|
||||||
g.DrawImage(img, 0, 0, img.Width, img.Height);
|
|
||||||
}
|
|
||||||
// Apply exact color-key transparency
|
|
||||||
previewBmp.MakeTransparent(key);
|
|
||||||
PictureBox3.BackColor = key;
|
|
||||||
PictureBox3.Visible = true;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
previewBmp = new System.Drawing.Bitmap(img);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
previewBmp = new System.Drawing.Bitmap(img);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resize preview to fit into PictureBox1 while preserving aspect ratio
|
|
||||||
// Resize preview to fit into PictureBox1 while preserving aspect ratio
|
|
||||||
var boxW = PictureBox1.ClientSize.Width > 0 ? PictureBox1.ClientSize.Width : 449;
|
|
||||||
var boxH = PictureBox1.ClientSize.Height > 0 ? PictureBox1.ClientSize.Height : 369;
|
|
||||||
var ratio = Math.Min((double)boxW / previewBmp.Width, (double)boxH / previewBmp.Height);
|
|
||||||
var destW = Math.Max(1, (int)(previewBmp.Width * ratio));
|
|
||||||
var destH = Math.Max(1, (int)(previewBmp.Height * ratio));
|
|
||||||
var scaled = new System.Drawing.Bitmap(destW, destH, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
|
|
||||||
using (var g = System.Drawing.Graphics.FromImage(scaled))
|
|
||||||
{
|
|
||||||
g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
|
|
||||||
g.Clear(System.Drawing.Color.Transparent);
|
|
||||||
// Center the image in the PictureBox
|
|
||||||
var offsetX = Math.Max(0, (boxW - destW) / 2);
|
|
||||||
var offsetY = Math.Max(0, (boxH - destH) / 2);
|
|
||||||
using var canvas = new System.Drawing.Bitmap(boxW, boxH, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
|
|
||||||
using (var cg = System.Drawing.Graphics.FromImage(canvas))
|
|
||||||
{
|
|
||||||
cg.Clear(System.Drawing.Color.Transparent);
|
|
||||||
cg.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
|
|
||||||
cg.DrawImage(previewBmp, offsetX, offsetY, destW, destH);
|
|
||||||
}
|
|
||||||
g.DrawImage(canvas, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set PictureBox1 image (dispose previous)
|
|
||||||
var old = PictureBox1.Image;
|
|
||||||
PictureBox1.SizeMode = PictureBoxSizeMode.CenterImage;
|
|
||||||
PictureBox1.Image = scaled;
|
|
||||||
old?.Dispose();
|
|
||||||
previewBmp.Dispose();
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// Image loading failed, ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateLogoPreviewWithColorKey(string logoPath, Color keyColor)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var img = (Bitmap)Image.FromFile(logoPath);
|
|
||||||
// Create ARGB copy
|
|
||||||
var bmp = new Bitmap(img.Width, img.Height, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
|
|
||||||
using (var g = Graphics.FromImage(bmp))
|
|
||||||
{
|
|
||||||
g.DrawImage(img, 0, 0, img.Width, img.Height);
|
|
||||||
}
|
|
||||||
|
|
||||||
bmp.MakeTransparent(keyColor);
|
|
||||||
|
|
||||||
// Resize to PictureBox1 size similar to previous logic
|
|
||||||
Bitmap finalBmp;
|
|
||||||
if (bmp.Height >= bmp.Width)
|
|
||||||
{
|
|
||||||
finalBmp = new Bitmap(bmp, new Size((int)(160 * bmp.Width / (double)bmp.Height), 160));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
finalBmp = new Bitmap(bmp, new Size(160, (int)(160 * bmp.Height / (double)bmp.Width)));
|
|
||||||
}
|
|
||||||
|
|
||||||
PictureBox1.Image = finalBmp;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// ignore preview failures
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setLabel18Text(string text)
|
|
||||||
{
|
|
||||||
if (Label18.InvokeRequired)
|
|
||||||
{
|
|
||||||
Label18.Invoke(new Action<string>(setLabel18Text), text);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Label18.Text = text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class PicInfo
|
|
||||||
{
|
|
||||||
public DirectoryInfo DirSource, DirDest, DirDestStart;
|
|
||||||
public string NomeImmagine;
|
|
||||||
|
|
||||||
public PicInfo(DirectoryInfo Dir_Source, DirectoryInfo Dir_Dest, DirectoryInfo Dir_DestStart,
|
|
||||||
string Nome_Immagine)
|
|
||||||
{
|
|
||||||
DirSource = Dir_Source;
|
|
||||||
DirDest = Dir_Dest;
|
|
||||||
DirDestStart = Dir_DestStart;
|
|
||||||
NomeImmagine = Nome_Immagine;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
@ -1,135 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<root>
|
|
||||||
<!--
|
|
||||||
Microsoft ResX Schema
|
|
||||||
|
|
||||||
Version 2.0
|
|
||||||
|
|
||||||
The primary goals of this format is to allow a simple XML format
|
|
||||||
that is mostly human readable. The generation and parsing of the
|
|
||||||
various data types are done through the TypeConverter classes
|
|
||||||
associated with the data types.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
... ado.net/XML headers & schema ...
|
|
||||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
|
||||||
<resheader name="version">2.0</resheader>
|
|
||||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
|
||||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
|
||||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
|
||||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
|
||||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
|
||||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
|
||||||
</data>
|
|
||||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
|
||||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
|
||||||
<comment>This is a comment</comment>
|
|
||||||
</data>
|
|
||||||
|
|
||||||
There are any number of "resheader" rows that contain simple
|
|
||||||
name/value pairs.
|
|
||||||
|
|
||||||
Each data row contains a name, and value. The row also contains a
|
|
||||||
type or mimetype. Type corresponds to a .NET class that support
|
|
||||||
text/value conversion through the TypeConverter architecture.
|
|
||||||
Classes that don't support this are serialized and stored with the
|
|
||||||
mimetype set.
|
|
||||||
|
|
||||||
The mimetype is used for serialized objects, and tells the
|
|
||||||
ResXResourceReader how to depersist the object. This is currently not
|
|
||||||
extensible. For a given mimetype the value must be set accordingly:
|
|
||||||
|
|
||||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
|
||||||
that the ResXResourceWriter will generate, however the reader can
|
|
||||||
read any of the formats listed below.
|
|
||||||
|
|
||||||
mimetype: application/x-microsoft.net.object.binary.base64
|
|
||||||
value : The object must be serialized with
|
|
||||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
|
||||||
: and then encoded with base64 encoding.
|
|
||||||
|
|
||||||
mimetype: application/x-microsoft.net.object.soap.base64
|
|
||||||
value : The object must be serialized with
|
|
||||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
|
||||||
: and then encoded with base64 encoding.
|
|
||||||
|
|
||||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
|
||||||
value : The object must be serialized into a byte array
|
|
||||||
: using a System.ComponentModel.TypeConverter
|
|
||||||
: and then encoded with base64 encoding.
|
|
||||||
-->
|
|
||||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
|
||||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
|
||||||
<xsd:element name="root" msdata:IsDataSet="true">
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:choice maxOccurs="unbounded">
|
|
||||||
<xsd:element name="metadata">
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:sequence>
|
|
||||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
|
||||||
</xsd:sequence>
|
|
||||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
|
||||||
<xsd:attribute name="type" type="xsd:string" />
|
|
||||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
|
||||||
<xsd:attribute ref="xml:space" />
|
|
||||||
</xsd:complexType>
|
|
||||||
</xsd:element>
|
|
||||||
<xsd:element name="assembly">
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:attribute name="alias" type="xsd:string" />
|
|
||||||
<xsd:attribute name="name" type="xsd:string" />
|
|
||||||
</xsd:complexType>
|
|
||||||
</xsd:element>
|
|
||||||
<xsd:element name="data">
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:sequence>
|
|
||||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
|
||||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
|
||||||
</xsd:sequence>
|
|
||||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
|
||||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
|
||||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
|
||||||
<xsd:attribute ref="xml:space" />
|
|
||||||
</xsd:complexType>
|
|
||||||
</xsd:element>
|
|
||||||
<xsd:element name="resheader">
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:sequence>
|
|
||||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
|
||||||
</xsd:sequence>
|
|
||||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
|
||||||
</xsd:complexType>
|
|
||||||
</xsd:element>
|
|
||||||
</xsd:choice>
|
|
||||||
</xsd:complexType>
|
|
||||||
</xsd:element>
|
|
||||||
</xsd:schema>
|
|
||||||
<resheader name="resmimetype">
|
|
||||||
<value>text/microsoft-resx</value>
|
|
||||||
</resheader>
|
|
||||||
<resheader name="version">
|
|
||||||
<value>2.0</value>
|
|
||||||
</resheader>
|
|
||||||
<resheader name="reader">
|
|
||||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
|
||||||
</resheader>
|
|
||||||
<resheader name="writer">
|
|
||||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
|
||||||
</resheader>
|
|
||||||
<metadata name="bindingSource1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
|
||||||
<value>586, 17</value>
|
|
||||||
</metadata>
|
|
||||||
<metadata name="dataModelBindingSource.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
|
||||||
<value>120, 17</value>
|
|
||||||
</metadata>
|
|
||||||
<metadata name="timer1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
|
||||||
<value>17, 17</value>
|
|
||||||
</metadata>
|
|
||||||
<metadata name="dataModelBindingSource1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
|
||||||
<value>349, 17</value>
|
|
||||||
</metadata>
|
|
||||||
<metadata name="colorDialog1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
|
||||||
<value>802, 17</value>
|
|
||||||
</metadata>
|
|
||||||
</root>
|
|
||||||
|
|
@ -1,510 +0,0 @@
|
||||||
<controls:MetroWindow x:Class="ImageCatalog_2.MainWindow"
|
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
|
||||||
xmlns:iconPacks="http://metro.mahapps.com/winfx/xaml/iconpacks"
|
|
||||||
xmlns:controls="http://metro.mahapps.com/winfx/xaml/controls"
|
|
||||||
mc:Ignorable="d"
|
|
||||||
Title="Image Catalog - WPF" Height="540" Width="800"
|
|
||||||
Background="{DynamicResource WindowBackgroundBrush}" Foreground="{DynamicResource ControlForegroundBrush}"
|
|
||||||
GlowBrush="{DynamicResource AccentBrush}">
|
|
||||||
<controls:MetroWindow.Resources>
|
|
||||||
<ResourceDictionary>
|
|
||||||
<!-- Default (Light) theme resources placed at top-level so DynamicResource lookups resolve -->
|
|
||||||
<!-- style moved later to avoid early resource lookup -->
|
|
||||||
<SolidColorBrush x:Key="WindowBackgroundBrush" Color="White" />
|
|
||||||
<SolidColorBrush x:Key="ControlBackgroundBrush" Color="White" />
|
|
||||||
<SolidColorBrush x:Key="ControlForegroundBrush" Color="Black" />
|
|
||||||
<SolidColorBrush x:Key="BorderBrush" Color="#DDD" />
|
|
||||||
<SolidColorBrush x:Key="AccentBrush" Color="#0078D7" />
|
|
||||||
<SolidColorBrush x:Key="DataGridBackgroundBrush" Color="White" />
|
|
||||||
<SolidColorBrush x:Key="DataGridForegroundBrush" Color="Black" />
|
|
||||||
|
|
||||||
<!-- Also keep named theme dictionaries for future switching if needed -->
|
|
||||||
<ResourceDictionary x:Key="LightTheme">
|
|
||||||
<SolidColorBrush x:Key="WindowBackgroundBrush.Light" Color="White" />
|
|
||||||
<SolidColorBrush x:Key="ControlBackgroundBrush.Light" Color="White" />
|
|
||||||
<SolidColorBrush x:Key="ControlForegroundBrush.Light" Color="Black" />
|
|
||||||
<SolidColorBrush x:Key="BorderBrush.Light" Color="#DDD" />
|
|
||||||
<SolidColorBrush x:Key="AccentBrush.Light" Color="#0078D7" />
|
|
||||||
<SolidColorBrush x:Key="DataGridBackgroundBrush.Light" Color="White" />
|
|
||||||
<SolidColorBrush x:Key="DataGridForegroundBrush.Light" Color="Black" />
|
|
||||||
</ResourceDictionary>
|
|
||||||
|
|
||||||
<ResourceDictionary x:Key="DarkTheme">
|
|
||||||
<SolidColorBrush x:Key="WindowBackgroundBrush.Dark" Color="#1E1E1E" />
|
|
||||||
<SolidColorBrush x:Key="ControlBackgroundBrush.Dark" Color="#252526" />
|
|
||||||
<SolidColorBrush x:Key="ControlForegroundBrush.Dark" Color="#E6E6E6" />
|
|
||||||
<SolidColorBrush x:Key="BorderBrush.Dark" Color="#3A3A3A" />
|
|
||||||
<SolidColorBrush x:Key="AccentBrush.Dark" Color="#0A84FF" />
|
|
||||||
<SolidColorBrush x:Key="DataGridBackgroundBrush.Dark" Color="#252526" />
|
|
||||||
<SolidColorBrush x:Key="DataGridForegroundBrush.Dark" Color="#E6E6E6" />
|
|
||||||
</ResourceDictionary>
|
|
||||||
|
|
||||||
<!-- Improve tab header visuals so selected tab and boundaries are clear -->
|
|
||||||
<Style TargetType="controls:MetroTabItem">
|
|
||||||
<Setter Property="Background" Value="Transparent" />
|
|
||||||
<Setter Property="Foreground" Value="{DynamicResource ControlForegroundBrush}" />
|
|
||||||
<Setter Property="Margin" Value="0,0,4,0" />
|
|
||||||
<Setter Property="Padding" Value="6,4" />
|
|
||||||
<Setter Property="MinWidth" Value="56" />
|
|
||||||
<Setter Property="Template">
|
|
||||||
<Setter.Value>
|
|
||||||
<ControlTemplate TargetType="controls:MetroTabItem">
|
|
||||||
<Border x:Name="Bd"
|
|
||||||
Background="{TemplateBinding Background}"
|
|
||||||
CornerRadius="4"
|
|
||||||
BorderThickness="0"
|
|
||||||
Padding="{TemplateBinding Padding}">
|
|
||||||
<ContentPresenter ContentSource="Header" HorizontalAlignment="Center" VerticalAlignment="Center" />
|
|
||||||
</Border>
|
|
||||||
<ControlTemplate.Triggers>
|
|
||||||
<Trigger Property="IsSelected" Value="True">
|
|
||||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource AccentBrush}" />
|
|
||||||
<Setter Property="Foreground" Value="{DynamicResource ControlForegroundBrush}" />
|
|
||||||
</Trigger>
|
|
||||||
<Trigger Property="IsMouseOver" Value="True">
|
|
||||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource BorderBrush}" />
|
|
||||||
</Trigger>
|
|
||||||
<Trigger Property="IsEnabled" Value="False">
|
|
||||||
<Setter Property="Opacity" Value="0.5" />
|
|
||||||
</Trigger>
|
|
||||||
</ControlTemplate.Triggers>
|
|
||||||
</ControlTemplate>
|
|
||||||
</Setter.Value>
|
|
||||||
</Setter>
|
|
||||||
</Style>
|
|
||||||
</ResourceDictionary>
|
|
||||||
</controls:MetroWindow.Resources>
|
|
||||||
<controls:MetroWindow.RightWindowCommands>
|
|
||||||
<controls:WindowCommands>
|
|
||||||
<!-- Show version in title area; theme toggle moved into window content -->
|
|
||||||
<TextBlock Name="VersionTextBlock" Text="" VerticalAlignment="Center" Margin="8,0,0,0" />
|
|
||||||
</controls:WindowCommands>
|
|
||||||
</controls:MetroWindow.RightWindowCommands>
|
|
||||||
|
|
||||||
<Grid>
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="*" />
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
|
|
||||||
<Grid Grid.Row="0" Margin="10">
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="3*" />
|
|
||||||
<!-- Make the live view/right side narrower -->
|
|
||||||
<ColumnDefinition Width="0.8*" />
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
|
|
||||||
<!-- Left: Tabs -->
|
|
||||||
<controls:MetroTabControl Grid.Column="0" Margin="0,0,10,0">
|
|
||||||
<controls:MetroTabItem>
|
|
||||||
<controls:MetroTabItem.Header>
|
|
||||||
<StackPanel Orientation="Horizontal">
|
|
||||||
<iconPacks:PackIconMaterial Kind="CogOutline" Width="16" Height="16" Foreground="{StaticResource AccentBrush}" Margin="0,0,6,0" />
|
|
||||||
<TextBlock Text="Generale" />
|
|
||||||
</StackPanel>
|
|
||||||
</controls:MetroTabItem.Header>
|
|
||||||
<ScrollViewer>
|
|
||||||
<StackPanel Margin="8">
|
|
||||||
<TextBlock Text="Percorsi" FontWeight="Bold" />
|
|
||||||
<StackPanel Margin="0,6,0,0">
|
|
||||||
<!-- Source path row: textbox with pick and open buttons aligned to the end -->
|
|
||||||
<Grid Margin="0,0,0,6">
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="Auto" />
|
|
||||||
<ColumnDefinition Width="*" />
|
|
||||||
<ColumnDefinition Width="Auto" />
|
|
||||||
<ColumnDefinition Width="Auto" />
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
<TextBlock Text="Sorgente:" VerticalAlignment="Center" Margin="0,0,8,0" Grid.Column="0" />
|
|
||||||
<TextBox Text="{Binding SourcePath, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Grid.Column="1" VerticalAlignment="Center" />
|
|
||||||
<Button Width="88" Margin="8,0,0,0" Command="{Binding SelectSourceFolderCommand}" Grid.Column="2">
|
|
||||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
|
||||||
<iconPacks:PackIconMaterial Kind="FolderOpenOutline" Width="14" Height="14" Foreground="{StaticResource AccentBrush}" Margin="0,0,6,0" />
|
|
||||||
<TextBlock Text="Scegli..." />
|
|
||||||
</StackPanel>
|
|
||||||
</Button>
|
|
||||||
<Button Width="56" Margin="8,0,0,0" Click="OpenSourceFolder_Click" Grid.Column="3">
|
|
||||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
|
||||||
<iconPacks:PackIconMaterial Kind="Folder" Width="14" Height="14" Foreground="{StaticResource AccentBrush}" Margin="0,0,6,0" />
|
|
||||||
<TextBlock Text="Apri" />
|
|
||||||
</StackPanel>
|
|
||||||
</Button>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<!-- Destination path row -->
|
|
||||||
<Grid>
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="Auto" />
|
|
||||||
<ColumnDefinition Width="*" />
|
|
||||||
<ColumnDefinition Width="Auto" />
|
|
||||||
<ColumnDefinition Width="Auto" />
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
<TextBlock Text="Destinazione:" VerticalAlignment="Center" Margin="0,0,8,0" Grid.Column="0" />
|
|
||||||
<TextBox Text="{Binding DestinationPath, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Grid.Column="1" VerticalAlignment="Center" />
|
|
||||||
<Button Width="88" Margin="8,0,0,0" Command="{Binding SelectDestinationFolderCommand}" Grid.Column="2">
|
|
||||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
|
||||||
<iconPacks:PackIconMaterial Kind="FolderOpenOutline" Width="14" Height="14" Foreground="{StaticResource AccentBrush}" Margin="0,0,6,0" />
|
|
||||||
<TextBlock Text="Scegli..." />
|
|
||||||
</StackPanel>
|
|
||||||
</Button>
|
|
||||||
<Button Width="56" Margin="8,0,0,0" Click="OpenDestinationFolder_Click" Grid.Column="3">
|
|
||||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
|
||||||
<iconPacks:PackIconMaterial Kind="Folder" Width="14" Height="14" Foreground="{StaticResource AccentBrush}" Margin="0,0,6,0" />
|
|
||||||
<TextBlock Text="Apri" />
|
|
||||||
</StackPanel>
|
|
||||||
</Button>
|
|
||||||
</Grid>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<TextBlock Text="Opzioni" FontWeight="Bold" Margin="0,12,0,0" />
|
|
||||||
<StackPanel Orientation="Vertical" Margin="0,6,0,0">
|
|
||||||
<CheckBox Content="Forza JPEG" IsChecked="{Binding ForceJpeg}" />
|
|
||||||
<CheckBox Content="Aggiorna sottodirectory" IsChecked="{Binding UpdateSubdirectories}" />
|
|
||||||
<CheckBox Content="Crea sottocartelle" IsChecked="{Binding CreateSubfolders}" />
|
|
||||||
<CheckBox Content="Sovrascrivi immagini" IsChecked="{Binding OverwriteImages}" />
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<TextBlock Text="Elaborazione" FontWeight="Bold" Margin="0,12,0,0" />
|
|
||||||
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
|
|
||||||
<TextBlock Text="Threads:" VerticalAlignment="Center" />
|
|
||||||
<TextBox Text="{Binding ThreadsCount, Mode=TwoWay}" Width="60" Margin="8,0,0,0" />
|
|
||||||
<TextBlock Text="Chunk:" VerticalAlignment="Center" Margin="12,0,0,0" />
|
|
||||||
<TextBox Text="{Binding ChunkSize, Mode=TwoWay}" Width="60" Margin="8,0,0,0" />
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<TextBlock Text="Divisione cartelle" FontWeight="Bold" Margin="0,12,0,0" />
|
|
||||||
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
|
|
||||||
<TextBlock Text="File per cartella:" VerticalAlignment="Center" />
|
|
||||||
<TextBox Text="{Binding FilesPerFolder}" Width="60" Margin="8,0,0,0" />
|
|
||||||
<TextBlock Text="Suffisso:" VerticalAlignment="Center" Margin="12,0,0,0" />
|
|
||||||
<TextBox Text="{Binding FolderSuffix}" Width="120" Margin="8,0,0,0" />
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<TextBlock Text="Numerazione" FontWeight="Bold" Margin="0,12,0,0" />
|
|
||||||
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
|
|
||||||
<RadioButton Content="Progressiva" IsChecked="{Binding UseProgressiveNumbering}" GroupName="Num" />
|
|
||||||
<RadioButton Content="Per file" IsChecked="{Binding UseFileNumbering}" GroupName="Num" Margin="8,0,0,0" />
|
|
||||||
<TextBlock Text="Cifre:" VerticalAlignment="Center" Margin="12,0,0,0" />
|
|
||||||
<TextBox Text="{Binding CounterDigits}" Width="40" Margin="8,0,0,0" />
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<TextBlock Text="Libreria Immagini" FontWeight="Bold" Margin="0,12,0,0" />
|
|
||||||
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
|
|
||||||
<RadioButton Content="System.Graphics" IsChecked="{Binding UseSystemGraphics}" GroupName="Lib" />
|
|
||||||
<RadioButton Content="ImageSharp" IsChecked="{Binding UseImageSharp}" GroupName="Lib" Margin="8,0,0,0" />
|
|
||||||
</StackPanel>
|
|
||||||
</StackPanel>
|
|
||||||
</ScrollViewer>
|
|
||||||
</controls:MetroTabItem>
|
|
||||||
|
|
||||||
<controls:MetroTabItem>
|
|
||||||
<controls:MetroTabItem.Header>
|
|
||||||
<StackPanel Orientation="Horizontal">
|
|
||||||
<iconPacks:PackIconMaterial Kind="FormatLetterCase" Width="16" Height="16" Foreground="{StaticResource AccentBrush}" Margin="0,0,6,0" />
|
|
||||||
<TextBlock Text="Testo" />
|
|
||||||
</StackPanel>
|
|
||||||
</controls:MetroTabItem.Header>
|
|
||||||
<ScrollViewer>
|
|
||||||
<StackPanel Margin="8">
|
|
||||||
<TextBlock Text="Testo Orizzontale" FontWeight="Bold" />
|
|
||||||
<TextBox Text="{Binding HorizontalText, Mode=TwoWay}" />
|
|
||||||
|
|
||||||
<TextBlock Text="Testo Verticale" FontWeight="Bold" Margin="0,8,0,0" />
|
|
||||||
<TextBox Text="{Binding VerticalText, Mode=TwoWay}" AcceptsReturn="True" TextWrapping="Wrap" MinLines="4" VerticalScrollBarVisibility="Auto" />
|
|
||||||
|
|
||||||
<TextBlock Text="Font" FontWeight="Bold" Margin="0,8,0,0" />
|
|
||||||
<StackPanel Orientation="Horizontal">
|
|
||||||
<ComboBox ItemsSource="{Binding AvailableFonts}" SelectedItem="{Binding FontName}" Width="250" />
|
|
||||||
<TextBox Text="{Binding FontSize}" Width="60" Margin="8,0,0,0" />
|
|
||||||
<CheckBox Content="Grassetto" IsChecked="{Binding FontBold}" Margin="8,0,0,0" />
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<TextBlock Text="Colore testo" FontWeight="Bold" Margin="0,8,0,0" />
|
|
||||||
<StackPanel Orientation="Horizontal">
|
|
||||||
<TextBox Text="{Binding TextColorRGB}" Width="120" />
|
|
||||||
<Button Content="Seleziona colore" Command="{Binding SelectColorCommand}" Margin="8,0,0,0" />
|
|
||||||
<!-- MahApps color picker for direct color selection -->
|
|
||||||
<controls:ColorPicker SelectedColor="{Binding TextColor}" Width="160" Margin="8,0,0,0" />
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<TextBlock Text="Dimensioni verticale" FontWeight="Bold" Margin="0,8,0,0" />
|
|
||||||
<StackPanel Orientation="Horizontal">
|
|
||||||
<TextBlock Text="Size:" VerticalAlignment="Center" />
|
|
||||||
<TextBox Text="{Binding VerticalTextSize}" Width="60" Margin="8,0,0,0" />
|
|
||||||
<TextBlock Text="Margin:" VerticalAlignment="Center" Margin="12,0,0,0" />
|
|
||||||
<TextBox Text="{Binding VerticalTextMargin}" Width="60" Margin="8,0,0,0" />
|
|
||||||
</StackPanel>
|
|
||||||
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
|
|
||||||
<TextBlock Text="Trasparenza testo:" VerticalAlignment="Center" />
|
|
||||||
<TextBox Text="{Binding TextTransparency}" Width="60" Margin="8,0,0,0" />
|
|
||||||
<TextBlock Text="Margine testo:" VerticalAlignment="Center" Margin="12,0,0,0" />
|
|
||||||
<TextBox Text="{Binding TextMargin}" Width="60" Margin="8,0,0,0" />
|
|
||||||
</StackPanel>
|
|
||||||
<!-- Tempo Gara section moved from Foto tab -->
|
|
||||||
<TextBlock Text="Tempo Gara" FontWeight="Bold" Margin="0,12,0,0" />
|
|
||||||
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
|
|
||||||
<CheckBox Content="Aggiungi Orario" IsChecked="{Binding AddTime}" />
|
|
||||||
<CheckBox Content="Aggiungi tempo gara" IsChecked="{Binding AddRaceTime}" Margin="12,0,0,0" />
|
|
||||||
</StackPanel>
|
|
||||||
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
|
|
||||||
<TextBlock Text="Partenza:" VerticalAlignment="Center" />
|
|
||||||
<controls:DateTimePicker SelectedDateTime="{Binding RaceTime}" IsEnabled="{Binding AddRaceTime}" Margin="8,0,0,0" Width="200" />
|
|
||||||
<TextBox Text="{Binding TimeLabel}" Width="220" Margin="12,0,0,0" />
|
|
||||||
</StackPanel>
|
|
||||||
</StackPanel>
|
|
||||||
</ScrollViewer>
|
|
||||||
</controls:MetroTabItem>
|
|
||||||
|
|
||||||
<controls:MetroTabItem>
|
|
||||||
<controls:MetroTabItem.Header>
|
|
||||||
<StackPanel Orientation="Horizontal">
|
|
||||||
<iconPacks:PackIconMaterial Kind="CameraFrontVariant" Width="16" Height="16" Foreground="{StaticResource AccentBrush}" Margin="0,0,6,0" />
|
|
||||||
<TextBlock Text="Foto" />
|
|
||||||
</StackPanel>
|
|
||||||
</controls:MetroTabItem.Header>
|
|
||||||
<ScrollViewer>
|
|
||||||
<StackPanel Margin="8">
|
|
||||||
<TextBlock Text="Dimensioni foto grandi" FontWeight="Bold" />
|
|
||||||
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
|
|
||||||
<TextBox Text="{Binding PhotoBigWidth}" Width="80" />
|
|
||||||
<TextBox Text="{Binding PhotoBigHeight}" Width="80" Margin="8,0,0,0" />
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<TextBlock Text="Opzioni foto" FontWeight="Bold" Margin="0,8,0,0" />
|
|
||||||
<StackPanel Orientation="Vertical" Margin="0,6,0,0">
|
|
||||||
<CheckBox Content="Mantieni dimensioni originali" IsChecked="{Binding KeepOriginalDimensions}" />
|
|
||||||
<CheckBox Content="Rotazione automatica" IsChecked="{Binding AutomaticRotation}" />
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<TextBlock Text="JPEG" FontWeight="Bold" Margin="0,8,0,0" />
|
|
||||||
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
|
|
||||||
<TextBlock Text="Qualità:" VerticalAlignment="Center" />
|
|
||||||
<TextBox Text="{Binding JpegQuality}" Width="60" Margin="8,0,0,0" />
|
|
||||||
<TextBlock Text="Miniature Qualità:" VerticalAlignment="Center" Margin="12,0,0,0" />
|
|
||||||
<TextBox Text="{Binding JpegQualityThumbnail}" Width="60" Margin="8,0,0,0" />
|
|
||||||
</StackPanel>
|
|
||||||
</StackPanel>
|
|
||||||
</ScrollViewer>
|
|
||||||
</controls:MetroTabItem>
|
|
||||||
|
|
||||||
<controls:MetroTabItem>
|
|
||||||
<controls:MetroTabItem.Header>
|
|
||||||
<StackPanel Orientation="Horizontal">
|
|
||||||
<iconPacks:PackIconMaterial Kind="Image" Width="16" Height="16" Foreground="{StaticResource AccentBrush}" Margin="0,0,6,0" />
|
|
||||||
<TextBlock Text="Miniature" />
|
|
||||||
</StackPanel>
|
|
||||||
</controls:MetroTabItem.Header>
|
|
||||||
<ScrollViewer>
|
|
||||||
<StackPanel Margin="8">
|
|
||||||
<TextBlock Text="Miniature" FontWeight="Bold" />
|
|
||||||
<CheckBox Content="Crea miniature" IsChecked="{Binding CreateThumbnails}" Margin="0,6,0,0" />
|
|
||||||
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
|
|
||||||
<TextBlock Text="Prefisso:" VerticalAlignment="Center" />
|
|
||||||
<TextBox Text="{Binding ThumbnailPrefix}" Width="120" Margin="8,0,0,0" />
|
|
||||||
</StackPanel>
|
|
||||||
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
|
|
||||||
<TextBox Text="{Binding ThumbnailWidth}" Width="80" />
|
|
||||||
<TextBox Text="{Binding ThumbnailHeight}" Width="80" Margin="8,0,0,0" />
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<!-- New unified thumbnail mode selector (Italian labels) -->
|
|
||||||
<StackPanel Orientation="Vertical" Margin="0,8,0,0">
|
|
||||||
<TextBlock Text="Modalità miniature:" VerticalAlignment="Center" />
|
|
||||||
<ComboBox SelectedIndex="{Binding ThumbnailOptionIndex, Mode=TwoWay}" Width="220" Margin="0,6,0,0">
|
|
||||||
<ComboBoxItem>Nessuna</ComboBoxItem>
|
|
||||||
<ComboBoxItem>Aggiungi scritta</ComboBoxItem>
|
|
||||||
<ComboBoxItem>Nome file</ComboBoxItem>
|
|
||||||
<ComboBoxItem>Aggiungi orario</ComboBoxItem>
|
|
||||||
<ComboBoxItem>Nome+Orario</ComboBoxItem>
|
|
||||||
<ComboBoxItem>Tempo gara</ComboBoxItem>
|
|
||||||
</ComboBox>
|
|
||||||
</StackPanel>
|
|
||||||
</StackPanel>
|
|
||||||
</ScrollViewer>
|
|
||||||
</controls:MetroTabItem>
|
|
||||||
|
|
||||||
<controls:MetroTabItem>
|
|
||||||
<controls:MetroTabItem.Header>
|
|
||||||
<StackPanel Orientation="Horizontal">
|
|
||||||
<iconPacks:PackIconMaterial Kind="ImageFilterCenterFocus" Width="16" Height="16" Foreground="{StaticResource AccentBrush}" Margin="0,0,6,0" />
|
|
||||||
<TextBlock Text="Logo" />
|
|
||||||
</StackPanel>
|
|
||||||
</controls:MetroTabItem.Header>
|
|
||||||
<ScrollViewer>
|
|
||||||
<StackPanel Margin="8">
|
|
||||||
<TextBlock Text="Logo" FontWeight="Bold" />
|
|
||||||
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
|
|
||||||
<Button Command="{Binding SelectLogoFileCommand}">
|
|
||||||
<StackPanel Orientation="Horizontal">
|
|
||||||
<iconPacks:PackIconMaterial Kind="ImageOutline" Width="14" Height="14" Foreground="{StaticResource AccentBrush}" Margin="0,0,6,0" />
|
|
||||||
<TextBlock Text="Seleziona logo" />
|
|
||||||
</StackPanel>
|
|
||||||
</Button>
|
|
||||||
<TextBlock Text="{Binding LogoFile}" Margin="8,0,0,0" VerticalAlignment="Center" Width="250" TextTrimming="CharacterEllipsis" />
|
|
||||||
</StackPanel>
|
|
||||||
<StackPanel Orientation="Horizontal" Margin="0,8,0,0">
|
|
||||||
<Image Name="LogoPreview" Width="160" Height="160" Stretch="Uniform" VerticalAlignment="Center" />
|
|
||||||
</StackPanel>
|
|
||||||
<StackPanel Orientation="Horizontal" Margin="0,8,0,0">
|
|
||||||
<TextBox Text="{Binding LogoWidth}" Width="80" />
|
|
||||||
<TextBox Text="{Binding LogoHeight}" Width="80" Margin="8,0,0,0" />
|
|
||||||
</StackPanel>
|
|
||||||
<CheckBox Content="Aggiungi logo" IsChecked="{Binding AddLogo}" Margin="0,8,0,0" />
|
|
||||||
<StackPanel Orientation="Horizontal" Margin="0,8,0,0">
|
|
||||||
<TextBlock Text="Margine:" VerticalAlignment="Center" />
|
|
||||||
<TextBox Text="{Binding LogoMargin}" Width="80" Margin="8,0,0,0" />
|
|
||||||
<TextBlock Text="Trasparenza:" VerticalAlignment="Center" Margin="12,0,0,0" />
|
|
||||||
<TextBox Text="{Binding LogoTransparency}" Width="60" Margin="8,0,0,0" />
|
|
||||||
<Button Command="{Binding SelectTransparentColorCommand}" Margin="8,0,0,0">
|
|
||||||
<StackPanel Orientation="Horizontal">
|
|
||||||
<iconPacks:PackIconMaterial Kind="PaletteOutline" Width="14" Height="14" Foreground="{StaticResource AccentBrush}" Margin="0,0,6,0" />
|
|
||||||
<TextBlock Text="Seleziona trasparente" />
|
|
||||||
</StackPanel>
|
|
||||||
</Button>
|
|
||||||
</StackPanel>
|
|
||||||
<StackPanel Orientation="Horizontal" Margin="0,8,0,0">
|
|
||||||
<TextBlock Text="Posizione:" VerticalAlignment="Center" />
|
|
||||||
<ComboBox ItemsSource="{Binding HorizontalAlignments}" SelectedItem="{Binding LogoHorizontalPosition}" Width="120" Margin="8,0,0,0" />
|
|
||||||
<ComboBox ItemsSource="{Binding VerticalPositions}" SelectedItem="{Binding LogoVerticalPosition}" Width="120" Margin="8,0,0,0" />
|
|
||||||
</StackPanel>
|
|
||||||
</StackPanel>
|
|
||||||
</ScrollViewer>
|
|
||||||
</controls:MetroTabItem>
|
|
||||||
|
|
||||||
<controls:MetroTabItem>
|
|
||||||
<controls:MetroTabItem.Header>
|
|
||||||
<StackPanel Orientation="Horizontal">
|
|
||||||
<iconPacks:PackIconMaterial Kind="Robot" Width="16" Height="16" Foreground="{StaticResource AccentBrush}" Margin="0,0,6,0" />
|
|
||||||
<TextBlock Text="AI" />
|
|
||||||
</StackPanel>
|
|
||||||
</controls:MetroTabItem.Header>
|
|
||||||
<ScrollViewer>
|
|
||||||
<StackPanel Margin="8">
|
|
||||||
<TextBlock Text="AI / OCR" FontWeight="Bold" />
|
|
||||||
<CheckBox Content="Estrai numeri dalle immagini" IsChecked="{Binding ExtractNumbers}" Margin="0,8,0,0" />
|
|
||||||
|
|
||||||
<TextBlock Text="Modelli" FontWeight="Bold" Margin="0,12,0,0" />
|
|
||||||
<Grid Margin="0,6,0,0">
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="Auto" />
|
|
||||||
<ColumnDefinition Width="*" />
|
|
||||||
<ColumnDefinition Width="Auto" />
|
|
||||||
<ColumnDefinition Width="Auto" />
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
<TextBlock Text="Cartella modelli:" VerticalAlignment="Center" Margin="0,0,8,0" Grid.Column="0" />
|
|
||||||
<TextBox Text="{Binding ModelsFolderPath, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Grid.Column="1" VerticalAlignment="Center" />
|
|
||||||
<Button Width="88" Margin="8,0,0,0" Command="{Binding SelectModelsFolderCommand}" Grid.Column="2">
|
|
||||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
|
||||||
<iconPacks:PackIconMaterial Kind="FolderSearchOutline" Width="14" Height="14" Foreground="{StaticResource AccentBrush}" Margin="0,0,6,0" />
|
|
||||||
<TextBlock Text="Scegli..." />
|
|
||||||
</StackPanel>
|
|
||||||
</Button>
|
|
||||||
<Button Width="56" Margin="8,0,0,0" Click="OpenModelsFolder_Click" Grid.Column="3">
|
|
||||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
|
||||||
<iconPacks:PackIconMaterial Kind="Folder" Width="14" Height="14" Foreground="{StaticResource AccentBrush}" Margin="0,0,6,0" />
|
|
||||||
<TextBlock Text="Apri" />
|
|
||||||
</StackPanel>
|
|
||||||
</Button>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<TextBlock Text="Output CSV" FontWeight="Bold" Margin="0,12,0,0" />
|
|
||||||
<Grid Margin="0,6,0,0">
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="Auto" />
|
|
||||||
<ColumnDefinition Width="*" />
|
|
||||||
<ColumnDefinition Width="Auto" />
|
|
||||||
<ColumnDefinition Width="Auto" />
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
<TextBlock Text="Percorso CSV:" VerticalAlignment="Center" Margin="0,0,8,0" Grid.Column="0" />
|
|
||||||
<TextBox Text="{Binding CsvOutputPath, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Grid.Column="1" VerticalAlignment="Center" />
|
|
||||||
<Button Width="88" Margin="8,0,0,0" Command="{Binding SelectCsvOutputCommand}" Grid.Column="2">
|
|
||||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
|
||||||
<iconPacks:PackIconMaterial Kind="FileFindOutline" Width="14" Height="14" Foreground="{StaticResource AccentBrush}" Margin="0,0,6,0" />
|
|
||||||
<TextBlock Text="Scegli..." />
|
|
||||||
</StackPanel>
|
|
||||||
</Button>
|
|
||||||
<Button Width="56" Margin="8,0,0,0" Click="OpenCsvOutputFolder_Click" Grid.Column="3">
|
|
||||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
|
||||||
<iconPacks:PackIconMaterial Kind="Folder" Width="14" Height="14" Foreground="{StaticResource AccentBrush}" Margin="0,0,6,0" />
|
|
||||||
<TextBlock Text="Apri" />
|
|
||||||
</StackPanel>
|
|
||||||
</Button>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<TextBlock Text="Anteprima risultati" FontWeight="Bold" Margin="0,12,0,0" />
|
|
||||||
<DataGrid ItemsSource="{Binding PreviewResults}" IsReadOnly="True" AutoGenerateColumns="False" Height="200" Margin="0,6,0,0">
|
|
||||||
<DataGrid.Columns>
|
|
||||||
<DataGridTextColumn Header="Path" Binding="{Binding Path}" Width="*" />
|
|
||||||
<DataGridTextColumn Header="Text" Binding="{Binding Text}" Width="2*" />
|
|
||||||
</DataGrid.Columns>
|
|
||||||
</DataGrid>
|
|
||||||
</StackPanel>
|
|
||||||
</ScrollViewer>
|
|
||||||
</controls:MetroTabItem>
|
|
||||||
</controls:MetroTabControl>
|
|
||||||
|
|
||||||
<!-- Right: Controls and live info -->
|
|
||||||
<StackPanel Grid.Column="1" Orientation="Vertical">
|
|
||||||
<!-- Compact theme toggle panel (icon-only) aligned right -->
|
|
||||||
<StackPanel HorizontalAlignment="Stretch" Margin="0,0,0,12">
|
|
||||||
<Button Width="24" Height="24" Click="ToggleTheme_Click" ToolTip="Cambia tema" HorizontalAlignment="Right" Padding="2">
|
|
||||||
<iconPacks:PackIconMaterial Kind="ThemeLightDark" Width="12" Height="12" Foreground="{StaticResource AccentBrush}" />
|
|
||||||
</Button>
|
|
||||||
</StackPanel>
|
|
||||||
<Border BorderBrush="#DDD" BorderThickness="1" Padding="8" MaxWidth="280">
|
|
||||||
<!-- Buttons and status live info inside the bordered panel -->
|
|
||||||
<StackPanel>
|
|
||||||
<!-- Buttons stacked vertically as requested -->
|
|
||||||
<StackPanel Orientation="Vertical" HorizontalAlignment="Stretch">
|
|
||||||
<Button Width="120" Margin="0,0,0,8" Command="{Binding LoadSettingsCommand}">
|
|
||||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
|
||||||
<iconPacks:PackIconMaterial Kind="FolderUploadOutline" Width="14" Height="14" Foreground="{StaticResource AccentBrush}" Margin="0,0,6,0" />
|
|
||||||
<TextBlock Text="Carica" />
|
|
||||||
</StackPanel>
|
|
||||||
</Button>
|
|
||||||
<Button Width="120" Margin="0,0,0,8" Command="{Binding SaveSettingsCommand}">
|
|
||||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
|
||||||
<iconPacks:PackIconMaterial Kind="ContentSaveOutline" Width="14" Height="14" Foreground="{StaticResource AccentBrush}" Margin="0,0,6,0" />
|
|
||||||
<TextBlock Text="Salva" />
|
|
||||||
</StackPanel>
|
|
||||||
</Button>
|
|
||||||
<Button Width="120" Height="36" Margin="0,6,0,8" Command="{Binding ProcessImagesCommand}" IsEnabled="{Binding UiEnabled}">
|
|
||||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
|
||||||
<iconPacks:PackIconMaterial Kind="PlayCircleOutline" Width="14" Height="14" Foreground="Green" Margin="0,0,6,0" />
|
|
||||||
<TextBlock Text="Avvia" />
|
|
||||||
</StackPanel>
|
|
||||||
</Button>
|
|
||||||
<Button Width="120" Height="36" Command="{Binding AsyncCancelOperationCommand}">
|
|
||||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
|
||||||
<iconPacks:PackIconMaterial Kind="StopCircleOutline" Width="14" Height="14" Foreground="Red" Margin="0,0,6,0" />
|
|
||||||
<TextBlock Text="Stop" />
|
|
||||||
</StackPanel>
|
|
||||||
</Button>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<Separator Margin="0,12,0,12" />
|
|
||||||
|
|
||||||
<TextBlock Text="Stato" FontWeight="Bold" />
|
|
||||||
<TextBlock Text="{Binding ProcessingStatus}" TextWrapping="Wrap" />
|
|
||||||
|
|
||||||
<TextBlock Text="Progresso" FontWeight="Bold" Margin="0,8,0,0" />
|
|
||||||
<ProgressBar Minimum="0" Maximum="{Binding ProgressBarMaximum}" Value="{Binding ProgressBarValue}" Height="20" />
|
|
||||||
<TextBlock Margin="0,6,0,0">
|
|
||||||
<Run Text="{Binding ProcessedImagesCount}" />
|
|
||||||
<Run Text=" / " />
|
|
||||||
<Run Text="{Binding TotalImagesCount}" />
|
|
||||||
</TextBlock>
|
|
||||||
|
|
||||||
<TextBlock Text="Velocità" FontWeight="Bold" Margin="0,8,0,0" />
|
|
||||||
<TextBlock Text="{Binding SpeedCounter}" TextWrapping="Wrap" />
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
</StackPanel>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<!-- Status bar removed; version now shown in the title commands area -->
|
|
||||||
</Grid>
|
|
||||||
</controls:MetroWindow>
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -282,6 +282,26 @@ namespace ImageCatalog_2.Models
|
||||||
[XmlElement("AI_FaceOutputFolderPath")]
|
[XmlElement("AI_FaceOutputFolderPath")]
|
||||||
public string FaceOutputFolderPath { get; set; } = string.Empty;
|
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
|
// Race upload settings
|
||||||
[JsonPropertyName("ApiLogin")]
|
[JsonPropertyName("ApiLogin")]
|
||||||
[XmlElement("RaceUpload_Login")]
|
[XmlElement("RaceUpload_Login")]
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ using Microsoft.Extensions.Logging.Console;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls.ApplicationLifetimes;
|
|
||||||
|
|
||||||
namespace ImageCatalog_2;
|
namespace ImageCatalog_2;
|
||||||
|
|
||||||
|
|
@ -21,6 +20,9 @@ static class Program
|
||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
private static extern bool AllocConsole();
|
private static extern bool AllocConsole();
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
private static extern bool FreeConsole();
|
||||||
|
|
||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
static extern IntPtr GetStdHandle(int nStdHandle);
|
static extern IntPtr GetStdHandle(int nStdHandle);
|
||||||
|
|
||||||
|
|
@ -36,6 +38,12 @@ static class Program
|
||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
static extern bool AttachConsole(int dwProcessId);
|
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)]
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
static extern IntPtr CreateFile(
|
static extern IntPtr CreateFile(
|
||||||
string lpFileName,
|
string lpFileName,
|
||||||
|
|
@ -48,6 +56,9 @@ static class Program
|
||||||
|
|
||||||
private const uint GENERIC_WRITE = 0x40000000;
|
private const uint GENERIC_WRITE = 0x40000000;
|
||||||
private const uint OPEN_EXISTING = 3;
|
private const uint OPEN_EXISTING = 3;
|
||||||
|
private const uint CTRL_C_EVENT = 0;
|
||||||
|
|
||||||
|
private delegate bool ConsoleCtrlDelegate(uint ctrlType);
|
||||||
|
|
||||||
private static void RedirectConsoleOutput()
|
private static void RedirectConsoleOutput()
|
||||||
{
|
{
|
||||||
|
|
@ -58,6 +69,12 @@ static class Program
|
||||||
Console.SetOut(standardOutput);
|
Console.SetOut(standardOutput);
|
||||||
Console.SetError(standardOutput);
|
Console.SetError(standardOutput);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static bool TrySendConsoleInterrupt(int processId)
|
||||||
|
{
|
||||||
|
_ = processId;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
public static IServiceProvider ServiceProvider { get; private set; } = default!;
|
public static IServiceProvider ServiceProvider { get; private set; } = default!;
|
||||||
|
|
@ -71,10 +88,6 @@ static class Program
|
||||||
static void Main(string[] args)
|
static void Main(string[] args)
|
||||||
{
|
{
|
||||||
#if WINDOWS
|
#if WINDOWS
|
||||||
System.Windows.Forms.Application.SetHighDpiMode(System.Windows.Forms.HighDpiMode.SystemAware);
|
|
||||||
System.Windows.Forms.Application.EnableVisualStyles();
|
|
||||||
System.Windows.Forms.Application.SetCompatibleTextRenderingDefault(false);
|
|
||||||
|
|
||||||
AllocConsole();
|
AllocConsole();
|
||||||
RedirectConsoleOutput();
|
RedirectConsoleOutput();
|
||||||
#endif
|
#endif
|
||||||
|
|
@ -84,59 +97,7 @@ static class Program
|
||||||
|
|
||||||
ServiceProvider = serviceCollection.BuildServiceProvider();
|
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<string>());
|
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args ?? Array.Empty<string>());
|
||||||
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<MainForm>();
|
|
||||||
System.Windows.Forms.Application.Run(mainForm);
|
|
||||||
#else
|
|
||||||
// On non-Windows, Avalonia is the only available UI
|
|
||||||
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args ?? Array.Empty<string>());
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ConfigureServices(ServiceCollection services)
|
private static void ConfigureServices(ServiceCollection services)
|
||||||
|
|
@ -189,11 +150,6 @@ static class Program
|
||||||
|
|
||||||
services.AddTransient<AvaloniaMainWindow>();
|
services.AddTransient<AvaloniaMainWindow>();
|
||||||
|
|
||||||
#if WINDOWS
|
|
||||||
services.AddTransient<MainForm>();
|
|
||||||
services.AddTransient<ImageCatalog_2.MainWindow>();
|
|
||||||
#endif
|
|
||||||
|
|
||||||
services.AddSingleton<MaddoShared.IVersionProvider, MaddoShared.VersionProvider>();
|
services.AddSingleton<MaddoShared.IVersionProvider, MaddoShared.VersionProvider>();
|
||||||
|
|
||||||
services.AddLogging(configure =>
|
services.AddLogging(configure =>
|
||||||
|
|
|
||||||
3
imagecatalog/Properties/InternalsVisibleTo.cs
Normal file
3
imagecatalog/Properties/InternalsVisibleTo.cs
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
[assembly: InternalsVisibleTo("MaddoShared.Tests")]
|
||||||
|
|
@ -6,18 +6,12 @@ using System.Runtime.CompilerServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
#if WINDOWS
|
|
||||||
using System.Windows.Forms;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
namespace ImageCatalog_2
|
namespace ImageCatalog_2
|
||||||
{
|
{
|
||||||
public class ViewModelBase : INotifyPropertyChanged
|
public class ViewModelBase : INotifyPropertyChanged
|
||||||
{
|
{
|
||||||
private readonly SynchronizationContext? _synchronizationContext;
|
private readonly SynchronizationContext? _synchronizationContext;
|
||||||
#if WINDOWS
|
|
||||||
private Control? _control;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
protected ViewModelBase()
|
protected ViewModelBase()
|
||||||
{
|
{
|
||||||
|
|
@ -25,17 +19,6 @@ namespace ImageCatalog_2
|
||||||
_synchronizationContext = SynchronizationContext.Current;
|
_synchronizationContext = SynchronizationContext.Current;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Set a Control to use for thread marshalling in WinForms applications.
|
|
||||||
/// This is required for proper cross-thread handling with data binding.
|
|
||||||
/// </summary>
|
|
||||||
#if WINDOWS
|
|
||||||
public void SetControl(Control control)
|
|
||||||
{
|
|
||||||
_control = control;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
public event PropertyChangedEventHandler? PropertyChanged;
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
|
||||||
// This method is called by the Set accessor of each property.
|
// This method is called by the Set accessor of each property.
|
||||||
|
|
@ -46,22 +29,6 @@ namespace ImageCatalog_2
|
||||||
if (PropertyChanged == null)
|
if (PropertyChanged == null)
|
||||||
return;
|
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)
|
if (_synchronizationContext != null && SynchronizationContext.Current != _synchronizationContext)
|
||||||
{
|
{
|
||||||
// We're on a different thread, marshal to the UI thread
|
// We're on a different thread, marshal to the UI thread
|
||||||
|
|
|
||||||
|
|
@ -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;
|
private double _aiProgress;
|
||||||
public double AiProgress
|
public double AiProgress
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue