2026-02-15 11:14:19 +01:00
|
|
|
using System.Windows;
|
2026-02-21 16:49:13 +01:00
|
|
|
using MahApps.Metro.Controls;
|
|
|
|
|
using ControlzEx.Theming;
|
2026-02-15 11:14:19 +01:00
|
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
|
|
|
using System;
|
|
|
|
|
using System.IO;
|
|
|
|
|
using System.Windows.Media.Imaging;
|
2026-02-17 20:51:35 +01:00
|
|
|
using System.Diagnostics;
|
2026-02-15 11:14:19 +01:00
|
|
|
using Microsoft.Win32;
|
|
|
|
|
using System.Windows.Forms;
|
|
|
|
|
|
|
|
|
|
namespace ImageCatalog_2
|
|
|
|
|
{
|
2026-02-21 16:49:13 +01:00
|
|
|
public partial class MainWindow : MetroWindow
|
2026-02-15 11:14:19 +01:00
|
|
|
{
|
|
|
|
|
private readonly DataModel _model;
|
2026-02-21 16:49:13 +01:00
|
|
|
private bool _isDarkTheme = false;
|
2026-02-15 11:14:19 +01:00
|
|
|
public MainWindow(DataModel model)
|
|
|
|
|
{
|
|
|
|
|
InitializeComponent();
|
|
|
|
|
_model = model;
|
|
|
|
|
DataContext = _model;
|
2026-02-17 20:51:35 +01:00
|
|
|
// 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 { }
|
2026-02-21 16:49:13 +01:00
|
|
|
// Ensure MahApps resource dictionaries are loaded so chrome/styles are available
|
|
|
|
|
EnsureMahAppsResourcesLoaded();
|
|
|
|
|
|
2026-02-16 18:32:04 +01:00
|
|
|
// Apply theme based on user preference or system setting (default to light)
|
|
|
|
|
ApplyTheme(isDark: false);
|
2026-02-15 11:14:19 +01:00
|
|
|
// 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;
|
2026-02-16 18:32:04 +01:00
|
|
|
_model.SelectModelsFolderRequested += Model_SelectModelsFolderRequested;
|
|
|
|
|
_model.SelectCsvOutputRequested += Model_SelectCsvOutputRequested;
|
2026-02-15 11:14:19 +01:00
|
|
|
|
|
|
|
|
// Watch for logo changes to update preview
|
|
|
|
|
_model.PropertyChanged += Model_PropertyChanged;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-16 18:32:04 +01:00
|
|
|
private void ApplyTheme(bool isDark)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var rd = isDark ? (ResourceDictionary)Resources["DarkTheme"] : (ResourceDictionary)Resources["LightTheme"];
|
|
|
|
|
foreach (var key in rd.Keys)
|
|
|
|
|
{
|
2026-02-21 16:49:13 +01:00
|
|
|
// 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];
|
2026-02-16 18:32:04 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
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 { }
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-15 11:14:19 +01:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-21 16:49:13 +01:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-15 11:14:19 +01:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|