Refactor application to remove Windows Forms dependencies and transition to Avalonia UI

- Deleted MainWindow.xaml.cs, which contained the WPF implementation of the main window.
- Updated Program.cs to remove Windows Forms initialization and support only Avalonia UI.
- Removed Windows Forms specific code from ViewModelBase, including control marshalling logic.
This commit is contained in:
MaddoScientisto 2026-05-09 14:04:21 +02:00
commit d6b778a648
16 changed files with 64 additions and 4415 deletions

View file

@ -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
View 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
View 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"
}
]
}

View file

@ -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
View 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>

View file

@ -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>

View file

@ -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);
}
}
}
}

View file

@ -15,9 +15,6 @@ 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;
@ -167,8 +164,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; }
@ -179,7 +175,7 @@ namespace ImageCatalog_2
if (UiInvoker != null) if (UiInvoker != null)
UiInvoker(action); UiInvoker(action);
else else
System.Windows.Application.Current?.Dispatcher.Invoke(action); action();
}); });
} }
@ -1886,11 +1882,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)

View file

@ -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. -->

File diff suppressed because it is too large Load diff

View file

@ -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

View file

@ -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>

View file

@ -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>

View file

@ -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

View file

@ -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;
@ -115,10 +114,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
@ -128,59 +123,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)
@ -233,11 +176,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 =>

View file

@ -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