diff --git a/imagecatalog/AvaloniaApp.axaml b/imagecatalog/AvaloniaApp.axaml
new file mode 100644
index 0000000..8ce9be1
--- /dev/null
+++ b/imagecatalog/AvaloniaApp.axaml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/imagecatalog/AvaloniaApp.axaml.cs b/imagecatalog/AvaloniaApp.axaml.cs
new file mode 100644
index 0000000..9a14f4d
--- /dev/null
+++ b/imagecatalog/AvaloniaApp.axaml.cs
@@ -0,0 +1,20 @@
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Markup.Xaml;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace ImageCatalog_2;
+
+public partial class AvaloniaApp : Avalonia.Application
+{
+ public override void Initialize() => AvaloniaXamlLoader.Load(this);
+
+ public override void OnFrameworkInitializationCompleted()
+ {
+ if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+ {
+ var model = Program.ServiceProvider.GetRequiredService();
+ desktop.MainWindow = new AvaloniaMainWindow(model);
+ }
+ base.OnFrameworkInitializationCompleted();
+ }
+}
diff --git a/imagecatalog/AvaloniaMainWindow.axaml b/imagecatalog/AvaloniaMainWindow.axaml
new file mode 100644
index 0000000..a16b949
--- /dev/null
+++ b/imagecatalog/AvaloniaMainWindow.axaml
@@ -0,0 +1,317 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Nessuna
+ Aggiungi scritta
+ Nome file
+ Aggiungi orario
+ Nome+Orario
+ Tempo gara
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/imagecatalog/AvaloniaMainWindow.axaml.cs b/imagecatalog/AvaloniaMainWindow.axaml.cs
new file mode 100644
index 0000000..2095407
--- /dev/null
+++ b/imagecatalog/AvaloniaMainWindow.axaml.cs
@@ -0,0 +1,151 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Avalonia.Media.Imaging;
+using Avalonia.Platform.Storage;
+using Avalonia.Styling;
+using Avalonia.Threading;
+using System;
+using System.IO;
+
+namespace ImageCatalog_2;
+
+public partial class AvaloniaMainWindow : Window
+{
+ private readonly DataModel _model;
+ private bool _isDarkTheme = false;
+
+ public AvaloniaMainWindow(DataModel model)
+ {
+ InitializeComponent();
+ _model = model;
+ DataContext = _model;
+
+ // Provide Avalonia dispatcher so DataModel can marshal UI updates
+ _model.UiInvoker = action => Dispatcher.UIThread.Invoke(action);
+
+ // Wire dialog events
+ _model.SelectSourceFolderRequested += async (_, _) =>
+ {
+ var folders = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions { Title = "Seleziona cartella sorgente" });
+ if (folders.Count > 0) _model.SourcePath = folders[0].Path.LocalPath + Path.DirectorySeparatorChar;
+ };
+
+ _model.SelectDestinationFolderRequested += async (_, _) =>
+ {
+ var folders = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions { Title = "Seleziona cartella destinazione" });
+ if (folders.Count > 0) _model.DestinationPath = folders[0].Path.LocalPath + Path.DirectorySeparatorChar;
+ };
+
+ _model.SelectLogoFileRequested += async (_, _) =>
+ {
+ var files = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
+ {
+ Title = "Seleziona logo",
+ FileTypeFilter = new[] { new FilePickerFileType("Immagini") { Patterns = new[] { "*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif" } } }
+ });
+ if (files.Count > 0)
+ {
+ _model.LogoFile = files[0].Path.LocalPath;
+ UpdateLogoPreview(_model.LogoFile);
+ }
+ };
+
+ _model.SelectModelsFolderRequested += async (_, _) =>
+ {
+ var folders = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions { Title = "Seleziona cartella modelli" });
+ if (folders.Count > 0) _model.ModelsFolderPath = folders[0].Path.LocalPath + Path.DirectorySeparatorChar;
+ };
+
+ _model.SelectCsvOutputRequested += async (_, _) =>
+ {
+ var file = await StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
+ {
+ Title = "Salva CSV",
+ DefaultExtension = "csv",
+ FileTypeChoices = new[] { new FilePickerFileType("CSV") { Patterns = new[] { "*.csv" } } }
+ });
+ if (file != null) _model.CsvOutputPath = file.Path.LocalPath;
+ };
+
+ _model.SaveSettingsRequested += async (_, _) =>
+ {
+ var file = await StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
+ {
+ Title = "Salva impostazioni",
+ DefaultExtension = "xml",
+ FileTypeChoices = new[] { new FilePickerFileType("Setup") { Patterns = new[] { "*.xml" } } }
+ });
+ if (file != null) await _model.SaveSettingsToFileAsync(file.Path.LocalPath);
+ };
+
+ _model.LoadSettingsRequested += async (_, _) =>
+ {
+ var files = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
+ {
+ Title = "Carica impostazioni",
+ FileTypeFilter = new[] { new FilePickerFileType("Setup") { Patterns = new[] { "*.xml" } } }
+ });
+ if (files.Count > 0) await _model.LoadSettingsFromFileAsync(files[0].Path.LocalPath);
+ };
+
+ _model.SelectColorRequested += (_, _) =>
+ {
+ // Color is set by typing hex directly in the TextBox
+ };
+
+ _model.SelectTransparentColorRequested += (_, _) =>
+ {
+ // Color is set by typing hex directly in the TextBox
+ };
+
+ _model.PropertyChanged += (_, e) =>
+ {
+ if (e.PropertyName == nameof(_model.LogoFile))
+ UpdateLogoPreview(_model.LogoFile);
+ };
+ }
+
+ private void ToggleTheme_Click(object? sender, RoutedEventArgs e)
+ {
+ _isDarkTheme = !_isDarkTheme;
+ if (Avalonia.Application.Current != null)
+ Avalonia.Application.Current.RequestedThemeVariant = _isDarkTheme ? ThemeVariant.Dark : ThemeVariant.Light;
+ }
+
+ private void OpenSourceFolder_Click(object? sender, RoutedEventArgs e) => OpenInExplorer(_model.SourcePath);
+ private void OpenDestinationFolder_Click(object? sender, RoutedEventArgs e) => OpenInExplorer(_model.DestinationPath);
+ private void OpenModelsFolder_Click(object? sender, RoutedEventArgs e) => OpenInExplorer(_model.ModelsFolderPath);
+ private void OpenCsvOutputFolder_Click(object? sender, RoutedEventArgs e)
+ {
+ var dir = Path.GetDirectoryName(_model.CsvOutputPath);
+ OpenInExplorer(string.IsNullOrWhiteSpace(dir) ? _model.CsvOutputPath : dir);
+ }
+
+ private static void OpenInExplorer(string? path)
+ {
+ if (string.IsNullOrWhiteSpace(path)) return;
+ path = path.Trim().Trim('"');
+ try
+ {
+ if (File.Exists(path))
+ System.Diagnostics.Process.Start("explorer.exe", $"/select,\"{path}\"");
+ else if (Directory.Exists(path))
+ System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = path, UseShellExecute = true });
+ }
+ catch { }
+ }
+
+ private void UpdateLogoPreview(string? path)
+ {
+ var preview = this.FindControl("LogoPreview");
+ if (preview == null) return;
+ if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
+ {
+ preview.Source = null;
+ return;
+ }
+ try { preview.Source = new Avalonia.Media.Imaging.Bitmap(path); }
+ catch { preview.Source = null; }
+ }
+}
diff --git a/imagecatalog/DataModel.cs b/imagecatalog/DataModel.cs
index fbb0223..ca7c44a 100644
--- a/imagecatalog/DataModel.cs
+++ b/imagecatalog/DataModel.cs
@@ -207,12 +207,20 @@ namespace ImageCatalog_2
return digits;
}
+ ///
+ /// Optional UI-thread invoker set by the active UI layer (WPF, Avalonia, etc.).
+ /// When set, uses this delegate instead of the WPF dispatcher.
+ ///
+ public Action? UiInvoker { get; set; }
+
private Task InvokeOnUiThreadAsync(Action action)
{
- // Use SynchronizationContext via Task to ensure UI thread update
return Task.Run(() =>
{
- System.Windows.Application.Current?.Dispatcher.Invoke(action);
+ if (UiInvoker != null)
+ UiInvoker(action);
+ else
+ System.Windows.Application.Current?.Dispatcher.Invoke(action);
});
}
diff --git a/imagecatalog/ImageCatalog 2.csproj b/imagecatalog/ImageCatalog 2.csproj
index cbd865d..d5724d5 100644
--- a/imagecatalog/ImageCatalog 2.csproj
+++ b/imagecatalog/ImageCatalog 2.csproj
@@ -48,6 +48,10 @@
+
+
+
+
all
diff --git a/imagecatalog/Program.cs b/imagecatalog/Program.cs
index ba559a4..29390ab 100644
--- a/imagecatalog/Program.cs
+++ b/imagecatalog/Program.cs
@@ -9,6 +9,8 @@ using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Logging.Console;
using System.IO;
using Microsoft.Extensions.Options;
+using Avalonia;
+using Avalonia.Controls.ApplicationLifetimes;
namespace ImageCatalog_2;
@@ -56,12 +58,18 @@ static class Program
}
public static IServiceProvider ServiceProvider { get; private set; }
+
+ public static Avalonia.AppBuilder BuildAvaloniaApp()
+ => Avalonia.AppBuilder.Configure()
+ .UsePlatformDetect()
+ .LogToTrace();
+
[STAThread]
static void Main(string[] args)
{
- Application.SetHighDpiMode(HighDpiMode.SystemAware);
- Application.EnableVisualStyles();
- Application.SetCompatibleTextRenderingDefault(false);
+ System.Windows.Forms.Application.SetHighDpiMode(HighDpiMode.SystemAware);
+ System.Windows.Forms.Application.EnableVisualStyles();
+ System.Windows.Forms.Application.SetCompatibleTextRenderingDefault(false);
AllocConsole();
RedirectConsoleOutput();
@@ -74,8 +82,15 @@ static class Program
// Resolve WPF MainWindow when available, otherwise fall back to WinForms MainForm
var serviceProvider = ServiceProvider;
- // Determine UI based on command line. Default: WinForms. Use --wpf to explicitly request WPF.
+ // Determine UI based on command line. Default: WinForms. Use --wpf for WPF, --avalonia for Avalonia.
bool useWpf = args is not null && Array.Exists(args, a => string.Equals(a, "--wpf", StringComparison.OrdinalIgnoreCase));
+ bool useAvalonia = args is not null && Array.Exists(args, a => string.Equals(a, "--avalonia", StringComparison.OrdinalIgnoreCase));
+
+ if (useAvalonia)
+ {
+ BuildAvaloniaApp().StartWithClassicDesktopLifetime(args ?? Array.Empty());
+ return;
+ }
if (useWpf)
{
diff --git a/imagecatalog/Properties/launchSettings.json b/imagecatalog/Properties/launchSettings.json
index 12da6a8..40df2da 100644
--- a/imagecatalog/Properties/launchSettings.json
+++ b/imagecatalog/Properties/launchSettings.json
@@ -1,12 +1,15 @@
{
"profiles": {
"ImageCatalog (WinForms)": {
- "commandName": "Project",
- "commandLineArgs": ""
+ "commandName": "Project"
},
"ImageCatalog (WPF)": {
"commandName": "Project",
"commandLineArgs": "--wpf"
+ },
+ "ImageCatalog (Avalonia)": {
+ "commandName": "Project",
+ "commandLineArgs": "--avalonia"
}
}
-}
+}
\ No newline at end of file