diff --git a/installer/DesktopMagic-Installer.exe b/installer/DesktopMagic-Installer.exe
index 216629b..ef85580 100644
Binary files a/installer/DesktopMagic-Installer.exe and b/installer/DesktopMagic-Installer.exe differ
diff --git a/installer/Installer.iss b/installer/Installer.iss
index 71a2c34..80a7417 100644
--- a/installer/Installer.iss
+++ b/installer/Installer.iss
@@ -1,7 +1,7 @@
; Script generated by the Inno Setup Script Wizard.
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
-#expr Exec('cmd.exe /C ', 'dotnet build -o "' + SourcePath + '\publish" -c Release ' + SourcePath + '..\src\DesktopMagic\')
+#expr Exec('cmd.exe', '/C dotnet build -o "' + SourcePath + '\publish" -c Release ' + SourcePath + '..\src\DesktopMagic\')
#define MyAppName "DesktopMagic"
#define MyAppVersion GetStringFileInfo("/publish/DesktopMagic.exe","ProductVersion")
diff --git a/src/DesktopMagic/App.xaml b/src/DesktopMagic/App.xaml
index e943dca..83dca8a 100644
--- a/src/DesktopMagic/App.xaml
+++ b/src/DesktopMagic/App.xaml
@@ -1,20 +1,25 @@
-
-
+
+
-
-
+
-
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/DesktopMagic/App.xaml.cs b/src/DesktopMagic/App.xaml.cs
index 55be20b..da9e781 100644
--- a/src/DesktopMagic/App.xaml.cs
+++ b/src/DesktopMagic/App.xaml.cs
@@ -5,6 +5,8 @@ using System.IO;
using System.Threading;
using System.Windows;
+using Wpf.Ui;
+
namespace DesktopMagic;
///
@@ -26,6 +28,8 @@ public partial class App : Application
public static string PluginsPath => Path.Combine(ApplicationDataPath, "Plugins");
+ public static IContentDialogService DialogService { get; } = new ContentDialogService();
+
public static Logger Logger { get; } = new Logger()
{
Config = new()
@@ -130,7 +134,7 @@ public partial class App : Application
Logger.LogFatal(exception + (e.IsTerminating ? "\t Process terminating!" : ""), source: exception.Source ?? "Unknown");
}
- private static void Setup(bool clearLogFile)
+ private static async void Setup(bool clearLogFile)
{
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
@@ -156,7 +160,13 @@ public partial class App : Application
}
catch (Exception ex)
{
- _ = MessageBox.Show(ex.ToString(), AppName, MessageBoxButton.OK, MessageBoxImage.Error);
+ Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
+ {
+ Title = AppName,
+ Content = ex.ToString(),
+ CloseButtonText = "Ok"
+ };
+ _ = await messageBox.ShowDialogAsync();
Logger.Log(ex.Message, "Setup", LogSeverity.Error);
}
diff --git a/src/DesktopMagic/BuiltInPlugins/CpuMonitorPlugin.cs b/src/DesktopMagic/BuiltInPlugins/CpuMonitorPlugin.cs
index da4d02f..75d1763 100644
--- a/src/DesktopMagic/BuiltInPlugins/CpuMonitorPlugin.cs
+++ b/src/DesktopMagic/BuiltInPlugins/CpuMonitorPlugin.cs
@@ -56,7 +56,7 @@ internal class CpuMonitorPlugin : Plugin
}
else
{
- gr.DrawString(cpuUsageString, font, new SolidBrush(Application.Theme.PrimaryColor), 0, (size.Width / 2) - size.Height);
+ gr.DrawString(cpuUsageString, font, new SolidBrush(Application.Theme.PrimaryColor), 0, 0);
}
return bmp;
diff --git a/src/DesktopMagic/BuiltInPlugins/DatePlugin.cs b/src/DesktopMagic/BuiltInPlugins/DatePlugin.cs
index 9a6ad36..cefe11e 100644
--- a/src/DesktopMagic/BuiltInPlugins/DatePlugin.cs
+++ b/src/DesktopMagic/BuiltInPlugins/DatePlugin.cs
@@ -13,26 +13,23 @@ internal class DatePlugin : Plugin
private readonly CheckBox shortDateCheckBox = new CheckBox(true);
private DateTime oldDateTime = DateTime.MinValue;
- private Color oldColor = Color.White;
- private string oldFont = string.Empty;
- private bool oldShortDateCheckBoxValue;
+ private bool themeChanged = false;
+
public override int UpdateInterval => 1000;
public override Bitmap? Main()
{
- if (oldDateTime.Date == DateTime.Now.Date && oldColor == Application.Theme.PrimaryColor && oldFont == Application.Theme.Font && oldShortDateCheckBoxValue == shortDateCheckBox.Value)
+ if (oldDateTime.Date == DateTime.Now.Date && !themeChanged)
{
return null;
}
oldDateTime = DateTime.Now;
- oldColor = Application.Theme.PrimaryColor;
- oldFont = Application.Theme.Font;
- oldShortDateCheckBoxValue = shortDateCheckBox.Value;
+ themeChanged = false;
string date = shortDateCheckBox.Value ? DateTime.Now.ToShortDateString() : DateTime.Now.ToLongDateString();
- Font font = new Font(Application.Theme.Font, 200);
+ using Font font = new Font(Application.Theme.Font, 200);
Bitmap bmp = new Bitmap(1, 1);
bmp.SetResolution(100, 100);
@@ -45,10 +42,16 @@ internal class DatePlugin : Plugin
bmp.SetResolution(100, 100);
using Graphics gr = Graphics.FromImage(bmp);
+ using SolidBrush brush = new SolidBrush(Application.Theme.PrimaryColor);
gr.TextRenderingHint = TextRenderingHint.AntiAlias;
- gr.DrawString(date, font, new SolidBrush(Application.Theme.PrimaryColor), 0, 0);
+ gr.DrawString(date, font, brush, 0, 0);
return bmp;
}
+
+ public override void OnThemeChanged()
+ {
+ themeChanged = true;
+ }
}
\ No newline at end of file
diff --git a/src/DesktopMagic/BuiltInPlugins/TimePlugin.cs b/src/DesktopMagic/BuiltInPlugins/TimePlugin.cs
index f56d699..c8030be 100644
--- a/src/DesktopMagic/BuiltInPlugins/TimePlugin.cs
+++ b/src/DesktopMagic/BuiltInPlugins/TimePlugin.cs
@@ -13,26 +13,23 @@ internal class TimePlugin : Plugin
private readonly CheckBox displaySecondsCheckBox = new CheckBox(true);
private string oldTime = string.Empty;
- private Color oldColor = Color.White;
- private string oldFont = string.Empty;
- private bool oldDisplaySecondsCheckBoxValue;
+ private bool themeChanged;
+
public override int UpdateInterval => 1000;
public override Bitmap? Main()
{
string time = displaySecondsCheckBox.Value ? DateTime.Now.ToLongTimeString() : DateTime.Now.ToShortTimeString();
- if (oldTime == time && oldColor == Application.Theme.PrimaryColor && oldFont == Application.Theme.Font && oldDisplaySecondsCheckBoxValue == displaySecondsCheckBox.Value)
+ if (oldTime == time && !themeChanged)
{
return null;
}
oldTime = time;
- oldColor = Application.Theme.PrimaryColor;
- oldFont = Application.Theme.Font;
- oldDisplaySecondsCheckBoxValue = displaySecondsCheckBox.Value;
+ themeChanged = false;
- Font font = new Font(Application.Theme.Font, 200);
+ using Font font = new Font(Application.Theme.Font, 200);
Bitmap bmp = new Bitmap(1, 1);
bmp.SetResolution(100, 100);
@@ -45,10 +42,21 @@ internal class TimePlugin : Plugin
bmp.SetResolution(100, 100);
using Graphics gr = Graphics.FromImage(bmp);
+ using SolidBrush brush = new SolidBrush(Application.Theme.PrimaryColor);
gr.TextRenderingHint = TextRenderingHint.AntiAlias;
- gr.DrawString(time, font, new SolidBrush(Application.Theme.PrimaryColor), 0, 0);
+ gr.DrawString(time, font, brush, 0, 0);
return bmp;
}
+
+ public override void OnThemeChanged()
+ {
+ themeChanged = true;
+ }
+
+ public override void OnSettingsChanged()
+ {
+ themeChanged = true;
+ }
}
\ No newline at end of file
diff --git a/src/DesktopMagic/BuiltInPlugins/WeatherPlugin.cs b/src/DesktopMagic/BuiltInPlugins/WeatherPlugin.cs
new file mode 100644
index 0000000..722cd2c
--- /dev/null
+++ b/src/DesktopMagic/BuiltInPlugins/WeatherPlugin.cs
@@ -0,0 +1,208 @@
+using DesktopMagic.Api;
+using DesktopMagic.Api.Settings;
+
+using System;
+using System.Diagnostics;
+using System.Drawing;
+using System.Globalization;
+using System.Net.Http;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace DesktopMagic.BuiltInPlugins;
+
+public class WeatherPlugin : AsyncPlugin
+{
+ [Setting("city-name", "City Name")]
+ private readonly TextBox cityInput = new TextBox("Tokyo");
+
+ [Setting("search-btn", "Update Location")]
+ private readonly Button searchButton = new Button("Update Location");
+
+ [Setting("show-city", "Show City Name")]
+ private readonly CheckBox showCity = new CheckBox(true);
+
+ [Setting("show-temp", "Show Temperature")]
+ private readonly CheckBox showTemp = new CheckBox(true);
+
+ [Setting("show-status", "Show Weather Status")]
+ private readonly CheckBox showStatus = new CheckBox(true);
+
+ [Setting("show-time", "Show Last Updated")]
+ private readonly CheckBox showTime = new CheckBox(false);
+
+ [Setting("font-size", "Base Font Size")]
+ private readonly Slider fontSizeSlider = new Slider(10, 100, 30);
+
+ public override int UpdateInterval { get; set; } = 900000; // 15 Minutes
+
+ private readonly HttpClient httpClient = new HttpClient();
+
+ private string? cachedLat;
+ private string? cachedLon;
+ private string currentTemp = "--";
+ private string weatherStatus = "Enter city and search";
+ private string foundCityName = "No City Selected";
+ private string lastUpdated = "";
+ private bool isLoading = false;
+
+ public override async Task StartAsync(CancellationToken cancellationToken)
+ {
+ httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (DesktopMagic)");
+
+ // Setup Event Handlers for instant UI updates when settings change
+ showCity.OnValueChanged += Application.UpdateWindow;
+ showTemp.OnValueChanged += Application.UpdateWindow;
+ showStatus.OnValueChanged += Application.UpdateWindow;
+ showTime.OnValueChanged += Application.UpdateWindow;
+ fontSizeSlider.OnValueChanged += Application.UpdateWindow;
+
+ await UpdateLocationAndWeather();
+ Application.UpdateWindow();
+
+ searchButton.OnClick += () =>
+ {
+ isLoading = true;
+ Application.UpdateWindow();
+
+ _ = Task.Run(async () =>
+ {
+ await UpdateLocationAndWeather();
+ isLoading = false;
+ Application.UpdateWindow();
+ });
+ };
+ }
+
+ public override async Task MainAsync(CancellationToken cancellationToken)
+ {
+ // Periodic background update
+ if (!isLoading && cachedLat != null && cachedLon != null)
+ {
+ await UpdateWeatherOnly(cachedLat, cachedLon);
+ }
+
+ Bitmap bmp = new Bitmap(1200, 800);
+ bmp.SetResolution(300, 300);
+
+ using (Graphics g = Graphics.FromImage(bmp))
+ {
+ g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
+ g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAliasGridFit;
+ g.Clear(Color.Transparent);
+
+ int lineCount = 0;
+ int baseSize = (int)fontSizeSlider.Value;
+
+ if (isLoading)
+ {
+ DrawLine(bmp, g, "Fetching...", ref lineCount, baseSize, FontStyle.Italic);
+ }
+ else
+ {
+ if (showCity.Value)
+ {
+ DrawLine(bmp, g, foundCityName.ToUpper(), ref lineCount, baseSize, FontStyle.Bold);
+ }
+
+ if (showTemp.Value)
+ {
+ DrawLine(bmp, g, currentTemp, ref lineCount, baseSize, FontStyle.Bold);
+ }
+
+ if (showStatus.Value)
+ {
+ DrawLine(bmp, g, weatherStatus, ref lineCount, baseSize, FontStyle.Regular);
+ }
+
+ if (showTime.Value && !string.IsNullOrEmpty(lastUpdated))
+ {
+ DrawLine(bmp, g, $"Updated: {lastUpdated}", ref lineCount, baseSize, FontStyle.Italic);
+ }
+ }
+ }
+
+ return bmp;
+ }
+
+ private void DrawLine(Bitmap bitmap, Graphics graphics, string text, ref int lineCount, int fontSize, FontStyle style)
+ {
+ using Font font = new Font(Application.Theme.Font, fontSize, style);
+ SizeF textSize = graphics.MeasureString(text, font);
+
+ float x = (bitmap.Width - textSize.Width) / 2;
+ // Adjust vertical spacing based on font size
+ float y = (lineCount == 0) ? 60 : (lineCount * (textSize.Height + 10)) + 60;
+
+ using Brush brush = new SolidBrush(Application.Theme.PrimaryColor);
+ graphics.DrawString(text, font, brush, x, y);
+
+ lineCount++;
+ }
+
+ private async Task UpdateLocationAndWeather()
+ {
+ try
+ {
+ string geoUrl = $"https://geocoding-api.open-meteo.com/v1/search?name={Uri.EscapeDataString(cityInput.Value.Trim())}&count=1&language=en&format=json";
+ string geoJson = await httpClient.GetStringAsync(geoUrl);
+
+ using JsonDocument geoDoc = JsonDocument.Parse(geoJson);
+ if (!geoDoc.RootElement.TryGetProperty("results", out JsonElement results) || results.GetArrayLength() == 0)
+ {
+ foundCityName = cityInput.Value;
+ currentTemp = "--";
+ weatherStatus = "City not found";
+ return;
+ }
+
+ JsonElement location = results[0];
+ cachedLat = location.GetProperty("latitude").GetDouble().ToString(CultureInfo.InvariantCulture);
+ cachedLon = location.GetProperty("longitude").GetDouble().ToString(CultureInfo.InvariantCulture);
+ foundCityName = location.GetProperty("name").GetString() ?? cityInput.Value;
+
+ // We update the textbox to the "official" name found by the API
+ cityInput.Value = foundCityName;
+
+ await UpdateWeatherOnly(cachedLat, cachedLon);
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"Error: {ex.Message}");
+ weatherStatus = "Connection Error";
+ }
+ }
+
+ private async Task UpdateWeatherOnly(string lat, string lon)
+ {
+ try
+ {
+ string weatherUrl = $"https://api.open-meteo.com/v1/forecast?latitude={lat}&longitude={lon}¤t_weather=true";
+ string weatherJson = await httpClient.GetStringAsync(weatherUrl);
+
+ using JsonDocument weatherDoc = JsonDocument.Parse(weatherJson);
+ JsonElement current = weatherDoc.RootElement.GetProperty("current_weather");
+
+ currentTemp = $"{current.GetProperty("temperature").GetDouble()}°C";
+ weatherStatus = MapWeatherCode(current.GetProperty("weathercode").GetInt32());
+ lastUpdated = DateTime.Now.ToString("HH:mm");
+ }
+ catch { /* Ignore background update errors */ }
+ }
+
+ private string MapWeatherCode(int code)
+ {
+ return code switch
+ {
+ 0 => "Clear Sky",
+ 1 or 2 or 3 => "Partly Cloudy",
+ 45 or 48 => "Foggy",
+ 51 or 53 or 55 => "Drizzle",
+ 61 or 63 or 65 => "Rainy",
+ 71 or 73 or 75 => "Snowy",
+ 95 or 96 or 99 => "Thunderstorm",
+ _ => "Cloudy"
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/DesktopMagic/DataContexts/MainWindowDataContext.cs b/src/DesktopMagic/DataContexts/MainWindowDataContext.cs
index 183d23b..8c3c0fd 100644
--- a/src/DesktopMagic/DataContexts/MainWindowDataContext.cs
+++ b/src/DesktopMagic/DataContexts/MainWindowDataContext.cs
@@ -11,8 +11,18 @@ internal class MainWindowDataContext : INotifyPropertyChanged
public event PropertyChangedEventHandler? PropertyChanged;
private static DesktopMagicSettings settings = new();
+
private bool isLoading = true;
+ public string Title =>
+#if DEBUG
+ $"{App.AppName} - Dev {System.Windows.Forms.Application.ProductVersion}";
+#else
+ $"{App.AppName} - {System.Windows.Forms.Application.ProductVersion}";
+#endif
+
+ public string AppName => App.AppName;
+
public DesktopMagicSettings Settings
{
get => settings;
diff --git a/src/DesktopMagic/DataContexts/PluginEntryDataContext.cs b/src/DesktopMagic/DataContexts/PluginEntryDataContext.cs
index c13876f..ae235fa 100644
--- a/src/DesktopMagic/DataContexts/PluginEntryDataContext.cs
+++ b/src/DesktopMagic/DataContexts/PluginEntryDataContext.cs
@@ -1,18 +1,17 @@
using DesktopMagic.Helpers;
using DesktopMagic.Plugins;
-using MaterialDesignThemes.Wpf;
-
using System;
using System.ComponentModel;
using System.Diagnostics;
+using System.IO;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Input;
namespace DesktopMagic.DataContexts;
-internal class PluginEntryDataContext(PluginMetadata pluginMetadata, ICommand command, PluginEntryDataContext.Mode mode, string? path = null) : INotifyPropertyChanged
+internal class PluginEntryDataContext(PluginMetadata pluginMetadata, ICommand command, PluginEntryDataContext.Mode mode, string? path = null, string? csprojPath = null) : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
@@ -20,7 +19,7 @@ internal class PluginEntryDataContext(PluginMetadata pluginMetadata, ICommand co
public string Name => pluginMetadata.Name;
- public string? Description => pluginMetadata.Description;
+ public string? Description => pluginMetadata.Description?.Trim();
public string Author => pluginMetadata.Author ?? (string)App.LanguageDictionary["unknown"];
@@ -35,22 +34,27 @@ internal class PluginEntryDataContext(PluginMetadata pluginMetadata, ICommand co
public string? Path => path;
- public bool IsLocalPlugin => string.IsNullOrWhiteSpace(pluginMetadata.ProfileUri?.ToString());
+ public bool IsLocalPlugin => pluginMetadata.IsLocalPlugin;
+
public ICommand Command => command;
- public ButtonData InstallUninstallButtonData => new(mode == Mode.Install ? PackIconKind.Download : PackIconKind.Remove, GetInstallUninstallButtonText(), true, Command);
+ public ButtonData InstallUninstallButtonData => new(mode == Mode.Install ? "Download24" : "Delete24", GetInstallUninstallButtonText(), true, Command);
public ButtonData OpenButtonData
{
get
{
- if (string.IsNullOrWhiteSpace(pluginMetadata.ProfileUri?.ToString()))
+ if (File.Exists(csprojPath))
{
- return new(PackIconKind.FolderOutline, (string)App.LanguageDictionary["folder"], path is not null, new CommandHandler(() => Process.Start("explorer.exe", path!)));
+ return new("Code24", "IDE", true, new CommandHandler(OpenCsprojInIDE));
+ }
+ else if (string.IsNullOrWhiteSpace(pluginMetadata.ProfileUri?.ToString()))
+ {
+ return new("Folder24", (string)App.LanguageDictionary["folder"], path is not null, new CommandHandler(() => Process.Start("explorer.exe", path!)));
}
else
{
- return new(PackIconKind.ExternalLink, "mod.io", true, new CommandHandler(OpenModIoPage));
+ return new("Open24", "mod.io", true, new CommandHandler(OpenModIoPage));
}
}
}
@@ -79,6 +83,34 @@ internal class PluginEntryDataContext(PluginMetadata pluginMetadata, ICommand co
_ = Process.Start(psi);
}
+ private void OpenCsprojInIDE()
+ {
+ string? associatedProgram = FileUtilities.GetAssociatedProgram(".csproj");
+ string pluginProjectPath = System.IO.Path.GetDirectoryName(csprojPath)!;
+
+ if (string.IsNullOrWhiteSpace(pluginProjectPath))
+ {
+ App.Logger.LogError("Could not determine plugin project directory.", source: "PluginManager");
+ return;
+ }
+
+ if (associatedProgram is null)
+ {
+ App.Logger.LogInfo($"Opening plugin project in Explorer: {pluginProjectPath}", source: "PluginManager");
+ _ = Process.Start("explorer.exe", pluginProjectPath);
+ return;
+ }
+
+ App.Logger.LogInfo($"Opening plugin project in IDE: {associatedProgram}", source: "PluginManager");
+ ProcessStartInfo psi = new ProcessStartInfo
+ {
+ FileName = associatedProgram,
+ Arguments = csprojPath,
+ };
+
+ _ = Process.Start(psi);
+ }
+
private string GetInstallUninstallButtonText()
{
return mode switch
@@ -94,7 +126,7 @@ internal class PluginEntryDataContext(PluginMetadata pluginMetadata, ICommand co
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
- public record ButtonData(PackIconKind IconKind, string Text, bool IsEnabled, ICommand Command);
+ public record ButtonData(string Icon, string Text, bool IsEnabled, ICommand Command);
public enum Mode
{
diff --git a/src/DesktopMagic/DataContexts/PluginManagerDataContext.cs b/src/DesktopMagic/DataContexts/PluginManagerDataContext.cs
index b1ca54f..896985a 100644
--- a/src/DesktopMagic/DataContexts/PluginManagerDataContext.cs
+++ b/src/DesktopMagic/DataContexts/PluginManagerDataContext.cs
@@ -1,6 +1,10 @@
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
+using System.Windows;
+using System.Windows.Media.Imaging;
+
+using Wpf.Ui.Appearance;
namespace DesktopMagic.DataContexts;
@@ -70,6 +74,23 @@ internal class PluginManagerDataContext : INotifyPropertyChanged
}
}
+ public BitmapImage ModIoIcon
+ {
+ get
+ {
+ ApplicationTheme theme = ApplicationThemeManager.GetAppTheme();
+
+ if (theme == ApplicationTheme.Light)
+ {
+ return Application.Current.Resources["ModioLogoBlueDark"] as BitmapImage ?? new BitmapImage();
+ }
+ else
+ {
+ return Application.Current.Resources["ModioLogoBlueLight"] as BitmapImage ?? new BitmapImage();
+ }
+ }
+ }
+
public string LoginButtonText => isAuthenticated ? (string)App.LanguageDictionary["logout"] : (string)App.LanguageDictionary["login"];
protected void OnPropertyChanged([CallerMemberName] string? name = null)
diff --git a/src/DesktopMagic/DesktopMagic.csproj b/src/DesktopMagic/DesktopMagic.csproj
index 7177fd5..6348d60 100644
--- a/src/DesktopMagic/DesktopMagic.csproj
+++ b/src/DesktopMagic/DesktopMagic.csproj
@@ -7,11 +7,11 @@
icon.ico
OnBuildSuccess
Stone_Red
- 1.2.0.0
+ 1.3.0.0
https://github.com/Stone-Red-Code/DesktopMagic
https://github.com/Stone-Red-Code/DesktopMagic
- 1.2.0.0
- 1.2.0.0
+ 1.3.0.0
+ 1.3.0.0
net8.0-windows7.0
win-x64
enable
@@ -33,6 +33,7 @@
+
@@ -46,9 +47,12 @@
+
+
+
@@ -63,6 +67,7 @@
Always
+
diff --git a/src/DesktopMagic/Dialogs/ColorDialog.xaml b/src/DesktopMagic/Dialogs/ColorDialog.xaml
index 18c4330..16bf62b 100644
--- a/src/DesktopMagic/Dialogs/ColorDialog.xaml
+++ b/src/DesktopMagic/Dialogs/ColorDialog.xaml
@@ -1,50 +1,74 @@
-
+ ExtendsContentIntoTitleBar="True">
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/src/DesktopMagic/Dialogs/ColorDialog.xaml.cs b/src/DesktopMagic/Dialogs/ColorDialog.xaml.cs
index af9b60c..0b860a8 100644
--- a/src/DesktopMagic/Dialogs/ColorDialog.xaml.cs
+++ b/src/DesktopMagic/Dialogs/ColorDialog.xaml.cs
@@ -4,12 +4,14 @@ using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
+using Wpf.Ui.Controls;
+
namespace DesktopMagic.Dialogs;
///
/// Interaction logic for ColorDialog.xaml
///
-public partial class ColorDialog : Window
+public partial class ColorDialog : FluentWindow
{
public System.Drawing.Color ResultColor { get; private set; }
@@ -86,8 +88,8 @@ public partial class ColorDialog : Window
ResultBrush = brush;
ResultColor = systemColor;
- colorRechtangle.Fill = brush;
- colorHexTextBox.Foreground = Brushes.Black;
+ colorRechtangle.Background = brush;
+ colorHexTextBox.Foreground = FindResource("TextFillColorPrimaryBrush") as Brush;
}
else
{
diff --git a/src/DesktopMagic/Dialogs/InputDialog.xaml b/src/DesktopMagic/Dialogs/InputDialog.xaml
index d873210..3e5a734 100644
--- a/src/DesktopMagic/Dialogs/InputDialog.xaml
+++ b/src/DesktopMagic/Dialogs/InputDialog.xaml
@@ -1,22 +1,47 @@
-
+ ExtendsContentIntoTitleBar="True">
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
+
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/src/DesktopMagic/Dialogs/InputDialog.xaml.cs b/src/DesktopMagic/Dialogs/InputDialog.xaml.cs
index c0beac8..981bd82 100644
--- a/src/DesktopMagic/Dialogs/InputDialog.xaml.cs
+++ b/src/DesktopMagic/Dialogs/InputDialog.xaml.cs
@@ -2,7 +2,7 @@
namespace DesktopMagic.Dialogs;
-public partial class InputDialog : Window
+public partial class InputDialog : Wpf.Ui.Controls.FluentWindow
{
public string ResponseText
{
diff --git a/src/DesktopMagic/Dialogs/ThemeDialog.xaml b/src/DesktopMagic/Dialogs/ThemeDialog.xaml
index 4611d40..e97fad9 100644
--- a/src/DesktopMagic/Dialogs/ThemeDialog.xaml
+++ b/src/DesktopMagic/Dialogs/ThemeDialog.xaml
@@ -1,22 +1,45 @@
-
+ ExtendsContentIntoTitleBar="True">
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
@@ -33,44 +56,37 @@
-
-
-
-
-
+
-
-
-
-
-
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
+
+
+
-
-
-
-
-
+
+
+
-
+
-
-
-
-
+
+
+
+
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/src/DesktopMagic/Dialogs/ThemeDialog.xaml.cs b/src/DesktopMagic/Dialogs/ThemeDialog.xaml.cs
index d922c03..38fbb19 100644
--- a/src/DesktopMagic/Dialogs/ThemeDialog.xaml.cs
+++ b/src/DesktopMagic/Dialogs/ThemeDialog.xaml.cs
@@ -10,7 +10,7 @@ namespace DesktopMagic.Dialogs;
///
/// Interaction logic for ColorDialog.xaml
///
-public partial class ThemeDialog : Window
+public partial class ThemeDialog : Wpf.Ui.Controls.FluentWindow
{
private readonly Theme theme;
@@ -22,11 +22,11 @@ public partial class ThemeDialog : Window
Resources.MergedDictionaries.Add(App.LanguageDictionary);
- cornerRadiusTextBox.Text = theme.CornerRadius.ToString();
- marginTextBox.Text = theme.Margin.ToString();
- primaryColorRechtangle.Fill = new SolidColorBrush(MultiColorConverter.ConvertToMediaColor(theme.PrimaryColor));
- secondaryColorRechtangle.Fill = new SolidColorBrush(MultiColorConverter.ConvertToMediaColor(theme.SecondaryColor));
- backgroundColorRechtangle.Fill = new SolidColorBrush(MultiColorConverter.ConvertToMediaColor(theme.BackgroundColor));
+ cornerRadiusTextBox.Value = theme.CornerRadius;
+ marginTextBox.Value = theme.Margin;
+ primaryColorRechtangle.Background = new SolidColorBrush(MultiColorConverter.ConvertToMediaColor(theme.PrimaryColor));
+ secondaryColorRechtangle.Background = new SolidColorBrush(MultiColorConverter.ConvertToMediaColor(theme.SecondaryColor));
+ backgroundColorRechtangle.Background = new SolidColorBrush(MultiColorConverter.ConvertToMediaColor(theme.BackgroundColor));
label.Content = content;
Title = title;
@@ -81,7 +81,7 @@ public partial class ThemeDialog : Window
if (colorDialog.ShowDialog() == true)
{
theme.PrimaryColor = colorDialog.ResultColor;
- primaryColorRechtangle.Fill = colorDialog.ResultBrush;
+ primaryColorRechtangle.Background = colorDialog.ResultBrush;
}
}
@@ -95,7 +95,7 @@ public partial class ThemeDialog : Window
if (colorDialog.ShowDialog() == true)
{
theme.SecondaryColor = colorDialog.ResultColor;
- secondaryColorRechtangle.Fill = colorDialog.ResultBrush;
+ secondaryColorRechtangle.Background = colorDialog.ResultBrush;
}
}
@@ -109,35 +109,23 @@ public partial class ThemeDialog : Window
if (colorDialog.ShowDialog() == true)
{
theme.BackgroundColor = colorDialog.ResultColor;
- backgroundColorRechtangle.Fill = colorDialog.ResultBrush;
+ backgroundColorRechtangle.Background = colorDialog.ResultBrush;
}
}
- private void CornerRadiusTextBox_TextChanged(object? sender, TextChangedEventArgs? e)
+ private void MarginTextBox_ValueChanged(object sender, Wpf.Ui.Controls.NumberBoxValueChangedEventArgs args)
{
- bool success = int.TryParse(cornerRadiusTextBox.Text, out int cornerRadius);
- if (success)
+ if (args.NewValue is >= 0)
{
- cornerRadiusTextBox.Foreground = Brushes.Black;
- theme.CornerRadius = cornerRadius;
- }
- else
- {
- cornerRadiusTextBox.Foreground = Brushes.Red;
+ theme.Margin = (int)args.NewValue;
}
}
- private void MarginTextBox_TextChanged(object? sender, TextChangedEventArgs? e)
+ private void CornerRadiusTextBox_ValueChanged(object sender, Wpf.Ui.Controls.NumberBoxValueChangedEventArgs args)
{
- bool success = int.TryParse(marginTextBox.Text, out int margin);
- if (success)
+ if (args.NewValue is >= 0)
{
- marginTextBox.Foreground = Brushes.Black;
- theme.Margin = margin;
- }
- else
- {
- marginTextBox.Foreground = Brushes.Red;
+ theme.CornerRadius = (int)args.NewValue;
}
}
}
\ No newline at end of file
diff --git a/src/DesktopMagic/GlobalSuppressions.cs b/src/DesktopMagic/GlobalSuppressions.cs
index 18fd782..6930d88 100644
--- a/src/DesktopMagic/GlobalSuppressions.cs
+++ b/src/DesktopMagic/GlobalSuppressions.cs
@@ -9,6 +9,8 @@ using System.Diagnostics.CodeAnalysis;
[assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "No need to")]
[assembly: SuppressMessage("Minor Code Smell", "S1075:URIs should not be hardcoded", Justification = "")]
[assembly: SuppressMessage("Critical Code Smell", "S2696:Instance members should not write to \"static\" fields", Justification = "", Scope = "member", Target = "~P:DesktopMagic.DataContexts.MainWindowDataContext.Settings")]
+[assembly: SuppressMessage("Critical Code Smell", "S2696:Instance members should not write to \"static\" fields", Justification = "", Scope = "member", Target = "~P:DesktopMagic.DataContexts.MainWindowDataContext.DialogService")]
[assembly: SuppressMessage("Major Code Smell", "S3885:\"Assembly.Load\" should be used", Justification = "Assembly.Load does not load dependencies", Scope = "member", Target = "~M:DesktopMagic.PluginWindow.ExecuteSource")]
[assembly: SuppressMessage("Minor Code Smell", "S2325:Methods and properties that don't access instance data should be static", Justification = "", Scope = "member", Target = "~M:DesktopMagic.MainWindow.OpenPluginsFolderButton_Click(System.Object,System.Windows.RoutedEventArgs)")]
[assembly: SuppressMessage("Minor Code Smell", "S2325:Methods and properties that don't access instance data should be static", Justification = "", Scope = "member", Target = "~M:DesktopMagic.MainWindow.ScrollViewer_PreviewMouseWheel(System.Object,System.Windows.Input.MouseWheelEventArgs)")]
+[assembly: SuppressMessage("Major Code Smell", "S6966:Awaitable method should be used", Justification = "Breaks stuff", Scope = "member", Target = "~M:DesktopMagic.PluginWindow.UpdateTimer_Elapsed(System.Object,System.Timers.ElapsedEventArgs)")]
diff --git a/src/DesktopMagic/Helpers/ColorConverter.cs b/src/DesktopMagic/Helpers/ColorConverter.cs
index e2c337b..667de56 100644
--- a/src/DesktopMagic/Helpers/ColorConverter.cs
+++ b/src/DesktopMagic/Helpers/ColorConverter.cs
@@ -6,16 +6,26 @@ namespace DesktopMagic.Helpers;
internal static partial class MultiColorConverter
{
- public static string ConvertToHex(System.Drawing.Color color)
+ public static string ConvertToHexArgb(System.Drawing.Color color)
{
return $"#{color.A:X2}{color.R:X2}{color.G:X2}{color.B:X2}";
}
- public static string ConvertToHex(System.Windows.Media.Color color)
+ public static string ConvertToHexArgb(System.Windows.Media.Color color)
{
return $"#{color.A:X2}{color.R:X2}{color.G:X2}{color.B:X2}";
}
+ public static string ConvertToHexRgba(System.Drawing.Color color)
+ {
+ return $"#{color.R:X2}{color.G:X2}{color.B:X2}{color.A:X2}";
+ }
+
+ public static string ConvertToHexRgba(System.Windows.Media.Color color)
+ {
+ return $"#{color.R:X2}{color.G:X2}{color.B:X2}{color.A:X2}";
+ }
+
public static bool TryConvertToSystemColor(string hex, out System.Drawing.Color color)
{
hex = hex.Replace("#", "");
diff --git a/src/DesktopMagic/Helpers/SettingElementGenerator.cs b/src/DesktopMagic/Helpers/SettingElementGenerator.cs
index 3327a7b..dd236d6 100644
--- a/src/DesktopMagic/Helpers/SettingElementGenerator.cs
+++ b/src/DesktopMagic/Helpers/SettingElementGenerator.cs
@@ -1,19 +1,19 @@
-using DesktopMagic.Plugins;
+using DesktopMagic.Dialogs;
+using DesktopMagic.Plugins;
+
+using Microsoft.Win32;
using System;
using System.Windows;
using System.Windows.Controls;
+using System.Windows.Media;
namespace DesktopMagic.Helpers;
-internal class SettingElementGenerator(ComboBox optionsComboBox)
+internal class SettingElementGenerator(uint pluginId)
{
- private readonly ComboBox optionsComboBox = optionsComboBox;
-
- public void Generate(SettingElement settingElement, DockPanel dockPanel, TextBlock textBlock)
+ public FrameworkElement? Generate(SettingElement settingElement, System.Windows.Controls.TextBlock textBlock)
{
- dockPanel.UpdateLayout();
- textBlock.UpdateLayout();
if (settingElement.Input is DesktopMagic.Api.Settings.Label eLabel)
{
textBlock.Text = eLabel.Value;
@@ -32,16 +32,13 @@ internal class SettingElementGenerator(ComboBox optionsComboBox)
textBlock.Text = eLabel.Value;
});
};
+ return null;
}
else if (settingElement.Input is DesktopMagic.Api.Settings.Button eButton)
{
- Button button = new()
+ Wpf.Ui.Controls.Button button = new()
{
Content = eButton.Value,
- FontSize = 10,
- Height = 20,
- Margin = new Thickness(0, 10, 0, 10),
- Padding = new Thickness(0),
VerticalAlignment = VerticalAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Stretch
};
@@ -64,16 +61,18 @@ internal class SettingElementGenerator(ComboBox optionsComboBox)
});
};
- _ = dockPanel.Children.Add(button);
+ return button;
}
else if (settingElement.Input is DesktopMagic.Api.Settings.CheckBox eCheckBox)
{
- CheckBox checkBox = new()
+ Wpf.Ui.Controls.ToggleSwitch checkBox = new()
{
IsChecked = eCheckBox.Value,
- Style = (Style)dockPanel.FindResource("MaterialDesignDarkCheckBox"),
VerticalAlignment = VerticalAlignment.Center,
- HorizontalAlignment = HorizontalAlignment.Stretch
+ HorizontalAlignment = HorizontalAlignment.Stretch,
+ HorizontalContentAlignment = HorizontalAlignment.Right,
+ Height = 30,
+ Style = Application.Current.FindResource("ToggleSwitchContentLeftStyle") as Style
};
checkBox.Click += (_s, _e) =>
{
@@ -94,11 +93,11 @@ internal class SettingElementGenerator(ComboBox optionsComboBox)
});
};
- _ = dockPanel.Children.Add(checkBox);
+ return checkBox;
}
else if (settingElement.Input is DesktopMagic.Api.Settings.TextBox eTextBox)
{
- TextBox textBox = new()
+ Wpf.Ui.Controls.TextBox textBox = new()
{
Text = eTextBox.Value,
TextWrapping = TextWrapping.Wrap,
@@ -123,26 +122,23 @@ internal class SettingElementGenerator(ComboBox optionsComboBox)
textBox.Text = eTextBox.Value;
});
};
- _ = dockPanel.Children.Add(textBox);
+ return textBox;
}
else if (settingElement.Input is DesktopMagic.Api.Settings.IntegerUpDown eIntegerUpDown)
{
- MaterialDesignThemes.Wpf.NumericUpDown numericUpDown = new()
+ Wpf.Ui.Controls.NumberBox numberBox = new()
{
Value = eIntegerUpDown.Value,
Minimum = eIntegerUpDown.Minimum,
Maximum = eIntegerUpDown.Maximum,
- IncreaseContent = new Label() { Content = "+" },
- DecreaseContent = new Label() { Content = "–" },
VerticalAlignment = VerticalAlignment.Center,
- HorizontalAlignment = HorizontalAlignment.Stretch,
- AllowChangeOnScroll = true
+ HorizontalAlignment = HorizontalAlignment.Stretch
};
- numericUpDown.ValueChanged += (_s, _e) =>
+ numberBox.ValueChanged += (_s, _e) =>
{
try
{
- eIntegerUpDown.Value = numericUpDown.Value;
+ eIntegerUpDown.Value = (int)numberBox.Value;
}
catch (Exception ex)
{
@@ -151,12 +147,12 @@ internal class SettingElementGenerator(ComboBox optionsComboBox)
};
eIntegerUpDown.OnValueChanged += () =>
{
- numericUpDown.Dispatcher.Invoke(() =>
+ numberBox.Dispatcher.Invoke(() =>
{
- numericUpDown.Value = eIntegerUpDown.Value;
+ numberBox.Value = eIntegerUpDown.Value;
});
};
- _ = dockPanel.Children.Add(numericUpDown);
+ return numberBox;
}
else if (settingElement.Input is DesktopMagic.Api.Settings.Slider eSlider)
{
@@ -168,7 +164,8 @@ internal class SettingElementGenerator(ComboBox optionsComboBox)
TickFrequency = 1,
IsSnapToTickEnabled = true,
VerticalAlignment = VerticalAlignment.Center,
- HorizontalAlignment = HorizontalAlignment.Stretch
+ HorizontalAlignment = HorizontalAlignment.Stretch,
+ Margin = new Thickness(0, 5, 0, 5)
};
slider.ValueChanged += (_s, _e) =>
{
@@ -189,7 +186,7 @@ internal class SettingElementGenerator(ComboBox optionsComboBox)
});
};
- _ = dockPanel.Children.Add(slider);
+ return slider;
}
else if (settingElement.Input is DesktopMagic.Api.Settings.ComboBox eComboBox)
{
@@ -220,17 +217,170 @@ internal class SettingElementGenerator(ComboBox optionsComboBox)
comboBox.SelectedItem = eComboBox.Value;
};
- _ = dockPanel.Children.Add(comboBox);
+ return comboBox;
}
+ else if (settingElement.Input is DesktopMagic.Api.Settings.ColorPicker eColorPicker)
+ {
+ Grid panel = new()
+ {
+ HorizontalAlignment = HorizontalAlignment.Stretch
+ };
+ panel.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
+ panel.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
+
+ Border colorPreview = new()
+ {
+ Width = 30,
+ Height = 30,
+ CornerRadius = new CornerRadius(4),
+ Background = new SolidColorBrush(Color.FromArgb(eColorPicker.Value.A, eColorPicker.Value.R, eColorPicker.Value.G, eColorPicker.Value.B)),
+ Margin = new Thickness(0, 0, 10, 0),
+ BorderBrush = Application.Current.FindResource("ControlElevationBorderBrush") as Brush,
+ BorderThickness = new Thickness(1)
+ };
+ Grid.SetColumn(colorPreview, 0);
+
+ Wpf.Ui.Controls.Button browseButton = new()
+ {
+ Content = "Select Color",
+ VerticalAlignment = VerticalAlignment.Center,
+ HorizontalAlignment = HorizontalAlignment.Stretch
+ };
+ Grid.SetColumn(browseButton, 1);
+
+ browseButton.Click += (_s, _e) =>
+ {
+ try
+ {
+ ColorDialog colorDialog = new("Select a color", eColorPicker.Value)
+ {
+ Owner = Application.Current.MainWindow
+ };
+
+ if (colorDialog.ShowDialog() == true)
+ {
+ eColorPicker.Value = colorDialog.ResultColor;
+ colorPreview.Background = new SolidColorBrush(Color.FromArgb(eColorPicker.Value.A, eColorPicker.Value.R, eColorPicker.Value.G, eColorPicker.Value.B));
+ }
+ }
+ catch (Exception ex)
+ {
+ DisplayException(ex.Message);
+ }
+ };
+
+ eColorPicker.OnValueChanged += () =>
+ {
+ colorPreview.Dispatcher.Invoke(() =>
+ {
+ colorPreview.Background = new SolidColorBrush(Color.FromArgb(eColorPicker.Value.A, eColorPicker.Value.R, eColorPicker.Value.G, eColorPicker.Value.B));
+ });
+ };
+
+ _ = panel.Children.Add(colorPreview);
+ _ = panel.Children.Add(browseButton);
+
+ return panel;
+ }
+ else if (settingElement.Input is DesktopMagic.Api.Settings.FileSelector eFileSelector)
+ {
+ Grid grid = new()
+ {
+ HorizontalAlignment = HorizontalAlignment.Stretch
+ };
+ grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
+ grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
+
+ Wpf.Ui.Controls.TextBox pathTextBox = new()
+ {
+ Text = eFileSelector.Value,
+ IsReadOnly = true,
+ VerticalAlignment = VerticalAlignment.Center,
+ HorizontalAlignment = HorizontalAlignment.Stretch,
+ Margin = new Thickness(0, 0, 5, 0)
+ };
+ Grid.SetColumn(pathTextBox, 0);
+
+ Wpf.Ui.Controls.Button browseButton = new()
+ {
+ Content = "Browse",
+ VerticalAlignment = VerticalAlignment.Center
+ };
+ Grid.SetColumn(browseButton, 1);
+
+ browseButton.Click += (_s, _e) =>
+ {
+ try
+ {
+ if (eFileSelector.SelectFolder)
+ {
+ OpenFolderDialog folderDialog = new()
+ {
+ Title = eFileSelector.Title,
+ InitialDirectory = string.IsNullOrEmpty(eFileSelector.Value) ? Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments) : System.IO.Path.GetDirectoryName(eFileSelector.Value)
+ };
+
+ if (folderDialog.ShowDialog() == true)
+ {
+ eFileSelector.Value = folderDialog.FolderName;
+ pathTextBox.Text = eFileSelector.Value;
+ }
+ }
+ else
+ {
+ OpenFileDialog fileDialog = new()
+ {
+ Title = eFileSelector.Title,
+ Filter = eFileSelector.Filter,
+ InitialDirectory = string.IsNullOrEmpty(eFileSelector.Value) ? Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments) : System.IO.Path.GetDirectoryName(eFileSelector.Value)
+ };
+
+ if (fileDialog.ShowDialog() == true)
+ {
+ eFileSelector.Value = fileDialog.FileName;
+ pathTextBox.Text = eFileSelector.Value;
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ DisplayException(ex.Message);
+ }
+ };
+
+ eFileSelector.OnValueChanged += () =>
+ {
+ pathTextBox.Dispatcher.Invoke(() =>
+ {
+ pathTextBox.Text = eFileSelector.Value;
+ });
+ };
+
+ _ = grid.Children.Add(pathTextBox);
+ _ = grid.Children.Add(browseButton);
+
+ return grid;
+ }
+
+ return null;
}
- private void DisplayException(string message)
+ private async void DisplayException(string message)
{
App.Logger.LogInfo(message, source: "PluginInput");
- _ = MessageBox.Show("File execution error:\n" + message, "Error", MessageBoxButton.OK, MessageBoxImage.Error);
- int index = MainWindow.WindowNames.IndexOf(optionsComboBox.SelectedItem.ToString() ?? string.Empty);
+ Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
+ {
+ Title = "Error",
+ Content = "File execution error:\n" + message,
+ CloseButtonText = "Ok"
+ };
+ _ = await messageBox.ShowDialogAsync();
+ int index = Manager.Instance.PluginWindows.FindIndex(p => p.PluginMetadata.Id == pluginId);
- PluginWindow window = MainWindow.Windows[index];
- window?.Exit();
+ if (index >= 0 && index < Manager.Instance.PluginWindows.Count)
+ {
+ IPluginWindow window = Manager.Instance.PluginWindows[index];
+ window?.Exit();
+ }
}
}
\ No newline at end of file
diff --git a/src/DesktopMagic/Helpers/StartupManager.cs b/src/DesktopMagic/Helpers/StartupManager.cs
index 77bdec8..5772cec 100644
--- a/src/DesktopMagic/Helpers/StartupManager.cs
+++ b/src/DesktopMagic/Helpers/StartupManager.cs
@@ -3,7 +3,6 @@
using System;
using System.IO;
using System.Reflection;
-using System.Windows;
namespace DesktopMagic.Helpers;
@@ -67,16 +66,24 @@ public static class StartupManager
}
}
- public static void ToggleAutoStart()
+ public static async void ToggleAutoStart()
{
AutoStartStatus status = IsAutoStartEnabled() ? DisableAutoStart() : EnableAutoStart();
- _ = status switch
+ string message = status switch
{
- AutoStartStatus.Success => MessageBox.Show("Auto-start setting changed successfully."),
- AutoStartStatus.AlreadyEnabled => MessageBox.Show("Auto-start is already enabled."),
- AutoStartStatus.AlreadyDisabled => MessageBox.Show("Auto-start is already disabled."),
- _ => MessageBox.Show("Failed to change auto-start setting."),
+ AutoStartStatus.Success => "Auto-start setting changed successfully.",
+ AutoStartStatus.AlreadyEnabled => "Auto-start is already enabled.",
+ AutoStartStatus.AlreadyDisabled => "Auto-start is already disabled.",
+ _ => "Failed to change auto-start setting.",
};
+
+ Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
+ {
+ Title = App.AppName,
+ Content = message,
+ CloseButtonText = "Ok"
+ };
+ _ = await messageBox.ShowDialogAsync();
}
}
\ No newline at end of file
diff --git a/src/DesktopMagic/Helpers/UnderscoreEscapingConverter.cs b/src/DesktopMagic/Helpers/UnderscoreEscapingConverter.cs
new file mode 100644
index 0000000..7b057ed
--- /dev/null
+++ b/src/DesktopMagic/Helpers/UnderscoreEscapingConverter.cs
@@ -0,0 +1,23 @@
+using System;
+using System.Globalization;
+using System.Windows.Data;
+
+namespace DesktopMagic.Helpers;
+
+public class UnderscoreEscapingConverter : IValueConverter
+{
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value is string text)
+ {
+ // Replaces single underscores with double underscores to escape them in labels
+ return text.Replace("_", "__");
+ }
+ return value;
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+}
diff --git a/src/DesktopMagic/MainWindow.xaml b/src/DesktopMagic/MainWindow.xaml
index 1c1b7be..87d1dd7 100644
--- a/src/DesktopMagic/MainWindow.xaml
+++ b/src/DesktopMagic/MainWindow.xaml
@@ -1,156 +1,107 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
-
-
-
-
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
+
-
-
\ No newline at end of file
+
+
+
\ No newline at end of file
diff --git a/src/DesktopMagic/MainWindow.xaml.cs b/src/DesktopMagic/MainWindow.xaml.cs
index 4cc4f42..aad8d58 100644
--- a/src/DesktopMagic/MainWindow.xaml.cs
+++ b/src/DesktopMagic/MainWindow.xaml.cs
@@ -1,625 +1,157 @@
-using DesktopMagic.BuiltInPlugins;
-using DesktopMagic.DataContexts;
-using DesktopMagic.Dialogs;
-using DesktopMagic.Helpers;
-using DesktopMagic.Plugins;
-using DesktopMagic.Settings;
+using DesktopMagic.DataContexts;
using System;
-using System.Collections.Generic;
using System.Diagnostics;
-using System.IO;
-using System.Linq;
-using System.Text.Json;
using System.Windows;
-using System.Windows.Controls;
-namespace DesktopMagic
+using Wpf.Ui.Appearance;
+using Wpf.Ui.Controls;
+
+namespace DesktopMagic;
+
+public partial class MainWindow : FluentWindow
{
- public partial class MainWindow : Window
+ private readonly Manager _manager = Manager.Instance;
+ private readonly MainWindowDataContext _mainWindowDataContext = new();
+
+ public MainWindow()
{
- private readonly System.Windows.Forms.NotifyIcon notifyIcon = new();
+ SystemThemeWatcher.Watch(this);
- private readonly MainWindowDataContext mainWindowDataContext = new();
+ InitializeComponent();
- private readonly Dictionary builtInPlugins = new()
+ DataContext = _mainWindowDataContext;
+
+ Resources.MergedDictionaries.Add(App.LanguageDictionary);
+ App.DialogService.SetDialogHost(rootContentDialog);
+ }
+
+ private async void Window_Loaded(object sender, RoutedEventArgs e)
+ {
+ try
{
- {new((string)App.LanguageDictionary["musicVisualizer"], 1), typeof(MusicVisualizerPlugin)},
- {new((string)App.LanguageDictionary["time"],2), typeof(TimePlugin)},
- {new((string)App.LanguageDictionary["date"],3), typeof(DatePlugin)},
- {new((string)App.LanguageDictionary["cpuUsage"], 4), typeof(CpuMonitorPlugin)}
- };
+ App.Logger.LogInfo("Loading application", source: "MainWindow");
- private bool loaded = false;
- private bool blockWindowsClosing = true;
- public static List Windows { get; } = [];
- public static List WindowNames { get; } = [];
+ _mainWindowDataContext.IsLoading = true;
- private DesktopMagicSettings Settings
- {
- get => mainWindowDataContext.Settings;
- set => mainWindowDataContext.Settings = value;
+ // Load plugins and settings through manager
+ _manager.LoadSettings();
+ _mainWindowDataContext.Settings = _manager.Settings;
+ _manager.LoadPlugins();
+ _manager.LoadLayout();
+
+ _manager.IsLoaded = true;
+ _mainWindowDataContext.IsLoading = false;
+
+ App.Logger.LogInfo("Application loaded", source: "MainWindow");
}
-
- public MainWindow()
+ catch (Exception ex)
{
- DataContext = mainWindowDataContext;
-
- try
+ App.Logger.LogError(ex.Message, source: "MainWindow");
+ Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
{
- Stream iconStream = Application.GetResourceStream(new Uri("pack://application:,,,/DesktopMagic;component/icon.ico")).Stream;
- notifyIcon.MouseClick += NotifyIcon_MouseClick;
- notifyIcon.Visible = true;
- notifyIcon.Text = App.AppName;
- notifyIcon.Icon = new System.Drawing.Icon(iconStream);
- notifyIcon.ContextMenuStrip = new System.Windows.Forms.ContextMenuStrip()
- {
- Items =
- {
- new System.Windows.Forms.ToolStripMenuItem((string)App.LanguageDictionary["open"], null, (s, e) => RestoreWindow()),
- new System.Windows.Forms.ToolStripMenuItem((string)App.LanguageDictionary["toggleEditMode"], null, (s, e) => { editCheckBox.IsChecked = !editCheckBox.IsChecked; EditCheckBox_Click(null, null); }),
- new System.Windows.Forms.ToolStripMenuItem((string)App.LanguageDictionary["pluginManager"], null, (s, e) => PluginManagerButton_Click(null!, null!)),
- new System.Windows.Forms.ToolStripMenuItem("GitHub", null, (s, e) => GitHubButton_Click(null!, null!)),
- new System.Windows.Forms.ToolStripMenuItem((string)App.LanguageDictionary["quit"], null, (s, e) => Quit()),
- }
- };
-
- InitializeComponent();
-
- Resources.MergedDictionaries.Add(App.LanguageDictionary);
-
-#if DEBUG
- Title = $"{App.AppName} - Dev {System.Windows.Forms.Application.ProductVersion}";
-#else
- Title = $"{App.AppName} - {System.Windows.Forms.Application.ProductVersion}";
-#endif
- }
- catch (Exception ex)
- {
- _ = MessageBox.Show(ex.ToString(), App.AppName, MessageBoxButton.OK, MessageBoxImage.Error);
- }
- }
-
- #region Load
-
- private readonly Dictionary plugins = [];
-
- private void Window_Loaded(object sender, RoutedEventArgs e)
- {
- try
- {
- //Write To Log File and Load Elements
-
- App.Logger.LogInfo("Loading Plugin names", source: "Main");
- LoadPlugins();
- App.Logger.LogInfo("Loading Layout names", source: "Main");
- LoadSettings();
- App.Logger.LogInfo("Loading Layout", source: "Main");
- LoadLayout();
-
- loaded = true;
- App.Logger.LogInfo("Window Loaded", source: "Main");
- }
- catch (Exception ex)
- {
- App.Logger.LogError(ex.Message, source: "Main");
- _ = MessageBox.Show(ex.ToString(), App.AppName, MessageBoxButton.OK, MessageBoxImage.Error);
- }
- }
-
- private void LoadPlugins()
- {
- mainWindowDataContext.IsLoading = true;
- plugins.Clear();
-
- foreach (var buildInPlugin in builtInPlugins.Keys)
- {
- plugins.Add(buildInPlugin.Id, new(buildInPlugin, string.Empty));
- }
-
- foreach (string directory in Directory.GetDirectories(App.PluginsPath))
- {
- string? pluginDllPath = Directory.GetFiles(directory, "main.dll").FirstOrDefault();
- string? pluginMetadataPath = Directory.GetFiles(directory, "metadata.json").FirstOrDefault();
-
- if (pluginDllPath is null)
- {
- App.Logger.LogError($"Plugin \"{directory}\" has no \"main.dll\"", source: "Main");
- continue;
- }
-
- if (pluginMetadataPath is null)
- {
- App.Logger.LogWarn($"Plugin \"{directory}\" has no \"metadata.json\"", source: "Main");
- continue;
- }
-
- PluginMetadata? pluginMetadata = JsonSerializer.Deserialize(File.ReadAllText(pluginMetadataPath));
-
- if (pluginMetadata is null)
- {
- App.Logger.LogError($"Plugin \"{directory}\" has no valid \"metadata.json\"", source: "Main");
- continue;
- }
-
- if (plugins.ContainsKey(pluginMetadata.Id))
- {
- App.Logger.LogError($"Plugin \"{directory}\" has the same id as another plugin", source: "Main");
- continue;
- }
-
- plugins.Add(pluginMetadata.Id, new(pluginMetadata, directory));
- }
- mainWindowDataContext.IsLoading = false;
- }
-
- #endregion Load
-
- #region Windows
-
- private void EditCheckBox_Click(object? sender, RoutedEventArgs? e)
- {
- foreach (PluginWindow window in Windows)
- {
- window.SetEditMode(editCheckBox.IsChecked == true);
- }
-
- SaveSettings();
- }
-
- private void PluginCheckBox_Click(object sender, RoutedEventArgs? e)
- {
- if (sender is not CheckBox checkBox)
- {
- return;
- }
-
- LoadPlugin(uint.Parse(checkBox.Tag.ToString()!));
- }
-
- private void LoadPlugin(uint pluginId)
- {
- if (!plugins.TryGetValue(pluginId, out InternalPluginData? internalPluginData))
- {
- return;
- }
-
- if (!Settings.CurrentLayout.Plugins.TryGetValue(pluginId, out PluginSettings? pluginSettings))
- {
- pluginSettings = new PluginSettings();
- Settings.CurrentLayout.Plugins.Add(pluginId, pluginSettings);
- }
-
- if (WindowNames.Contains(internalPluginData.Metadata.Id.ToString()) || !pluginSettings.Enabled)
- {
- int index = WindowNames.IndexOf(internalPluginData.Metadata.Id.ToString());
-
- if (index >= 0)
- {
- try
- {
- blockWindowsClosing = false;
- Windows[index].Close();
- blockWindowsClosing = true;
- Windows.RemoveAt(index);
- WindowNames.RemoveAt(index);
- }
- catch (Exception ex)
- {
- App.Logger.LogError(ex.Message, source: "Main");
- }
- }
- return;
- }
-
- PluginWindow window;
-
- if (builtInPlugins.TryGetValue(internalPluginData.Metadata, out Type? pluginType))
- {
- window = new PluginWindow((Api.Plugin)Activator.CreateInstance(pluginType)!, internalPluginData.Metadata, pluginSettings)
- {
- Title = internalPluginData.Metadata.Id.ToString()
- };
- }
- else
- {
- window = new PluginWindow(internalPluginData.Metadata, pluginSettings, internalPluginData.DirectoryPath)
- {
- Title = internalPluginData.Metadata.Id.ToString()
- };
- }
-
- Action? onPluginLoaded = null;
- onPluginLoaded = () =>
- {
- Dispatcher.Invoke(() =>
- {
- if (!optionsComboBox.Items.Contains(internalPluginData.Metadata))
- {
- _ = optionsComboBox.Items.Add(internalPluginData.Metadata);
- }
- optionsComboBox.SelectedIndex = -1;
- optionsComboBox.SelectedIndex = optionsComboBox.Items.IndexOf(internalPluginData.Metadata);
- window.PluginLoaded -= onPluginLoaded;
- });
+ Title = App.AppName,
+ Content = ex.ToString(),
+ CloseButtonText = "Ok"
};
-
- window.OnExit += () =>
- {
- Windows.Remove(window);
- WindowNames.Remove(window.Title);
- blockWindowsClosing = false;
- window.Close();
- blockWindowsClosing = true;
- pluginSettings.Enabled = false;
- };
- window.PluginLoaded += onPluginLoaded;
-
- window.ShowInTaskbar = false;
- window.Show();
- window.SetEditMode(editCheckBox.IsChecked == true);
- window.Closing += DisplayWindow_Closing;
- Windows.Add(window);
- WindowNames.Add(window.Title);
- }
-
- private void DisplayWindow_Closing(object? sender, System.ComponentModel.CancelEventArgs e)
- {
- e.Cancel = blockWindowsClosing;
- }
-
- private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
- {
- SaveSettings();
-
- if (blockWindowsClosing)
- {
- e.Cancel = true;
-
- editCheckBox.IsChecked = false;
- EditCheckBox_Click(null, null);
- ShowInTaskbar = false;
- WindowState = WindowState.Minimized;
- Visibility = Visibility.Collapsed;
- }
- }
-
- private void Window_Closed(object sender, EventArgs e)
- {
- Visibility = Visibility.Collapsed;
- UpdateLayout();
- foreach (Window window in Windows)
- {
- window.Hide();
- }
- Environment.Exit(0);
- }
-
- #endregion Windows
-
- #region Options
-
- private void AddThemeButton_Click(object sender, RoutedEventArgs e)
- {
- InputDialog inputDialog = new((string)FindResource("enterThemeName"))
- {
- Owner = this
- };
-
- if (inputDialog.ShowDialog() == true)
- {
- if (Settings.Themes.Any(l => l.Name.Trim() == inputDialog.ResponseText.Trim()))
- {
- _ = MessageBox.Show((string)FindResource("themeAlreadyExists"), App.AppName, MessageBoxButton.OK, MessageBoxImage.Warning);
- return;
- }
-
- Settings.Themes.Add(new Theme(inputDialog.ResponseText.Trim()));
- Settings.CurrentLayout.CurrentThemeName = inputDialog.ResponseText.Trim();
- SaveSettings();
- }
- }
-
- private void DeleteThemeButton_Click(object sender, RoutedEventArgs e)
- {
- if (Settings.Themes.Count <= 1)
- {
- _ = MessageBox.Show((string)FindResource("cannotDeleteLastTheme"), App.AppName, MessageBoxButton.OK, MessageBoxImage.Warning);
- return;
- }
-
- Settings.Themes.Remove((Theme)themesListBox.SelectedItem);
- SaveSettings();
- }
-
- private void ChangeThemeButton_Click(object sender, RoutedEventArgs e)
- {
- Theme theme = themesListBox.SelectedItem as Theme ?? Settings.CurrentLayout.Theme;
-
- ThemeDialog themeDialog = new(theme.Name, theme, App.AppName)
- {
- Owner = this
- };
-
- themeDialog.ShowDialog();
- }
-
- private void OptionsComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
- {
- optionsPanel.Visibility = Visibility.Visible;
- optionsPanel.Children.Clear();
- optionsPanel.UpdateLayout();
-
- if (optionsComboBox.SelectedItem is null)
- {
- return;
- }
-
- bool success = Settings.CurrentLayout.Plugins.TryGetValue(((PluginMetadata)optionsComboBox.SelectedItem).Id, out Settings.PluginSettings? pluginSettings);
- if (!success || pluginSettings is null || pluginSettings.Settings.Count == 0)
- {
- _ = optionsPanel.Children.Add(new TextBlock() { Text = (string)FindResource("noOptions") });
- return;
- }
-
- SettingElementGenerator settingElementGenerator = new SettingElementGenerator(optionsComboBox);
-
- foreach (SettingElement settingElement in pluginSettings.Settings)
- {
- DockPanel dockPanel = new()
- {
- LastChildFill = true,
- HorizontalAlignment = HorizontalAlignment.Stretch,
- Margin = new Thickness(0, 0, 0, 5)
- };
- _ = optionsPanel.Children.Add(dockPanel);
-
- TextBlock textBlock = new()
- {
- Text = $"{settingElement.Name}:",
- Padding = new Thickness(0, 0, 3, 0),
- VerticalAlignment = VerticalAlignment.Center
- };
-
- _ = dockPanel.Children.Add(textBlock);
- settingElementGenerator.Generate(settingElement, dockPanel, textBlock);
- }
- }
-
- #endregion Options
-
- internal void RestoreWindow()
- {
- for (int i = 0; i < 10; i++)
- {
- ShowInTaskbar = true;
- Visibility = Visibility.Visible;
- SystemCommands.RestoreWindow(this);
- Topmost = true;
- _ = Activate();
- Topmost = false;
- }
- }
-
- private void Quit()
- {
- blockWindowsClosing = false;
- Close();
- }
-
- private void OpenPluginsFolderButton_Click(object sender, RoutedEventArgs e)
- {
- _ = Process.Start("explorer.exe", App.PluginsPath);
- }
-
- private void ScrollViewer_PreviewMouseWheel(object sender, System.Windows.Input.MouseWheelEventArgs e)
- {
- ScrollViewer scv = (ScrollViewer)sender;
- scv.ScrollToVerticalOffset(scv.VerticalOffset - e.Delta);
- e.Handled = true;
- }
-
- #region Layout
-
- private readonly JsonSerializerOptions jsonSettingsOptions = new()
- {
- Converters =
- {
- new ColorJsonConverter()
- }
- };
-
- private void LayoutsComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
- {
- if (loaded)
- {
- SaveSettings();
- LoadLayout(false);
- }
- }
-
- private void NewLayoutButton_Click(object sender, RoutedEventArgs e)
- {
- InputDialog inputDialog = new((string)FindResource("enterLayoutName"))
- {
- Owner = this
- };
-
- if (inputDialog.ShowDialog() == true)
- {
- if (Settings.Layouts.Any(l => l.Name.Trim() == inputDialog.ResponseText.Trim()))
- {
- _ = MessageBox.Show((string)FindResource("layoutAlreadyExists"), App.AppName, MessageBoxButton.OK, MessageBoxImage.Warning);
- return;
- }
-
- Settings.Layouts.Add(new Layout(inputDialog.ResponseText.Trim()));
- Settings.CurrentLayoutName = inputDialog.ResponseText.Trim();
- SaveSettings();
- }
- }
-
- private void RemoveLayoutButton_Click(object sender, RoutedEventArgs e)
- {
- if (Settings.Layouts.Count <= 1)
- {
- _ = MessageBox.Show((string)FindResource("cannotDeleteLastLayout"), App.AppName, MessageBoxButton.OK, MessageBoxImage.Warning);
- return;
- }
-
- Settings.Layouts.Remove(Settings.CurrentLayout);
- SaveSettings();
- }
-
- private void SaveSettings()
- {
- if (!loaded)
- {
- return;
- }
-
- string json = JsonSerializer.Serialize(Settings, jsonSettingsOptions);
- File.WriteAllText(Path.Combine(App.ApplicationDataPath, "settings.json"), json);
- }
-
- private void LoadSettings()
- {
- if (!File.Exists(Path.Combine(App.ApplicationDataPath, "settings.json")))
- {
- Settings = new DesktopMagicSettings();
-
- Settings.Layouts.Add(new Layout((string)FindResource("default")));
- Settings.Themes.Add(new Theme((string)FindResource("default")));
-
- return;
- }
-
- string json = File.ReadAllText(Path.Combine(App.ApplicationDataPath, "settings.json"));
-
- Settings = JsonSerializer.Deserialize(json, jsonSettingsOptions) ?? new DesktopMagicSettings();
-
- if(Settings.Layouts.Count == 0)
- {
- Settings.Layouts.Add(new Layout((string)FindResource("default")));
- }
-
- if (Settings.Themes.Count == 0)
- {
- Settings.Themes.Add(new Theme((string)FindResource("default")));
- }
- }
-
- private void LoadLayout(bool minimize = true)
- {
- mainWindowDataContext.IsLoading = true;
- blockWindowsClosing = false;
-
- foreach (Window window in Windows)
- {
- window.Close();
- }
-
- editCheckBox.IsChecked = false;
- EditCheckBox_Click(null, null);
- blockWindowsClosing = true;
- Windows.Clear();
- WindowNames.Clear();
- optionsComboBox.Items.Clear();
-
- bool showWindow = true;
-
- // Load plugins
- foreach (uint pluginId in plugins.Keys)
- {
- InternalPluginData internalPluginData = plugins[pluginId];
-
- // Add plugin to layout if it doesn't exist
- if (!Settings.CurrentLayout.Plugins.TryGetValue(pluginId, out PluginSettings? pluginSettings))
- {
- Settings.CurrentLayout.Plugins.Add(pluginId, new PluginSettings() { Name = internalPluginData.Metadata.Name });
-
- continue;
- }
-
- pluginSettings.Name = internalPluginData.Metadata.Name;
-
- if (pluginSettings.Enabled)
- {
- LoadPlugin(pluginId);
- }
-
- if (showWindow && pluginSettings.Enabled)
- {
- showWindow = false;
- }
- }
-
- // Remove plugins that are not loaded anymore
- foreach (uint pluginId in Settings.CurrentLayout.Plugins.Keys)
- {
- if (!plugins.ContainsKey(pluginId))
- {
- Settings.CurrentLayout.Plugins.Remove(pluginId);
- }
- }
-
- Settings.CurrentLayout.UpdatePlugins();
-
- if (!showWindow && minimize)
- {
- Close();
- }
- else
- {
- RestoreWindow();
- }
-
- mainWindowDataContext.IsLoading = false;
- }
-
- #endregion Layout
-
- private void UpdatePluginsButton_Click(object sender, RoutedEventArgs e)
- {
- LoadPlugins();
- LoadLayout(false);
- }
-
- private void NotifyIcon_MouseClick(object? sender, System.Windows.Forms.MouseEventArgs e)
- {
- if (e.Button == System.Windows.Forms.MouseButtons.Left)
- {
- RestoreWindow();
- }
- }
-
- private void GitHubButton_Click(object sender, RoutedEventArgs e)
- {
- string uri = "https://github.com/Stone-Red-Code/DesktopMagic";
- ProcessStartInfo psi = new ProcessStartInfo
- {
- UseShellExecute = true,
- FileName = uri
- };
- _ = Process.Start(psi);
- }
-
- private void PluginManagerButton_Click(object sender, RoutedEventArgs e)
- {
- PluginManager pluginManager = new PluginManager
- {
- Owner = this
- };
-
- pluginManager.ShowDialog();
- LoadPlugins();
- LoadLayout(Visibility != Visibility.Visible);
+ _ = await messageBox.ShowDialogAsync();
}
}
- internal class InternalPluginData(PluginMetadata pluginMetadata, string directoryPath)
+ private async void NavigationView_Loaded(object sender, RoutedEventArgs e)
{
- public PluginMetadata Metadata { get; set; } = pluginMetadata;
- public string DirectoryPath { get; set; } = directoryPath;
+ _ = NavigationView.Navigate(typeof(Pages.MainPage));
+ }
+
+ private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
+ {
+ _manager.SaveSettings();
+
+ if (_manager.BlockWindowsClosing)
+ {
+ e.Cancel = true;
+ _manager.SetEditMode(false);
+ ShowInTaskbar = false;
+ WindowState = WindowState.Minimized;
+ Visibility = Visibility.Collapsed;
+ }
+ }
+
+ private void Window_Closed(object sender, EventArgs e)
+ {
+ Visibility = Visibility.Collapsed;
+ UpdateLayout();
+ _manager.CloseAllPluginWindows();
+ Environment.Exit(0);
+ }
+
+ private void NotifyIcon_MouseClick(object? sender, System.Windows.Forms.MouseEventArgs e)
+ {
+ if (e.Button == System.Windows.Forms.MouseButtons.Left)
+ {
+ RestoreWindow();
+ }
+ }
+
+ internal void RestoreWindow()
+ {
+ for (int i = 0; i < 10; i++)
+ {
+ ShowInTaskbar = true;
+ Visibility = Visibility.Visible;
+ SystemCommands.RestoreWindow(this);
+ Topmost = true;
+ _ = Activate();
+ Topmost = false;
+ }
+ }
+
+ private void Quit()
+ {
+ _manager.BlockWindowsClosing = false;
+ Close();
+ }
+
+ private void ReportBugNavigationViewItem_Click(object sender, RoutedEventArgs e)
+ {
+ string uri = "https://github.com/Stone-Red-Code/DesktopMagic/issues/new?template=bug_report.md";
+ ProcessStartInfo psi = new()
+ {
+ UseShellExecute = true,
+ FileName = uri
+ };
+ _ = Process.Start(psi);
+ }
+
+ private void RequestFeatureNavigationViewItem_Click(object sender, RoutedEventArgs e)
+ {
+ string uri = "https://github.com/Stone-Red-Code/DesktopMagic/issues/new?template=feature_request.md";
+ ProcessStartInfo psi = new()
+ {
+ UseShellExecute = true,
+ FileName = uri
+ };
+ _ = Process.Start(psi);
+ }
+
+ private void NotifyIcon_LeftClick(Wpf.Ui.Tray.Controls.NotifyIcon sender, RoutedEventArgs e)
+ {
+ RestoreWindow();
+ }
+
+ private void SettingsMenuItem_Click(object sender, RoutedEventArgs e)
+ {
+ RestoreWindow();
+ _ = NavigationView.Navigate(typeof(Pages.MainPage));
+ }
+
+ private void EditLayoutMenuItem_Click(object sender, RoutedEventArgs e)
+ {
+ _manager.SetEditMode(!_manager.IsEditMode);
+ }
+
+ private void QuitMenuItem_Click(object sender, RoutedEventArgs e)
+ {
+ Quit();
}
}
\ No newline at end of file
diff --git a/src/DesktopMagic/Manager.cs b/src/DesktopMagic/Manager.cs
new file mode 100644
index 0000000..7a9d009
--- /dev/null
+++ b/src/DesktopMagic/Manager.cs
@@ -0,0 +1,373 @@
+using DesktopMagic.BuiltInPlugins;
+using DesktopMagic.Helpers;
+using DesktopMagic.Plugins;
+using DesktopMagic.Settings;
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text.Json;
+using System.Windows;
+
+namespace DesktopMagic;
+
+///
+/// Singleton Manager class that handles global state, plugin management, and shared operations
+///
+public sealed class Manager
+{
+ private static Manager? _instance;
+ private static readonly object _lock = new();
+
+ public static Manager Instance
+ {
+ get
+ {
+ if (_instance == null)
+ {
+ lock (_lock)
+ {
+ _instance ??= new Manager();
+ }
+ }
+ return _instance;
+ }
+ }
+
+ // Plugin management
+ private readonly Dictionary _plugins = [];
+ private readonly Dictionary _builtInPlugins = new()
+ {
+ {new((string)App.LanguageDictionary["musicVisualizer"], 1) { Author = "Stone_Red" }, typeof(MusicVisualizerPlugin)},
+ {new((string)App.LanguageDictionary["time"],2) { Author = "Stone_Red" }, typeof(TimePlugin)},
+ {new((string)App.LanguageDictionary["date"],3) { Author = "Stone_Red" }, typeof(DatePlugin)},
+ {new((string)App.LanguageDictionary["cpuUsage"], 4) { Author = "Stone_Red" }, typeof(CpuMonitorPlugin)},
+ {new((string)App.LanguageDictionary["weather"], 5) { Author = "Stone_Red" }, typeof(WeatherPlugin)},
+ };
+
+ // Window management
+ public List PluginWindows { get; } = [];
+
+ public IReadOnlyDictionary Plugins => _plugins;
+
+ public bool BlockWindowsClosing { get; set; } = true;
+
+ // Edit mode tracking
+ private bool _isEditMode = false;
+ public bool IsEditMode => _isEditMode;
+
+ // Settings
+ public DesktopMagicSettings Settings { get; set; } = new();
+ public bool IsLoaded { get; set; } = false;
+
+ private readonly JsonSerializerOptions _jsonSettingsOptions = new()
+ {
+ Converters = { new ColorJsonConverter() }
+ };
+
+ // Events
+ public event Action? PluginsChanged;
+ public event Action? SettingsChanged;
+ public event Action? EditModeChanged;
+
+ private Manager()
+ {
+ // Private constructor for singleton
+ }
+
+ #region Plugin Management
+
+ public Dictionary GetPlugins() => new(_plugins);
+
+ public void LoadPlugins()
+ {
+ App.Logger.LogInfo("Loading plugins", source: "Manager");
+ _plugins.Clear();
+
+ // Load built-in plugins
+ foreach (PluginMetadata builtInPlugin in _builtInPlugins.Keys)
+ {
+ _plugins.Add(builtInPlugin.Id, new(builtInPlugin, PluginType.DotNet, string.Empty));
+ }
+
+ // Load external plugins
+ foreach (string directory in Directory.GetDirectories(App.PluginsPath))
+ {
+ string? pluginDllPath = Directory.GetFiles(directory, "main.dll").FirstOrDefault();
+ string? pluginHtmlPath = Directory.GetFiles(directory, "main.html").FirstOrDefault();
+ string? pluginMetadataPath = Directory.GetFiles(directory, "metadata.json").FirstOrDefault();
+
+ if (pluginDllPath is null && pluginHtmlPath is null)
+ {
+ App.Logger.LogError($"Plugin \"{directory}\" has no \"main.dll\" or \"main.html\"", source: "Manager");
+ continue;
+ }
+
+ if (pluginMetadataPath is null)
+ {
+ App.Logger.LogWarn($"Plugin \"{directory}\" has no \"metadata.json\"", source: "Manager");
+ continue;
+ }
+
+ PluginMetadata? pluginMetadata = JsonSerializer.Deserialize(File.ReadAllText(pluginMetadataPath));
+
+ if (pluginMetadata is null)
+ {
+ App.Logger.LogError($"Plugin \"{directory}\" has no valid \"metadata.json\"", source: "Manager");
+ continue;
+ }
+
+ if (_plugins.ContainsKey(pluginMetadata.Id))
+ {
+ App.Logger.LogError($"Plugin \"{directory}\" has the same id as another plugin", source: "Manager");
+ continue;
+ }
+
+ PluginType pluginType = pluginHtmlPath is not null ? PluginType.Web : PluginType.DotNet;
+
+ _plugins.Add(pluginMetadata.Id, new(pluginMetadata, pluginType, directory));
+ }
+
+ PluginsChanged?.Invoke();
+ App.Logger.LogInfo($"Loaded {_plugins.Count} plugins", source: "Manager");
+ }
+
+ public void LoadPlugin(uint pluginId, Action? onPluginLoaded = null)
+ {
+ if (!_plugins.TryGetValue(pluginId, out InternalPluginData? internalPluginData))
+ {
+ return;
+ }
+
+ if (!Settings.CurrentLayout.Plugins.TryGetValue(pluginId, out PluginSettings? pluginSettings))
+ {
+ pluginSettings = new PluginSettings();
+ Settings.CurrentLayout.Plugins.Add(pluginId, pluginSettings);
+ }
+
+ IPluginWindow? existingWindow = PluginWindows.FirstOrDefault(w => w.PluginMetadata.Id == internalPluginData.Metadata.Id);
+
+ if (existingWindow is not null || !pluginSettings.Enabled)
+ {
+ // Close the window if it's already open or disabled
+ if (existingWindow is not null)
+ {
+ try
+ {
+ BlockWindowsClosing = false;
+ existingWindow.Close();
+ BlockWindowsClosing = true;
+ PluginWindows.Remove(existingWindow);
+ }
+ catch (Exception ex)
+ {
+ App.Logger.LogError(ex.Message, source: "Manager");
+ }
+ }
+ return;
+ }
+
+ IPluginWindow window;
+
+ if (_builtInPlugins.TryGetValue(internalPluginData.Metadata, out Type? pluginType))
+ {
+ window = new PluginWindow((Api.Plugin)Activator.CreateInstance(pluginType)!, internalPluginData.Metadata, pluginSettings)
+ {
+ Title = internalPluginData.Metadata.Id.ToString()
+ };
+ }
+ else if (internalPluginData.Type == PluginType.Web)
+ {
+ window = new WebPluginWindow(internalPluginData.Metadata, pluginSettings, internalPluginData.DirectoryPath)
+ {
+ Title = internalPluginData.Metadata.Id.ToString()
+ };
+ }
+ else
+ {
+ window = new PluginWindow(internalPluginData.Metadata, pluginSettings, internalPluginData.DirectoryPath)
+ {
+ Title = internalPluginData.Metadata.Id.ToString()
+ };
+ }
+
+ Action? pluginLoadedHandler = null;
+ pluginLoadedHandler = () =>
+ {
+ onPluginLoaded?.Invoke(internalPluginData);
+ window.PluginLoaded -= pluginLoadedHandler;
+ };
+
+ Action exitHandler = () =>
+ {
+ PluginWindows.Remove(window);
+ BlockWindowsClosing = false;
+ window.Close();
+ BlockWindowsClosing = true;
+ pluginSettings.Enabled = false;
+ };
+
+ window.PluginLoaded += pluginLoadedHandler;
+ window.OnExit += exitHandler;
+
+ window.Show();
+ window.SetEditMode(_isEditMode);
+
+ PluginWindows.Add(window);
+ }
+
+ public void ReloadPlugins()
+ {
+ App.Logger.LogInfo("Reloading plugins", source: "PluginManager");
+
+ LoadPlugins();
+ LoadLayout(false);
+ }
+
+ public void SetEditMode(bool editMode)
+ {
+ _isEditMode = editMode;
+ foreach (IPluginWindow window in PluginWindows)
+ {
+ window.SetEditMode(editMode);
+ }
+ EditModeChanged?.Invoke(editMode);
+ SaveSettings();
+ }
+
+ #endregion
+
+ #region Settings Management
+
+ public void LoadSettings()
+ {
+ App.Logger.LogInfo("Loading settings", source: "Manager");
+
+ if (!File.Exists(Path.Combine(App.ApplicationDataPath, "settings.json")))
+ {
+ Settings = new DesktopMagicSettings();
+ Settings.Layouts.Add(new Layout("Default"));
+ Settings.Themes.Add(new Theme("Default"));
+ return;
+ }
+
+ string json = File.ReadAllText(Path.Combine(App.ApplicationDataPath, "settings.json"));
+ Settings = JsonSerializer.Deserialize(json, _jsonSettingsOptions) ?? new DesktopMagicSettings();
+
+ if (Settings.Layouts.Count == 0)
+ {
+ Settings.Layouts.Add(new Layout("Default"));
+ }
+
+ if (Settings.Themes.Count == 0)
+ {
+ Settings.Themes.Add(new Theme("Default"));
+ }
+
+ SettingsChanged?.Invoke();
+ }
+
+ public void SaveSettings()
+ {
+ if (!IsLoaded)
+ {
+ return;
+ }
+
+ string json = JsonSerializer.Serialize(Settings, _jsonSettingsOptions);
+ File.WriteAllText(Path.Combine(App.ApplicationDataPath, "settings.json"), json);
+ App.Logger.LogInfo("Settings saved", source: "Manager");
+ SettingsChanged?.Invoke();
+ }
+
+ #endregion
+
+ #region Layout Management
+
+ public void LoadLayout(bool minimize = true, Action? onComplete = null)
+ {
+ App.Logger.LogInfo("Loading layout", source: "Manager");
+ BlockWindowsClosing = false;
+
+ foreach (IPluginWindow window in PluginWindows)
+ {
+ window.Close();
+ }
+
+ BlockWindowsClosing = true;
+ PluginWindows.Clear();
+
+ bool showWindow = true;
+
+ // Load plugins
+ foreach (uint pluginId in _plugins.Keys)
+ {
+ InternalPluginData internalPluginData = _plugins[pluginId];
+
+ // Add plugin to layout if it doesn't exist
+ if (!Settings.CurrentLayout.Plugins.TryGetValue(pluginId, out PluginSettings? pluginSettings))
+ {
+ Settings.CurrentLayout.Plugins.Add(pluginId, new PluginSettings() { Metadata = internalPluginData.Metadata });
+ continue;
+ }
+
+ pluginSettings.Metadata = internalPluginData.Metadata;
+
+ if (pluginSettings.Enabled)
+ {
+ LoadPlugin(pluginId);
+ }
+
+ if (showWindow && pluginSettings.Enabled)
+ {
+ showWindow = false;
+ }
+ }
+
+ // Remove plugins that are not loaded anymore
+ List pluginIdsToRemove = Settings.CurrentLayout.Plugins.Keys.Where(id => !_plugins.ContainsKey(id)).ToList();
+ foreach (uint pluginId in pluginIdsToRemove)
+ {
+ Settings.CurrentLayout.Plugins.Remove(pluginId);
+ }
+
+ Settings.CurrentLayout.UpdatePlugins();
+
+ if (minimize && !showWindow)
+ {
+ Application.Current.MainWindow.WindowState = WindowState.Minimized;
+ Application.Current.MainWindow.ShowInTaskbar = false;
+ }
+
+ onComplete?.Invoke();
+ App.Logger.LogInfo("Layout loaded", source: "Manager");
+ }
+
+ #endregion
+
+ #region Cleanup
+
+ public void CloseAllPluginWindows()
+ {
+ foreach (IPluginWindow window in PluginWindows)
+ {
+ window.Hide();
+ }
+ }
+
+ #endregion
+}
+
+public class InternalPluginData(PluginMetadata pluginMetadata, PluginType pluginType, string directoryPath)
+{
+ public PluginMetadata Metadata { get; set; } = pluginMetadata;
+ public PluginType Type { get; set; } = pluginType;
+ public string DirectoryPath { get; set; } = directoryPath;
+}
+
+public enum PluginType
+{
+ DotNet,
+ Web
+}
\ No newline at end of file
diff --git a/src/DesktopMagic/Pages/MainPage.xaml b/src/DesktopMagic/Pages/MainPage.xaml
new file mode 100644
index 0000000..713d53e
--- /dev/null
+++ b/src/DesktopMagic/Pages/MainPage.xaml
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/DesktopMagic/Pages/MainPage.xaml.cs b/src/DesktopMagic/Pages/MainPage.xaml.cs
new file mode 100644
index 0000000..65cc4d3
--- /dev/null
+++ b/src/DesktopMagic/Pages/MainPage.xaml.cs
@@ -0,0 +1,283 @@
+using DesktopMagic.DataContexts;
+using DesktopMagic.Dialogs;
+using DesktopMagic.Helpers;
+using DesktopMagic.Plugins;
+using DesktopMagic.Settings;
+
+using System.Collections.Generic;
+using System.Linq;
+using System.Windows;
+using System.Windows.Controls;
+
+namespace DesktopMagic.Pages;
+
+///
+/// Interaction logic for MainPage.xaml
+///
+public partial class MainPage : Page
+{
+ private readonly Manager _manager = Manager.Instance;
+ private readonly MainWindowDataContext _dataContext;
+ private bool _isLoadingLayout = false;
+
+ public MainPage()
+ {
+ InitializeComponent();
+
+ _dataContext = new MainWindowDataContext
+ {
+ Settings = _manager.Settings
+ };
+
+ DataContext = _dataContext;
+
+ // Subscribe to manager events
+ _manager.PluginsChanged += OnPluginsChanged;
+ _manager.EditModeChanged += OnEditModeChanged;
+
+ Loaded += MainPage_Loaded;
+ Unloaded += MainPage_Unloaded;
+ }
+
+ private void MainPage_Loaded(object sender, RoutedEventArgs e)
+ {
+ // Initialize edit checkbox state
+ editCheckBox.IsChecked = _manager.IsEditMode;
+ }
+
+ private void MainPage_Unloaded(object sender, RoutedEventArgs e)
+ {
+ // Unsubscribe from events
+ _manager.PluginsChanged -= OnPluginsChanged;
+ _manager.EditModeChanged -= OnEditModeChanged;
+ }
+
+ private void OnPluginsChanged()
+ {
+ Dispatcher.Invoke(() =>
+ {
+ // Refresh the UI if needed
+ _dataContext.Settings = _manager.Settings;
+ });
+ }
+
+ private void OnEditModeChanged(bool editMode)
+ {
+ Dispatcher.Invoke(() =>
+ {
+ editCheckBox.IsChecked = editMode;
+ });
+ }
+
+ private void EditCheckBox_Click(object sender, RoutedEventArgs e)
+ {
+ _manager.SetEditMode(editCheckBox.IsChecked == true);
+ }
+
+ private void PluginCheckBox_Click(object sender, RoutedEventArgs e)
+ {
+ if (sender is not Control checkBox)
+ {
+ return;
+ }
+
+ uint pluginId = uint.Parse(checkBox.Tag.ToString()!);
+
+ _manager.LoadPlugin(pluginId, (internalPluginData) =>
+ {
+ Dispatcher.Invoke(() =>
+ {
+ // Update the card expander to show options when enabling a plugin
+ Wpf.Ui.Controls.CardExpander? cardExpander = ((checkBox.Parent as FrameworkElement)?.Parent) as Wpf.Ui.Controls.CardExpander;
+
+ if (cardExpander is not null)
+ {
+ OptionsCardExpander_Expanded(cardExpander, new RoutedEventArgs());
+ }
+ });
+ });
+ }
+
+ private void ScrollViewer_PreviewMouseWheel(object sender, System.Windows.Input.MouseWheelEventArgs e)
+ {
+ if (sender is ScrollViewer scv)
+ {
+ scv.ScrollToVerticalOffset(scv.VerticalOffset - e.Delta);
+ e.Handled = true;
+ }
+ }
+
+ #region Layout Management
+
+ private void LayoutsComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ // Prevent recursive calls and only process if fully loaded
+ if (_isLoadingLayout || !_manager.IsLoaded || !IsLoaded)
+ {
+ return;
+ }
+
+ // Check if this is actually a user-initiated change
+ // by verifying that the removed and added items are different
+ if (e.RemovedItems.Count > 0 && e.AddedItems.Count > 0)
+ {
+ if (e.RemovedItems[0] == e.AddedItems[0])
+ {
+ return;
+ }
+ }
+
+ try
+ {
+ _isLoadingLayout = true;
+ _manager.SaveSettings();
+ _manager.LoadLayout(false);
+ }
+ finally
+ {
+ _isLoadingLayout = false;
+ }
+ }
+
+ private async void NewLayoutButton_Click(object sender, RoutedEventArgs e)
+ {
+ InputDialog inputDialog = new((string)FindResource("enterLayoutName"))
+ {
+ Owner = Window.GetWindow(this)
+ };
+
+ if (inputDialog.ShowDialog() == true)
+ {
+ if (_manager.Settings.Layouts.Any(l => l.Name.Trim() == inputDialog.ResponseText.Trim()))
+ {
+ Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
+ {
+ Title = App.AppName,
+ Content = (string)FindResource("layoutAlreadyExists"),
+ CloseButtonText = "Ok"
+ };
+ _ = await messageBox.ShowDialogAsync();
+ return;
+ }
+
+ try
+ {
+ _isLoadingLayout = true;
+ _manager.Settings.Layouts.Add(new Layout(inputDialog.ResponseText.Trim()));
+ _manager.Settings.CurrentLayoutName = inputDialog.ResponseText.Trim();
+ _manager.SaveSettings();
+ _manager.LoadLayout(false);
+ }
+ finally
+ {
+ _isLoadingLayout = false;
+ }
+ }
+ }
+
+ private async void RemoveLayoutButton_Click(object sender, RoutedEventArgs e)
+ {
+ if (_manager.Settings.Layouts.Count <= 1)
+ {
+ Wpf.Ui.Controls.MessageBox cannotDeleteMessageBox = new Wpf.Ui.Controls.MessageBox
+ {
+ Title = App.AppName,
+ Content = (string)FindResource("cannotDeleteLastLayout"),
+ CloseButtonText = "Ok"
+ };
+ _ = await cannotDeleteMessageBox.ShowDialogAsync();
+ return;
+ }
+
+ Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
+ {
+ Title = App.AppName,
+ Content = (string)FindResource("confirmDeleteLayout"),
+ PrimaryButtonText = "Yes",
+ SecondaryButtonText = "No",
+ IsCloseButtonEnabled = false
+ };
+ Wpf.Ui.Controls.MessageBoxResult result = await messageBox.ShowDialogAsync();
+ if (result != Wpf.Ui.Controls.MessageBoxResult.Primary) // Primary is "Yes"
+ {
+ return;
+ }
+
+ try
+ {
+ _isLoadingLayout = true;
+ _ = _manager.Settings.Layouts.Remove(_manager.Settings.CurrentLayout);
+ _manager.SaveSettings();
+ _manager.LoadLayout(false);
+ }
+ finally
+ {
+ _isLoadingLayout = false;
+ }
+ }
+
+ #endregion
+
+ private void OptionsCardExpander_Expanded(object sender, RoutedEventArgs e)
+ {
+ if (sender is not Wpf.Ui.Controls.CardExpander expander || expander.Tag is not KeyValuePair keyValuePair)
+ {
+ return;
+ }
+
+ PluginSettings? pluginSettings = keyValuePair.Value;
+ uint pluginId = keyValuePair.Key;
+
+ StackPanel optionsPanel = new StackPanel
+ {
+ Visibility = Visibility.Visible
+ };
+
+ expander.Content = optionsPanel;
+ optionsPanel.UpdateLayout();
+
+ // s.Input being null means the plugins has not been loaded yet but the settings are present in the saved configuration.
+ if (pluginSettings is null || pluginSettings.Settings.Count == 0 || pluginSettings.Settings.All(s => s.Input is null))
+ {
+ _ = optionsPanel.Children.Add(new System.Windows.Controls.TextBlock()
+ {
+ Text = (string)FindResource(pluginSettings?.Enabled == true ? "noOptions" : "enablePluginToConfigure")
+ });
+
+ return;
+ }
+
+ SettingElementGenerator settingElementGenerator = new SettingElementGenerator(pluginId);
+
+ foreach (SettingElement settingElement in pluginSettings.Settings)
+ {
+ Wpf.Ui.Controls.CardControl card = new()
+ {
+ Margin = new Thickness(0, 0, 0, 5),
+ Padding = new Thickness(5)
+ };
+
+ System.Windows.Controls.TextBlock textBlock = new()
+ {
+ Text = string.IsNullOrWhiteSpace(settingElement.Name) ? string.Empty : settingElement.Name,
+ Padding = new Thickness(0, 0, 3, 0),
+ VerticalAlignment = VerticalAlignment.Center,
+ FontWeight = FontWeights.SemiBold
+ };
+
+ card.Header = textBlock;
+
+ _ = optionsPanel.Children.Add(card);
+
+ FrameworkElement? control = settingElementGenerator.Generate(settingElement, textBlock);
+
+ if (control is not null)
+ {
+ control.MinWidth = 200;
+ card.Content = control;
+ }
+ }
+
+ optionsPanel.UpdateLayout();
+ }
+}
diff --git a/src/DesktopMagic/Pages/ThemePage.xaml b/src/DesktopMagic/Pages/ThemePage.xaml
new file mode 100644
index 0000000..653b9eb
--- /dev/null
+++ b/src/DesktopMagic/Pages/ThemePage.xaml
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/DesktopMagic/Pages/ThemePage.xaml.cs b/src/DesktopMagic/Pages/ThemePage.xaml.cs
new file mode 100644
index 0000000..1bfa67d
--- /dev/null
+++ b/src/DesktopMagic/Pages/ThemePage.xaml.cs
@@ -0,0 +1,126 @@
+using DesktopMagic.DataContexts;
+using DesktopMagic.Dialogs;
+using DesktopMagic.Plugins;
+
+using System.Linq;
+using System.Windows;
+using System.Windows.Controls;
+
+namespace DesktopMagic.Pages;
+
+///
+/// Interaction logic for ThemePage.xaml
+///
+public partial class ThemePage : Page
+{
+ private readonly Manager _manager = Manager.Instance;
+ private readonly MainWindowDataContext _dataContext;
+
+ public ThemePage()
+ {
+ InitializeComponent();
+
+ _dataContext = new MainWindowDataContext
+ {
+ Settings = _manager.Settings
+ };
+
+ DataContext = _dataContext;
+
+ // Subscribe to manager events
+ _manager.SettingsChanged += OnSettingsChanged;
+
+ Unloaded += ThemePage_Unloaded;
+ }
+
+ private void ThemePage_Unloaded(object sender, RoutedEventArgs e)
+ {
+ // Unsubscribe from events
+ _manager.SettingsChanged -= OnSettingsChanged;
+ }
+
+ private void OnSettingsChanged()
+ {
+ Dispatcher.Invoke(() =>
+ {
+ _dataContext.Settings = _manager.Settings;
+ });
+ }
+
+ private async void AddThemeButton_Click(object sender, RoutedEventArgs e)
+ {
+ InputDialog inputDialog = new((string)FindResource("enterThemeName"))
+ {
+ Owner = Window.GetWindow(this)
+ };
+
+ if (inputDialog.ShowDialog() == true)
+ {
+ if (_manager.Settings.Themes.Any(l => l.Name.Trim() == inputDialog.ResponseText.Trim()))
+ {
+ Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
+ {
+ Title = App.AppName,
+ Content = (string)FindResource("themeAlreadyExists"),
+ CloseButtonText = "Ok"
+ };
+ _ = await messageBox.ShowDialogAsync();
+ return;
+ }
+
+ _manager.Settings.Themes.Add(new Theme(inputDialog.ResponseText.Trim()));
+ _manager.Settings.CurrentLayout.CurrentThemeName = inputDialog.ResponseText.Trim();
+ _manager.SaveSettings();
+ }
+ }
+
+ private async void DeleteThemeButton_Click(object sender, RoutedEventArgs e)
+ {
+ if (_manager.Settings.Themes.Count <= 1)
+ {
+ Wpf.Ui.Controls.MessageBox cannotDeleteMessageBox = new Wpf.Ui.Controls.MessageBox
+ {
+ Title = App.AppName,
+ Content = (string)FindResource("cannotDeleteLastTheme"),
+ CloseButtonText = "Ok"
+ };
+ _ = await cannotDeleteMessageBox.ShowDialogAsync();
+ return;
+ }
+
+ var messageBox = new Wpf.Ui.Controls.MessageBox
+ {
+ Title = App.AppName,
+ Content = (string)FindResource("confirmDeleteTheme"),
+ PrimaryButtonText = "Yes",
+ SecondaryButtonText = "No",
+ IsCloseButtonEnabled = false
+ };
+ Wpf.Ui.Controls.MessageBoxResult result = await messageBox.ShowDialogAsync();
+ if (result != Wpf.Ui.Controls.MessageBoxResult.Primary) // Primary is "Yes"
+ {
+ return;
+ }
+
+ if (themesListBox.SelectedItem is Theme theme)
+ {
+ _ = _manager.Settings.Themes.Remove(theme);
+ _manager.SaveSettings();
+ }
+ }
+
+ private void EditThemeButton_Click(object sender, RoutedEventArgs e)
+ {
+ if (sender is not System.Windows.Controls.Button button || button.Tag is not Theme theme)
+ {
+ return;
+ }
+
+ ThemeDialog themeDialog = new(theme.Name, theme, App.AppName)
+ {
+ Owner = Window.GetWindow(this)
+ };
+
+ _ = themeDialog.ShowDialog();
+ }
+}
diff --git a/src/DesktopMagic/Plugins/IPluginWindow.cs b/src/DesktopMagic/Plugins/IPluginWindow.cs
new file mode 100644
index 0000000..d5e8ec4
--- /dev/null
+++ b/src/DesktopMagic/Plugins/IPluginWindow.cs
@@ -0,0 +1,24 @@
+using DesktopMagic.Plugins;
+
+using System;
+
+namespace DesktopMagic;
+
+public interface IPluginWindow
+{
+ event Action? PluginLoaded;
+ event Action? OnExit;
+
+ bool IsRunning { get; }
+ PluginMetadata PluginMetadata { get; }
+ string PluginFolderPath { get; }
+ string Title { get; set; }
+
+ void Exit();
+ void SetEditMode(bool enabled);
+ void Show();
+ void Hide();
+ void Close();
+
+ event System.ComponentModel.CancelEventHandler Closing;
+}
diff --git a/src/DesktopMagic/Plugins/PluginData.cs b/src/DesktopMagic/Plugins/PluginData.cs
index 6a39238..92fd242 100644
--- a/src/DesktopMagic/Plugins/PluginData.cs
+++ b/src/DesktopMagic/Plugins/PluginData.cs
@@ -1,8 +1,12 @@
-using DesktopMagic.Api;
+using CuteUtils.Logging;
+
+using DesktopMagic.Api;
using DesktopMagic.Settings;
using System.Drawing;
+using Wpf.Ui.Controls;
+
namespace DesktopMagic.Plugins;
internal class PluginData(PluginWindow window, PluginSettings pluginSettings) : IPluginData
@@ -19,8 +23,56 @@ internal class PluginData(PluginWindow window, PluginSettings pluginSettings) :
public string PluginPath => window.PluginFolderPath;
+ public void Log(string message, LogLevel level = LogLevel.Info)
+ {
+ LogSeverity severity = level switch
+ {
+ LogLevel.Info => LogSeverity.Info,
+ LogLevel.Warning => LogSeverity.Warn,
+ LogLevel.Error => LogSeverity.Error,
+ _ => LogSeverity.Info,
+ };
+
+ App.Logger.Log($"\"{PluginName}\" - {message}", "Plugin", severity);
+ }
+
+ public void ShowMessage(string message, string? title = null)
+ {
+ title ??= PluginName;
+
+ _ = window.Dispatcher.Invoke(async () =>
+ {
+ System.Windows.Window temporaryOwner = new()
+ {
+ AllowsTransparency = true,
+ ShowInTaskbar = false,
+ WindowStyle = System.Windows.WindowStyle.None,
+ Background = System.Windows.Media.Brushes.Transparent,
+ Topmost = true,
+ };
+
+ temporaryOwner.Show();
+
+ MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
+ {
+ Owner = temporaryOwner,
+ Title = $"{App.AppName} - {title}",
+ Content = message,
+ CloseButtonText = "Ok"
+ };
+ _ = await messageBox.ShowDialogAsync();
+
+ temporaryOwner.Close();
+ });
+ }
+
public void UpdateWindow()
{
window.UpdatePluginWindow();
}
+
+ public void SaveState()
+ {
+ window.SavePluginState();
+ }
}
\ No newline at end of file
diff --git a/src/DesktopMagic/Plugins/PluginEntry.xaml b/src/DesktopMagic/Plugins/PluginEntry.xaml
index d29b84a..3710e77 100644
--- a/src/DesktopMagic/Plugins/PluginEntry.xaml
+++ b/src/DesktopMagic/Plugins/PluginEntry.xaml
@@ -2,8 +2,8 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
- xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:local="clr-namespace:DesktopMagic.Plugins"
xmlns:dataContext="clr-namespace:DesktopMagic.DataContexts"
d:DataContext="{d:DesignInstance Type=dataContext:PluginEntryDataContext}"
@@ -12,52 +12,52 @@
Background="{DynamicResource MaterialDesignPaper}"
FontFamily="{DynamicResource MaterialDesignFont}"
Visibility="{Binding Visibility}">
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
\ No newline at end of file
diff --git a/src/DesktopMagic/Plugins/PluginManager.xaml b/src/DesktopMagic/Plugins/PluginManager.xaml
index 8031cac..b994a6a 100644
--- a/src/DesktopMagic/Plugins/PluginManager.xaml
+++ b/src/DesktopMagic/Plugins/PluginManager.xaml
@@ -1,25 +1,21 @@
-
+ d:DesignHeight="450"
+ d:DesignWidth="800"
+ ScrollViewer.CanContentScroll="False"
+ Title="{DynamicResource pluginManager}">
-
-
+
+
@@ -28,21 +24,29 @@
+
-
+
+
+
+
+
+
+
-
+
+
+
+
-
-
+
+
-
-
-
+
@@ -64,17 +68,11 @@
-
-
-
-
-
-
+
+
-
-
-
+
@@ -95,23 +93,39 @@
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/src/DesktopMagic/Plugins/PluginManager.xaml.cs b/src/DesktopMagic/Plugins/PluginManager.xaml.cs
index f203336..a37a331 100644
--- a/src/DesktopMagic/Plugins/PluginManager.xaml.cs
+++ b/src/DesktopMagic/Plugins/PluginManager.xaml.cs
@@ -8,7 +8,6 @@ using Modio.Models;
using System;
using System.Collections.Generic;
-using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using System.IO;
@@ -19,6 +18,7 @@ using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows;
+using System.Windows.Controls;
using System.Windows.Threading;
using System.Xml.Linq;
@@ -30,7 +30,7 @@ namespace DesktopMagic.Plugins;
///
/// Interaction logic for PluginManager.xaml
///
-public partial class PluginManager : Window
+public partial class PluginManager : Page
{
private const int ModIoGameId = 5665;
private const string ModIoApiKey = "88e6ea774c3a502b06114e7fee0829ac";
@@ -38,6 +38,9 @@ public partial class PluginManager : Window
private readonly PluginManagerDataContext pluginManagerDataContext = new();
private readonly string pluginsPath = Path.Combine(App.ApplicationDataPath, "Plugins");
private readonly string pluginDevelopmentPath = Path.Combine(App.ApplicationDataPath, "PluginDevelopment");
+ private readonly Manager _manager = Manager.Instance;
+
+ private bool changed = false;
private readonly DispatcherTimer searchTimer = new()
{
@@ -55,11 +58,13 @@ public partial class PluginManager : Window
if (modIoAccessToken is null)
{
modIoClient = new Client(new Credentials(ModIoApiKey));
+ App.Logger.LogInfo("Initialized mod.io client without authentication", source: "PluginManager");
}
else
{
modIoClient = new Client(new Credentials(ModIoApiKey, modIoAccessToken));
pluginManagerDataContext.IsAuthenticated = true;
+ App.Logger.LogInfo("Initialized mod.io client with authentication", source: "PluginManager");
}
InitializeComponent();
@@ -70,11 +75,115 @@ public partial class PluginManager : Window
searchTimer.Stop();
await SearchAllPlugins(pluginManagerDataContext.AllPluginsSearchText);
};
+
+ Loaded += PluginManager_Loaded;
+ Unloaded += PluginManager_Unloaded;
+ }
+
+ private async void PluginManager_Loaded(object sender, RoutedEventArgs e)
+ {
+ await InitializePluginManager();
+ }
+
+ private void PluginManager_Unloaded(object sender, RoutedEventArgs e)
+ {
+ searchTimer.Stop();
+
+ if (changed)
+ {
+ _manager.ReloadPlugins();
+ }
+ }
+
+ private async Task InitializePluginManager()
+ {
+ App.Logger.LogInfo("Initializing Plugin Manager", source: "PluginManager");
+ HashSet pluginIds = [];
+
+ pluginManagerDataContext.IsLoading = true;
+ pluginManagerDataContext.InstalledPlugins.Clear();
+ pluginManagerDataContext.AllPlugins.Clear();
+
+ foreach (string pluginPath in Directory.GetDirectories(pluginsPath))
+ {
+ string pluginMetadataPath = Path.Combine(pluginPath, "metadata.json");
+
+ if (!File.Exists(pluginMetadataPath))
+ {
+ App.Logger.LogWarn($"Plugin metadata not found at: {pluginMetadataPath}", source: "PluginManager");
+ continue;
+ }
+
+ PluginMetadata? pluginMetadata = JsonSerializer.Deserialize(await File.ReadAllTextAsync(pluginMetadataPath));
+
+ if (pluginMetadata is null)
+ {
+ App.Logger.LogWarn($"Failed to deserialize plugin metadata from: {pluginMetadataPath}", source: "PluginManager");
+ continue;
+ }
+
+ string csprojPath = GetCsprojPath(pluginMetadata.Name);
+
+ App.Logger.LogInfo($"Loaded plugin: {pluginMetadata.Name} (ID: {pluginMetadata.Id})", source: "PluginManager");
+ pluginManagerDataContext.InstalledPlugins.Add(new PluginEntryDataContext(pluginMetadata, new CommandHandler(async () => await Remove(pluginPath, pluginMetadata.Id)), PluginEntryDataContext.Mode.Uninstall, pluginPath, csprojPath));
+ _ = pluginIds.Add(pluginMetadata.Id);
+
+ if (pluginMetadata.IsLocalPlugin)
+ {
+ continue;
+ }
+
+ try
+ {
+ Mod mod = await modIoClient.Games[ModIoGameId].Mods[pluginMetadata.Id].Get();
+
+ if (DateTimeOffset.FromUnixTimeSeconds(mod.DateUpdated).DateTime > pluginMetadata.Updated && pluginMetadata.SupportsUnloading)
+ {
+ App.Logger.LogInfo($"Plugin {pluginMetadata.Id} has an update available, reinstalling", source: "PluginManager");
+ await Remove(pluginPath, pluginMetadata.Id);
+ await Install(mod);
+ pluginManagerDataContext.IsLoading = true;
+ }
+ }
+ catch (Exception ex)
+ {
+ App.Logger.LogError($"Failed to check for plugin {pluginMetadata.Id} updates: {ex.Message}", source: "PluginManager");
+ }
+ }
+
+ Filter filter = ModFilter.Popular.Desc().Limit(100);
+
+ try
+ {
+ App.Logger.LogInfo("Fetching popular plugins from mod.io", source: "PluginManager");
+ IAsyncEnumerable mods = modIoClient.Games[ModIoGameId].Mods.Search(filter).ToEnumerable();
+ await foreach (Mod mod in mods)
+ {
+ if (pluginIds.Contains(mod.Id))
+ {
+ continue;
+ }
+
+ pluginManagerDataContext.AllPlugins.Add(new PluginEntryDataContext(new(mod), new CommandHandler(async () => await Install(mod)), PluginEntryDataContext.Mode.Install));
+ }
+ App.Logger.LogInfo($"Loaded {pluginManagerDataContext.AllPlugins.Count} available plugins", source: "PluginManager");
+ }
+ catch (Exception ex)
+ {
+ App.Logger.LogError($"Failed to fetch plugins from mod.io: {ex.Message}", source: "PluginManager");
+ }
+
+ await SyncPlugins();
+
+ pluginManagerDataContext.IsLoading = false;
+ App.Logger.LogInfo("Plugin Manager initialization complete", source: "PluginManager");
}
public async Task Remove(string pluginPath, uint id)
{
+ App.Logger.LogInfo($"Removing plugin with ID {id} from path: {pluginPath}", source: "PluginManager");
pluginManagerDataContext.IsLoading = true;
+ changed = true;
PluginEntryDataContext? pluginEntryDataContext = pluginManagerDataContext.InstalledPlugins.FirstOrDefault(p => p.Id == id);
@@ -83,10 +192,17 @@ public partial class PluginManager : Window
try
{
Directory.Delete(pluginPath, true);
+ App.Logger.LogInfo($"Successfully deleted plugin directory: {pluginPath}", source: "PluginManager");
}
catch (Exception ex)
{
- _ = MessageBox.Show(ex.Message, "Plugin Manager", MessageBoxButton.OK, MessageBoxImage.Error);
+ Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
+ {
+ Title = "Plugin Manager",
+ Content = ex.Message,
+ CloseButtonText = "Ok"
+ };
+ _ = await messageBox.ShowDialogAsync();
App.Logger.LogError(ex.Message, source: "PluginManager");
}
}
@@ -94,71 +210,37 @@ public partial class PluginManager : Window
if (pluginEntryDataContext is not null)
{
_ = pluginManagerDataContext.InstalledPlugins.Remove(pluginEntryDataContext);
+ App.Logger.LogInfo($"Removed plugin {id} from installed plugins list", source: "PluginManager");
}
if (pluginManagerDataContext.IsAuthenticated)
{
- await modIoClient.Games[ModIoGameId].Mods.Unsubscribe(id);
+ try
+ {
+ await modIoClient.Games[ModIoGameId].Mods.Unsubscribe(id);
+ App.Logger.LogInfo($"Unsubscribed from plugin {id} on mod.io", source: "PluginManager");
+ }
+ catch (Exception ex)
+ {
+ App.Logger.LogError($"Failed to unsubscribe from plugin {id}: {ex.Message}", source: "PluginManager");
+ }
}
pluginManagerDataContext.IsLoading = false;
}
- protected override async void OnInitialized(EventArgs e)
- {
- base.OnInitialized(e);
-
- HashSet pluginIds = [];
-
- foreach (string pluginPath in Directory.GetDirectories(pluginsPath))
- {
- string pluginMetadataPath = Path.Combine(pluginPath, "metadata.json");
- if (!File.Exists(pluginMetadataPath))
- {
- continue;
- }
-
- PluginMetadata? pluginMetadata = JsonSerializer.Deserialize(await File.ReadAllTextAsync(pluginMetadataPath));
-
- if (pluginMetadata is not null)
- {
- pluginManagerDataContext.InstalledPlugins.Add(new PluginEntryDataContext(pluginMetadata, new CommandHandler(async () => await Remove(pluginPath, pluginMetadata.Id)), PluginEntryDataContext.Mode.Uninstall, pluginPath));
- _ = pluginIds.Add(pluginMetadata.Id);
- }
- }
-
- Filter filter = ModFilter.Popular.Desc().Limit(100);
-
- IAsyncEnumerable mods = modIoClient.Games[ModIoGameId].Mods.Search(filter).ToEnumerable();
- await foreach (Mod mod in mods)
- {
- if (pluginIds.Contains(mod.Id))
- {
- continue;
- }
-
- pluginManagerDataContext.AllPlugins.Add(new PluginEntryDataContext(new(mod), new CommandHandler(async () => await Install(mod)), PluginEntryDataContext.Mode.Install));
- }
-
- await SyncPlugins();
-
- pluginManagerDataContext.IsLoading = false;
- }
-
- protected override void OnClosing(CancelEventArgs e)
- {
- e.Cancel = pluginManagerDataContext.IsLoading;
- }
-
[GeneratedRegex(@"[^a-zA-Z0-9]")]
private static partial Regex IdentifierNameRegex();
private async Task Install(Mod mod)
{
+ App.Logger.LogInfo($"Installing plugin: {mod.Name} (ID: {mod.Id})", source: "PluginManager");
pluginManagerDataContext.IsLoading = true;
+ changed = true;
if (mod.Modfile?.Download?.BinaryUrl is null)
{
+ App.Logger.LogWarn($"Plugin {mod.Id} has no download URL", source: "PluginManager");
return;
}
@@ -166,45 +248,77 @@ public partial class PluginManager : Window
string pluginPath = Path.Combine(pluginsPath, pluginGuid);
string zipFilePath = Path.Combine(pluginsPath, pluginGuid + ".zip");
- using (Stream fileStream = await httpClient.GetStreamAsync(mod.Modfile.Download.BinaryUrl))
+ try
{
- using FileStream outputFileStream = new FileStream(zipFilePath, FileMode.Create);
+ App.Logger.LogInfo($"Downloading plugin from: {mod.Modfile.Download.BinaryUrl}", source: "PluginManager");
+ using (Stream fileStream = await httpClient.GetStreamAsync(mod.Modfile.Download.BinaryUrl))
+ {
+ using FileStream outputFileStream = new FileStream(zipFilePath, FileMode.Create);
+ await fileStream.CopyToAsync(outputFileStream);
+ }
+ App.Logger.LogInfo($"Plugin downloaded to: {zipFilePath}", source: "PluginManager");
- await fileStream.CopyToAsync(outputFileStream);
+ using (ZipArchive zipArchive = ZipFile.OpenRead(zipFilePath))
+ {
+ zipArchive.ExtractToDirectory(pluginPath);
+ }
+ App.Logger.LogInfo($"Plugin extracted to: {pluginPath}", source: "PluginManager");
+
+ string pluginMetadataPath = Path.Combine(pluginPath, "metadata.json");
+ await File.WriteAllTextAsync(pluginMetadataPath, JsonSerializer.Serialize(new PluginMetadata(mod)));
+
+ File.Delete(zipFilePath);
+
+ if (!File.Exists(Path.Combine(pluginPath, "main.dll")) && !File.Exists(Path.Combine(pluginPath, "main.html")))
+ {
+ App.Logger.LogError($"Plugin {mod.Id} does not contain main.dll or main.html", source: "PluginManager");
+ await Remove(pluginPath, mod.Id);
+ pluginManagerDataContext.IsLoading = false;
+ Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
+ {
+ Title = "Plugin Manager",
+ Content = "The plugin you are trying to install does not contain a \"main.dll\" or \"main.html\" file. Please contact the plugin author.",
+ CloseButtonText = "Ok"
+ };
+ _ = await messageBox.ShowDialogAsync();
+ return;
+ }
+
+ PluginEntryDataContext? pluginEntryDataContext = pluginManagerDataContext.AllPlugins.FirstOrDefault(p => p.Id == mod.Id);
+
+ if (pluginEntryDataContext is not null)
+ {
+ _ = pluginManagerDataContext.AllPlugins.Remove(pluginEntryDataContext);
+ }
+
+ if (pluginManagerDataContext.IsAuthenticated)
+ {
+ try
+ {
+ await modIoClient.Games[ModIoGameId].Mods.Subscribe(mod.Id);
+ App.Logger.LogInfo($"Subscribed to plugin {mod.Id} on mod.io", source: "PluginManager");
+ }
+ catch (Exception ex)
+ {
+ App.Logger.LogError($"Failed to subscribe to plugin {mod.Id}: {ex.Message}", source: "PluginManager");
+ }
+ }
+
+ pluginManagerDataContext.InstalledPlugins.Add(new PluginEntryDataContext(new PluginMetadata(mod), new CommandHandler(async () => await Remove(pluginPath, mod.Id)), PluginEntryDataContext.Mode.Uninstall, pluginPath));
+ App.Logger.LogInfo($"Successfully installed plugin: {mod.Name} (ID: {mod.Id})", source: "PluginManager");
}
-
- using (ZipArchive zipArchive = ZipFile.OpenRead(zipFilePath))
+ catch (Exception ex)
{
- zipArchive.ExtractToDirectory(pluginPath);
+ App.Logger.LogError($"Failed to install plugin {mod.Id}: {ex.Message}", source: "PluginManager");
+ Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
+ {
+ Title = "Plugin Manager",
+ Content = $"Failed to install plugin: {ex.Message}",
+ CloseButtonText = "Ok"
+ };
+ _ = await messageBox.ShowDialogAsync();
}
- string pluginMetadataPath = Path.Combine(pluginPath, "metadata.json");
- await File.WriteAllTextAsync(pluginMetadataPath, JsonSerializer.Serialize(new PluginMetadata(mod)));
-
- File.Delete(zipFilePath);
-
- if (!File.Exists(Path.Combine(pluginPath, "main.dll")))
- {
- _ = Remove(pluginPath, mod.Id);
- pluginManagerDataContext.IsLoading = false;
- _ = MessageBox.Show("The plugin you are trying to install does not contain a \"main.dll\" file. Please contact the plugin author.", "Plugin Manager", MessageBoxButton.OK, MessageBoxImage.Error);
- return;
- }
-
- PluginEntryDataContext? pluginEntryDataContext = pluginManagerDataContext.AllPlugins.FirstOrDefault(p => p.Id == mod.Id);
-
- if (pluginEntryDataContext is not null)
- {
- _ = pluginManagerDataContext.AllPlugins.Remove(pluginEntryDataContext);
- }
-
- if (pluginManagerDataContext.IsAuthenticated)
- {
- await modIoClient.Games[ModIoGameId].Mods.Subscribe(mod.Id);
- }
-
- pluginManagerDataContext.InstalledPlugins.Add(new PluginEntryDataContext(new PluginMetadata(mod), new CommandHandler(async () => await Remove(pluginPath, mod.Id)), PluginEntryDataContext.Mode.Uninstall, pluginPath));
-
pluginManagerDataContext.IsLoading = false;
}
@@ -217,6 +331,7 @@ public partial class PluginManager : Window
FileName = uri
};
_ = Process.Start(psi);
+ App.Logger.LogInfo($"Opened mod.io page: {uri}", source: "PluginManager");
}
private void AllPluginsSearchTextBox_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
@@ -246,6 +361,7 @@ public partial class PluginManager : Window
private async Task SearchAllPlugins(string searchString)
{
+ App.Logger.LogInfo($"Searching plugins with query: {searchString}", source: "PluginManager");
pluginManagerDataContext.AllPlugins.Clear();
if (!searchString.Contains('*'))
@@ -255,38 +371,68 @@ public partial class PluginManager : Window
Filter filter = ModFilter.Name.Like($"{searchString}").And(ModFilter.Popular.Desc()).Limit(100);
- IAsyncEnumerable mods = modIoClient.Games[ModIoGameId].Mods.Search(filter).ToEnumerable();
- await foreach (Mod mod in mods)
+ try
{
- if (pluginManagerDataContext.InstalledPlugins.Any(p => p.Id == mod.Id))
+ IAsyncEnumerable mods = modIoClient.Games[ModIoGameId].Mods.Search(filter).ToEnumerable();
+ await foreach (Mod mod in mods)
{
- continue;
- }
+ if (pluginManagerDataContext.InstalledPlugins.Any(p => p.Id == mod.Id))
+ {
+ continue;
+ }
- pluginManagerDataContext.AllPlugins.Add(new PluginEntryDataContext(new(mod), new CommandHandler(async () => await Install(mod)), PluginEntryDataContext.Mode.Install));
+ pluginManagerDataContext.AllPlugins.Add(new PluginEntryDataContext(new(mod), new CommandHandler(async () => await Install(mod)), PluginEntryDataContext.Mode.Install));
+ }
+ App.Logger.LogInfo($"Search completed: {pluginManagerDataContext.AllPlugins.Count} plugins found", source: "PluginManager");
+ }
+ catch (Exception ex)
+ {
+ App.Logger.LogError($"Plugin search failed: {ex.Message}", source: "PluginManager");
}
pluginManagerDataContext.IsSearching = false;
}
- private void CreatePluginButton_Click(object sender, RoutedEventArgs e)
+ private async void ReloadPluginsButton_Click(object sender, RoutedEventArgs e)
+ {
+ _manager.ReloadPlugins();
+ await InitializePluginManager();
+ changed = false;
+ }
+
+ private async void CreatePluginButton_Click(object sender, RoutedEventArgs e)
{
try
{
- CreateNewPlugin();
+ await CreateNewPlugin();
}
catch (Exception ex)
{
App.Logger.LogError(ex.Message, source: "PluginManager");
- _ = MessageBox.Show(ex.Message, "Plugin Manager", MessageBoxButton.OK, MessageBoxImage.Error);
+ Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
+ {
+ Title = "Plugin Manager",
+ Content = ex.Message,
+ CloseButtonText = "Ok"
+ };
+ _ = await messageBox.ShowDialogAsync();
}
}
- private void CreateNewPlugin()
+ private async Task CreateNewPlugin()
{
+ App.Logger.LogInfo("Creating new plugin", source: "PluginManager");
+
if (!FileUtilities.ExistsOnPath("dotnet.exe"))
{
- _ = MessageBox.Show("The .NET SDK is required to create a plugin. Please install it and try again.", "Plugin Manager", MessageBoxButton.OK, MessageBoxImage.Error);
+ App.Logger.LogError(".NET SDK not found on PATH", source: "PluginManager");
+ Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
+ {
+ Title = "Plugin Manager",
+ Content = "The .NET SDK is required to create a plugin. Please install it and try again.",
+ CloseButtonText = "Ok"
+ };
+ _ = await messageBox.ShowDialogAsync();
return;
}
@@ -296,20 +442,19 @@ public partial class PluginManager : Window
InputDialog inputDialog = new((string)FindResource("enterPluginName"), "Plugin Manager")
{
- Owner = this,
+ Owner = Window.GetWindow(this),
};
if (inputDialog.ShowDialog() != true)
{
+ App.Logger.LogInfo("Plugin creation cancelled by user", source: "PluginManager");
return;
}
string pluginName = inputDialog.ResponseText;
- string pluginSafeName = pluginName.ToLower().Replace("_", " ");
+ string pluginSafeName = GetPluginSafeName(pluginName);
- TextInfo info = CultureInfo.CurrentCulture.TextInfo;
- pluginSafeName = info.ToTitleCase(pluginSafeName);
- pluginSafeName = IdentifierNameRegex().Replace(pluginSafeName, "");
+ App.Logger.LogInfo($"Creating plugin with name: {pluginName} (safe name: {pluginSafeName})", source: "PluginManager");
if (!Directory.Exists(pluginDevelopmentPath))
{
@@ -320,25 +465,35 @@ public partial class PluginManager : Window
if (Directory.Exists(pluginProjectPath))
{
- _ = MessageBox.Show("A plugin with the same name already exists. Please choose a different name.", "Plugin Manager", MessageBoxButton.OK, MessageBoxImage.Error);
+ App.Logger.LogWarn($"Plugin project already exists: {pluginProjectPath}", source: "PluginManager");
+ Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
+ {
+ Title = "Plugin Manager",
+ Content = "A plugin with the same name already exists. Please choose a different name.",
+ CloseButtonText = "Ok"
+ };
+ _ = await messageBox.ShowDialogAsync();
return;
}
+ pluginManagerDataContext.IsLoading = true;
+
PluginMetadata pluginMetadata = new(pluginName, pluginId);
_ = Directory.CreateDirectory(pluginPath);
_ = Directory.CreateDirectory(pluginProjectPath);
string pluginMetadataPath = Path.Combine(pluginPath, "metadata.json");
- File.WriteAllText(pluginMetadataPath, JsonSerializer.Serialize(pluginMetadata));
+ await File.WriteAllTextAsync(pluginMetadataPath, JsonSerializer.Serialize(pluginMetadata));
+ App.Logger.LogInfo($"Creating .NET project at: {pluginProjectPath}", source: "PluginManager");
string cmd = $"new classlib -n {pluginSafeName} -o {pluginProjectPath} -f net8.0 --target-framework-override net8.0-windows7";
Process process = Process.Start("dotnet", cmd);
- process.WaitForExit();
+ await process.WaitForExitAsync();
- // Install the required NuGet packages
+ App.Logger.LogInfo("Installing NuGet package: DesktopMagic.Api", source: "PluginManager");
process = Process.Start("dotnet", $"add {pluginProjectPath} package DesktopMagic.Api");
- process.WaitForExit();
+ await process.WaitForExitAsync();
File.Move(Path.Combine(pluginProjectPath, "Class1.cs"), Path.Combine(pluginProjectPath, $"{pluginSafeName}.cs"));
@@ -352,26 +507,21 @@ public class {pluginSafeName}Plugin : Plugin
public override Bitmap Main()
{{
Bitmap bmp = new Bitmap(2000, 1000);
+ bmp.SetResolution(100, 100); // Set DPI to avoid scaling issues.
using (Graphics g = Graphics.FromImage(bmp))
{{
- g.Clear(Application.Theme.PrimaryColor); // Set the background color to the color specified in the DesktopMagic application.
-
- g.DrawString(""Hello World"", new Font(Application.Theme.Font, 100), Brushes.Black, new PointF(0, 0)); // Draw ""Hello World"" to the image.
+ g.DrawString(""Hello World!"", new Font(Application.Theme.Font, 100), new SolidBrush(Application.Theme.PrimaryColor), new PointF(0, 0)); // Draw ""Hello World"" to the image.
}}
- bmp.SetResolution(300, 300); // Set DPI to avoid scaling issues.
-
return bmp; // Return the image.
}}
}}
";
- // Update the .csproj file
-
string csprojPath = Path.Combine(pluginProjectPath, $"{pluginSafeName}.csproj");
- File.WriteAllText(Path.Combine(pluginProjectPath, $"{pluginSafeName}.cs"), code);
+ await File.WriteAllTextAsync(Path.Combine(pluginProjectPath, $"{pluginSafeName}.cs"), code);
XDocument doc = XDocument.Load(csprojPath);
XElement? propertyGroup = doc.Root?.Element("PropertyGroup");
@@ -412,62 +562,88 @@ public class {pluginSafeName}Plugin : Plugin
}
doc.Save(csprojPath);
+ App.Logger.LogInfo("Updated .csproj file with custom build settings", source: "PluginManager");
}
else
{
- _ = MessageBox.Show("PropertyGroup element not found in .csproj", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
+ App.Logger.LogError("PropertyGroup element not found in .csproj", source: "PluginManager");
+ Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
+ {
+ Title = "Error",
+ Content = "PropertyGroup element not found in .csproj",
+ CloseButtonText = "Ok"
+ };
+ _ = await messageBox.ShowDialogAsync();
return;
}
- // Open the project in the default IDE
+ App.Logger.LogInfo("Building plugin project", source: "PluginManager");
+ process = Process.Start("dotnet", $"build {csprojPath} -c Release");
+ await process.WaitForExitAsync();
+
+ changed = true;
+ await InitializePluginManager();
string? associatedProgram = FileUtilities.GetAssociatedProgram(".csproj");
if (associatedProgram is null)
{
+ App.Logger.LogInfo($"Opening plugin project in Explorer: {pluginProjectPath}", source: "PluginManager");
_ = Process.Start("explorer.exe", pluginProjectPath);
- return;
+ }
+ else
+ {
+ App.Logger.LogInfo($"Opening plugin project in IDE: {associatedProgram}", source: "PluginManager");
+ ProcessStartInfo psi = new ProcessStartInfo
+ {
+ FileName = associatedProgram,
+ Arguments = csprojPath,
+ };
+
+ _ = Process.Start(psi);
}
- ProcessStartInfo psi = new ProcessStartInfo
- {
- FileName = associatedProgram,
- Arguments = csprojPath,
- };
+ App.Logger.LogInfo($"Successfully created plugin: {pluginName}", source: "PluginManager");
- _ = Process.Start(psi);
+ pluginManagerDataContext.IsLoading = false;
}
private async void LogInButton_Click(object sender, RoutedEventArgs e)
{
if (pluginManagerDataContext.IsAuthenticated)
{
+ App.Logger.LogInfo("Logging out from mod.io", source: "PluginManager");
MainWindowDataContext.GetSettings().ModIoAccessToken = null;
pluginManagerDataContext.IsAuthenticated = false;
return;
}
+ App.Logger.LogInfo("Starting mod.io authentication", source: "PluginManager");
+
try
{
InputDialog inputDialog = new((string)FindResource("enterModIoEmail"), "Plugin Manager")
{
- Owner = this,
+ Owner = Window.GetWindow(this),
};
if (inputDialog.ShowDialog() != true)
{
+ App.Logger.LogInfo("Authentication cancelled by user", source: "PluginManager");
return;
}
+ App.Logger.LogInfo($"Requesting authentication code for email: {inputDialog.ResponseText}", source: "PluginManager");
await modIoClient.Auth.RequestCode(ModIoApiKey, inputDialog.ResponseText);
inputDialog = new((string)FindResource("enterModIoAccessToken"), "Plugin Manager")
{
- Owner = this,
+ Owner = Window.GetWindow(this),
};
if (inputDialog.ShowDialog() != true)
{
+ App.Logger.LogInfo("Authentication cancelled by user", source: "PluginManager");
return;
}
@@ -478,11 +654,13 @@ public class {pluginSafeName}Plugin : Plugin
if (accessToken.Value is not null)
{
modIoClient = new Client(new Credentials(ModIoApiKey, accessToken.Value));
+ App.Logger.LogInfo("Successfully authenticated with mod.io", source: "PluginManager");
}
MainWindowDataContext.GetSettings().ModIoAccessToken = accessToken.Value;
pluginManagerDataContext.IsAuthenticated = true;
+ App.Logger.LogInfo("Subscribing to installed plugins on mod.io", source: "PluginManager");
foreach (PluginEntryDataContext plugin in pluginManagerDataContext.InstalledPlugins)
{
if (!plugin.IsLocalPlugin)
@@ -490,10 +668,11 @@ public class {pluginSafeName}Plugin : Plugin
try
{
await modIoClient.Games[ModIoGameId].Mods.Subscribe(plugin.Id);
+ App.Logger.LogInfo($"Subscribed to plugin {plugin.Id}", source: "PluginManager");
}
catch (Exception ex)
{
- App.Logger.LogError(ex.Message, source: "PluginManager");
+ App.Logger.LogError($"Failed to subscribe to plugin {plugin.Id}: {ex.Message}", source: "PluginManager");
}
}
}
@@ -502,8 +681,14 @@ public class {pluginSafeName}Plugin : Plugin
}
catch (Exception ex)
{
- App.Logger.LogError(ex.Message, source: "PluginManager");
- _ = MessageBox.Show(ex.Message, "Plugin Manager", MessageBoxButton.OK, MessageBoxImage.Error);
+ App.Logger.LogError($"Authentication failed: {ex.Message}", source: "PluginManager");
+ Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
+ {
+ Title = "Plugin Manager",
+ Content = ex.Message,
+ CloseButtonText = "Ok"
+ };
+ _ = await messageBox.ShowDialogAsync();
}
pluginManagerDataContext.IsLoading = false;
@@ -516,33 +701,64 @@ public class {pluginSafeName}Plugin : Plugin
return;
}
- IReadOnlyList mods = await modIoClient.User.GetSubscriptions(ModFilter.GameId.Eq(ModIoGameId)).ToList();
+ App.Logger.LogInfo("Syncing plugins with mod.io subscriptions", source: "PluginManager");
- List pluginsToRemove = [];
-
- foreach (PluginEntryDataContext plugin in pluginManagerDataContext.InstalledPlugins)
+ try
{
- if (!plugin.IsLocalPlugin && !mods.Any(m => m.Id == plugin.Id))
- {
- pluginsToRemove.Add(plugin);
- }
- }
+ IReadOnlyList mods = await modIoClient.User.GetSubscriptions(ModFilter.GameId.Eq(ModIoGameId)).ToList();
+ App.Logger.LogInfo($"Found {mods.Count} subscribed plugins on mod.io", source: "PluginManager");
- foreach (PluginEntryDataContext plugin in pluginsToRemove)
- {
- if (plugin.Path is null)
+ List pluginsToRemove = [];
+
+ foreach (PluginEntryDataContext plugin in pluginManagerDataContext.InstalledPlugins)
{
- continue;
+ if (!plugin.IsLocalPlugin && !mods.Any(m => m.Id == plugin.Id))
+ {
+ App.Logger.LogInfo($"Plugin {plugin.Id} is no longer subscribed, marking for removal", source: "PluginManager");
+ pluginsToRemove.Add(plugin);
+ }
}
- await Remove(plugin.Path, plugin.Id);
+ foreach (PluginEntryDataContext plugin in pluginsToRemove)
+ {
+ if (plugin.Path is null)
+ {
+ continue;
+ }
+
+ await Remove(plugin.Path, plugin.Id);
+ }
+
+ IEnumerable notInstalledMods = mods.Where(m => !pluginManagerDataContext.InstalledPlugins.Any(p => p.Id == m.Id));
+
+ foreach (Mod mod in notInstalledMods)
+ {
+ App.Logger.LogInfo($"Installing subscribed plugin: {mod.Name} (ID: {mod.Id})", source: "PluginManager");
+ await Install(mod);
+ }
+
+ App.Logger.LogInfo("Plugin sync completed", source: "PluginManager");
}
-
- IEnumerable notInstalledMods = mods.Where(m => !pluginManagerDataContext.InstalledPlugins.Any(p => p.Id == m.Id));
-
- foreach (Mod mod in notInstalledMods)
+ catch (Exception ex)
{
- await Install(mod);
+ App.Logger.LogError($"Plugin sync failed: {ex.Message}", source: "PluginManager");
}
}
+
+ private static string GetPluginSafeName(string pluginName)
+ {
+ string pluginSafeName = pluginName.ToLower().Replace("_", " ");
+
+ TextInfo info = CultureInfo.CurrentCulture.TextInfo;
+ pluginSafeName = info.ToTitleCase(pluginSafeName);
+ pluginSafeName = IdentifierNameRegex().Replace(pluginSafeName, "");
+
+ return pluginSafeName;
+ }
+
+ private string GetCsprojPath(string pluginName)
+ {
+ string pluginSafeName = GetPluginSafeName(pluginName);
+ return Path.Combine(pluginDevelopmentPath, pluginSafeName, $"{pluginSafeName}.csproj");
+ }
}
\ No newline at end of file
diff --git a/src/DesktopMagic/Plugins/PluginMetadata.cs b/src/DesktopMagic/Plugins/PluginMetadata.cs
index 00d64c4..9bd42da 100644
--- a/src/DesktopMagic/Plugins/PluginMetadata.cs
+++ b/src/DesktopMagic/Plugins/PluginMetadata.cs
@@ -1,6 +1,8 @@
using Modio.Models;
using System;
+using System.Collections.Generic;
+using System.Linq;
using System.Text.Json.Serialization;
namespace DesktopMagic.Plugins;
@@ -27,6 +29,14 @@ public class PluginMetadata
public string? Version { get; set; }
+ public List Tags { get; set; } = [];
+
+ [JsonIgnore]
+ public bool SupportsUnloading => !Tags.Contains("Does Not Support Unloading");
+
+ [JsonIgnore]
+ public bool IsLocalPlugin => string.IsNullOrWhiteSpace(ProfileUri?.ToString());
+
public PluginMetadata(Mod mod)
{
Name = mod.Name ?? mod.Id.ToString();
@@ -39,6 +49,7 @@ public class PluginMetadata
Description = mod.DescriptionPlaintext;
Summary = mod.Summary;
Version = mod.Modfile?.Version;
+ Tags = mod.Tags.Select(t => t.Name).Where(t => t is not null).ToList()!;
}
public PluginMetadata(string name, uint id)
diff --git a/src/DesktopMagic/Plugins/PluginWindow.xaml b/src/DesktopMagic/Plugins/PluginWindow.xaml
index 79b7dda..9d0533c 100644
--- a/src/DesktopMagic/Plugins/PluginWindow.xaml
+++ b/src/DesktopMagic/Plugins/PluginWindow.xaml
@@ -1,6 +1,7 @@
-
-
+
+
+
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/DesktopMagic/Plugins/PluginWindow.xaml.cs b/src/DesktopMagic/Plugins/PluginWindow.xaml.cs
index 097ee17..f45913b 100644
--- a/src/DesktopMagic/Plugins/PluginWindow.xaml.cs
+++ b/src/DesktopMagic/Plugins/PluginWindow.xaml.cs
@@ -13,7 +13,10 @@ using System.Drawing.Imaging;
using System.IO;
using System.Linq;
using System.Reflection;
+using System.Runtime.Loader;
+using System.Text.Json;
using System.Threading;
+using System.Threading.Tasks;
using System.Timers;
using System.Windows;
using System.Windows.Interop;
@@ -22,7 +25,7 @@ using System.Windows.Media.Imaging;
namespace DesktopMagic;
-public partial class PluginWindow : Window
+public partial class PluginWindow : Window, IPluginWindow
{
public event Action? PluginLoaded;
@@ -32,6 +35,12 @@ public partial class PluginWindow : Window
private Thread? pluginThread;
private System.Timers.Timer? updateTimer;
private Plugin? pluginClassInstance;
+ private AssemblyLoadContext assemblyLoadContext;
+
+ private CancellationTokenSource? pluginCancellationTokenSource;
+ private FileSystemWatcher? pluginFileWatcher;
+ private System.Timers.Timer? reloadDebounceTimer;
+ private bool isReloading = false;
public bool IsRunning { get; private set; } = true;
public PluginMetadata PluginMetadata { get; private set; }
@@ -69,6 +78,11 @@ public partial class PluginWindow : Window
}
};
+ settings.Theme.PropertyChanged += (se, ev) =>
+ {
+ ThemeChanged();
+ };
+
PluginMetadata = pluginMetadata;
this.settings = settings;
@@ -78,6 +92,14 @@ public partial class PluginWindow : Window
Height = settings.Size.Y;
PluginFolderPath = pluginFolderPath;
+
+ assemblyLoadContext = CreateAssemblyLoadContext();
+
+ // Initialize hot reload watcher if plugin supports unloading and is external
+ if (pluginMetadata.SupportsUnloading && !string.IsNullOrEmpty(pluginFolderPath))
+ {
+ InitializeHotReload();
+ }
}
public PluginWindow(Plugin pluginClassInstance, PluginMetadata pluginMetadata, PluginSettings settings) : this(pluginMetadata, settings, string.Empty)
@@ -85,12 +107,190 @@ public partial class PluginWindow : Window
this.pluginClassInstance = pluginClassInstance;
}
+ private AssemblyLoadContext CreateAssemblyLoadContext()
+ {
+ AssemblyLoadContext context = new(PluginMetadata.Name, isCollectible: true);
+ context.Resolving += (ctx, assemblyName) =>
+ {
+ string assemblyPath = Path.Combine(PluginFolderPath, assemblyName.Name + ".dll");
+ if (File.Exists(assemblyPath))
+ {
+ return ctx.LoadFromAssemblyPath(assemblyPath);
+ }
+ else
+ {
+ _ = ctx.LoadFromAssemblyName(assemblyName);
+ }
+ return null;
+ };
+ return context;
+ }
+
+ private void InitializeHotReload()
+ {
+ try
+ {
+ pluginFileWatcher = new FileSystemWatcher(PluginFolderPath)
+ {
+ Filter = "main.dll",
+ NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Size,
+ EnableRaisingEvents = false
+ };
+
+ pluginFileWatcher.Changed += OnPluginFileChanged;
+
+ reloadDebounceTimer = new System.Timers.Timer(500); // 500ms debounce
+ reloadDebounceTimer.Elapsed += async (s, e) =>
+ {
+ reloadDebounceTimer!.Stop();
+ await ReloadPlugin();
+ };
+
+ App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Hot reload enabled", source: "Plugin");
+ }
+ catch (Exception ex)
+ {
+ App.Logger.LogWarn($"\"{PluginMetadata.Name}\" - Failed to initialize hot reload: {ex.Message}", source: "Plugin");
+ }
+ }
+
+ private void OnPluginFileChanged(object sender, FileSystemEventArgs e)
+ {
+ if (isReloading)
+ {
+ return;
+ }
+
+ App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Plugin file changed, scheduling reload", source: "Plugin");
+
+ // Debounce multiple file change events
+ reloadDebounceTimer?.Stop();
+ reloadDebounceTimer?.Start();
+ }
+
+ private async Task StopPlugin(bool unloadAssembly = true)
+ {
+ App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Stopping plugin", source: "Plugin");
+
+ try
+ {
+ pluginCancellationTokenSource?.Cancel();
+
+ if (pluginClassInstance is AsyncPlugin asyncPluginStop)
+ {
+ try
+ {
+ await asyncPluginStop.StopAsync(pluginCancellationTokenSource?.Token ?? CancellationToken.None);
+ }
+ catch (Exception ex)
+ {
+ App.Logger.LogError($"\"{PluginMetadata.Name}\" - Error stopping async plugin: {ex.Message}", source: "Plugin");
+ }
+ }
+
+ if (pluginClassInstance is not null)
+ {
+ SaveState(pluginClassInstance);
+ pluginClassInstance.Stop();
+ }
+ }
+ catch (Exception ex)
+ {
+ App.Logger.LogError($"\"{PluginMetadata.Name}\" - Error stopping plugin: {ex}", source: "Plugin");
+ }
+
+ if (unloadAssembly)
+ {
+ assemblyLoadContext.Unload();
+ }
+
+ pluginCancellationTokenSource?.Dispose();
+ pluginCancellationTokenSource = null;
+ }
+
+ private async Task ReloadPlugin()
+ {
+ if (isReloading || !IsRunning)
+ {
+ return;
+ }
+
+ isReloading = true;
+
+ try
+ {
+ App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Reloading plugin", source: "Plugin");
+
+ // Stop the file watcher during reload
+ if (pluginFileWatcher != null)
+ {
+ pluginFileWatcher.EnableRaisingEvents = false;
+ }
+
+ updateTimer?.Stop();
+
+ await StopPlugin(unloadAssembly: true);
+
+ // Wait for file to be fully written and released
+ await Task.Delay(200);
+
+ // Clear the current instance
+ pluginClassInstance = null;
+
+ // Create a new AssemblyLoadContext since the old one was unloaded
+ assemblyLoadContext = CreateAssemblyLoadContext();
+
+ // Show busy indicator
+ await Dispatcher.InvokeAsync(() => busyMask.IsBusy = true);
+
+ // Reload the plugin
+ await ExecuteSource();
+
+ // Hide busy indicator
+ await Dispatcher.InvokeAsync(() => busyMask.IsBusy = false);
+
+ App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Plugin reloaded successfully", source: "Plugin");
+ }
+ catch (Exception ex)
+ {
+ App.Logger.LogError($"\"{PluginMetadata.Name}\" - Failed to reload plugin: {ex}", source: "Plugin");
+
+ await Dispatcher.InvokeAsync(async () =>
+ {
+ Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
+ {
+ Title = $"Reload Error \"{PluginMetadata.Name}\"",
+ Content = $"Failed to reload plugin:\n{ex.Message}",
+ CloseButtonText = "Ok"
+ };
+ _ = await messageBox.ShowDialogAsync();
+ });
+ }
+ finally
+ {
+ isReloading = false;
+
+ // Re-enable file watcher
+ if (pluginFileWatcher != null && IsRunning)
+ {
+ pluginFileWatcher.EnableRaisingEvents = true;
+ }
+ }
+ }
+
public void UpdatePluginWindow()
{
- Dispatcher.Invoke(ThemeChanged);
UpdateTimer_Elapsed(updateTimer, null);
}
+ public void SavePluginState()
+ {
+ if (pluginClassInstance is not null)
+ {
+ SaveState(pluginClassInstance);
+ }
+ }
+
public void Exit()
{
IsRunning = false;
@@ -150,31 +350,35 @@ public partial class PluginWindow : Window
bitmapData.Scan0, bitmapData.Stride * bitmapData.Height, bitmapData.Stride);
bitmap.UnlockBits(bitmapData);
+
+ bitmapSource.Freeze();
return bitmapSource;
}
private void Window_ContentRendered(object? sender, EventArgs e)
{
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Starting plugin thread", source: "Plugin");
- pluginThread = new Thread(LoadPlugin);
+ pluginThread = new Thread(async () => await LoadPlugin());
pluginThread.Start();
}
- private void ThemeChanged()
- {
- viewBox.Margin = new Thickness(settings.Theme.Margin);
- border.Background = new SolidColorBrush(MultiColorConverter.ConvertToMediaColor(settings.Theme.BackgroundColor));
- border.CornerRadius = new CornerRadius(settings.Theme.CornerRadius);
- }
-
- private void LoadPlugin()
+ private async Task LoadPlugin()
{
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Loading plugin", source: "Plugin");
if (pluginClassInstance is null && !File.Exists($"{PluginFolderPath}\\main.dll"))
{
App.Logger.LogError($"\"{PluginMetadata.Name}\" - File \"main.dll\" does not exist", source: "Plugin");
- _ = MessageBox.Show("File \"main.dll\" does not exist!", $"Error \"{PluginMetadata.Name}\"", MessageBoxButton.OK, MessageBoxImage.Error);
+ _ = await Dispatcher.InvokeAsync(async () =>
+ {
+ Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
+ {
+ Title = $"Error \"{PluginMetadata.Name}\"",
+ Content = "File \"main.dll\" does not exist!",
+ CloseButtonText = "Ok"
+ };
+ _ = await messageBox.ShowDialogAsync();
+ });
Exit();
return;
@@ -182,31 +386,84 @@ public partial class PluginWindow : Window
try
{
- ExecuteSource();
+ await ExecuteSource();
}
catch (Exception ex)
{
App.Logger.LogError($"\"{PluginMetadata.Name}\" - {ex}", source: "Plugin");
- _ = MessageBox.Show("File execution error:\n" + ex, $"Error \"{PluginMetadata.Name}\"", MessageBoxButton.OK, MessageBoxImage.Error);
+ _ = await Dispatcher.InvokeAsync(async () =>
+ {
+ Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
+ {
+ Title = $"Error \"{PluginMetadata.Name}\"",
+ Content = "File execution error:\n" + ex,
+ CloseButtonText = "Ok"
+ };
+ _ = await messageBox.ShowDialogAsync();
+ });
Exit();
return;
}
PluginLoaded?.Invoke();
+
+ _ = await Dispatcher.InvokeAsync(() => busyMask.IsBusy = false);
+
+ // Enable hot reload after initial load
+ if (pluginFileWatcher != null)
+ {
+ pluginFileWatcher.EnableRaisingEvents = true;
+ App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Hot reload monitoring started", source: "Plugin");
+ }
}
- private void ExecuteSource()
+ private void ThemeChanged()
+ {
+ viewBox.Margin = new Thickness(settings.Theme.Margin);
+ border.Background = new SolidColorBrush(MultiColorConverter.ConvertToMediaColor(settings.Theme.BackgroundColor));
+ border.CornerRadius = new CornerRadius(settings.Theme.CornerRadius);
+
+ pluginClassInstance?.OnThemeChanged();
+
+ if (pluginClassInstance?.UpdateInterval is 0 or > 500)
+ {
+ pluginClassInstance.Application.UpdateWindow();
+ }
+ }
+
+ private async Task ExecuteSource()
{
object? instance = pluginClassInstance;
if (instance is null)
{
- Assembly dll = Assembly.LoadFrom($"{PluginFolderPath}\\main.dll");
+ Assembly dll;
+
+ if (PluginMetadata.SupportsUnloading)
+ {
+ byte[] assemblyData = await File.ReadAllBytesAsync($"{PluginFolderPath}\\main.dll");
+ using MemoryStream assemblyStream = new(assemblyData);
+
+ dll = assemblyLoadContext.LoadFromStream(assemblyStream);
+ }
+ else
+ {
+ dll = Assembly.LoadFrom($"{PluginFolderPath}\\main.dll");
+ }
+
Type? instanceType = Array.Find(dll.GetTypes(), type => type.GetTypeInfo().BaseType == typeof(Plugin));
if (instanceType is null)
{
App.Logger.LogError($"\"{PluginMetadata.Name}\" - The \"Plugin\" class could not be found! It has to inherit from \"{typeof(Plugin).FullName}\"", source: "Plugin");
- _ = MessageBox.Show($"The \"Plugin\" class could not be found! It has to inherit from \"{typeof(Plugin).FullName}\"", $"Error \"{PluginMetadata.Name}\"", MessageBoxButton.OK, MessageBoxImage.Error);
-
+ _ = await Dispatcher.InvokeAsync(async () =>
+ {
+ Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
+ {
+ Title = $"Error \"{PluginMetadata.Name}\"",
+ Content = $"The \"Plugin\" class could not be found! It has to inherit from \"{typeof(Plugin).FullName}\"",
+ CloseButtonText = "Ok"
+ };
+ _ = await messageBox.ShowDialogAsync();
+ });
Exit();
return;
}
@@ -216,17 +473,27 @@ public partial class PluginWindow : Window
if (instance is Plugin plugin)
{
pluginClassInstance = plugin;
- pluginClassInstance.Application = new Plugins.PluginData(this, settings);
+ pluginClassInstance.Application = new PluginData(this, settings);
}
else
{
App.Logger.LogError($"\"{PluginMetadata.Name}\" - The \"Plugin\" class could not be found! It has to inherit from \"{typeof(Plugin).FullName}\"", source: "Plugin");
- _ = MessageBox.Show($"The \"Plugin\" class has to inherit from \"{typeof(Plugin).FullName}\"", $"Error \"{PluginMetadata.Name}\"", MessageBoxButton.OK, MessageBoxImage.Error);
+ _ = await Dispatcher.InvokeAsync(async () =>
+ {
+ Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
+ {
+ Title = $"Error \"{PluginMetadata.Name}\"",
+ Content = $"The \"Plugin\" class has to inherit from \"{typeof(Plugin).FullName}\"",
+ CloseButtonText = "Ok"
+ };
+ _ = await messageBox.ShowDialogAsync();
+ });
Exit();
return;
}
- LoadOptions(pluginClassInstance);
+ LoadState(pluginClassInstance);
+ await LoadOptions(pluginClassInstance);
BindDefaultSettings(pluginClassInstance);
updateTimer = new System.Timers.Timer
@@ -236,7 +503,14 @@ public partial class PluginWindow : Window
updateTimer.Elapsed += UpdateTimer_Elapsed;
pluginClassInstance.Start();
+ if (pluginClassInstance is AsyncPlugin asyncPluginStart)
+ {
+ pluginCancellationTokenSource?.Dispose();
+ pluginCancellationTokenSource = new CancellationTokenSource();
+ await asyncPluginStart.StartAsync(pluginCancellationTokenSource.Token);
+ }
UpdatePluginWindow();
+ await Dispatcher.InvokeAsync(ThemeChanged);
if (pluginClassInstance.UpdateInterval > 0)
{
@@ -316,16 +590,13 @@ public partial class PluginWindow : Window
}
}
- private void LoadOptions(object instance)
+ private async Task LoadOptions(object instance)
{
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Loading plugin options", source: "Plugin");
try
{
-#pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields
FieldInfo[] props = instance.GetType().GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.GetField);
-#pragma warning restore S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields
-
List settingElements = [];
foreach (FieldInfo prop in props)
{
@@ -344,6 +615,16 @@ public partial class PluginWindow : Window
settingElement.JsonValue = settingsSettingElement.JsonValue;
}
+ element.OnValueChanged += () =>
+ {
+ pluginClassInstance?.OnSettingsChanged();
+
+ if (pluginClassInstance?.UpdateInterval is 0 or > 500)
+ {
+ pluginClassInstance.Application.UpdateWindow();
+ }
+ };
+
settingElements.Add(settingElement);
break;
}
@@ -357,18 +638,167 @@ public partial class PluginWindow : Window
{
IsRunning = false;
App.Logger.LogError($"\"{PluginMetadata.Name}\" - {ex}", source: "Plugin");
- _ = MessageBox.Show("File execution error:\n" + ex, $"Error \"{PluginMetadata.Name}\"", MessageBoxButton.OK, MessageBoxImage.Error);
+ _ = await Dispatcher.InvokeAsync(async () =>
+ {
+ Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
+ {
+ Title = $"Error \"{PluginMetadata.Name}\"",
+ Content = "File execution error:\n" + ex,
+ CloseButtonText = "Ok"
+ };
+ _ = await messageBox.ShowDialogAsync();
+ });
Exit();
}
}
- private void UpdateTimer_Elapsed(object? sender, ElapsedEventArgs? e)
+ private void LoadState(object instance)
+ {
+ App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Loading plugin state", source: "Plugin");
+
+ try
+ {
+ if (settings.State.Count == 0)
+ {
+ App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - No state found, using defaults", source: "Plugin");
+ return;
+ }
+
+ FieldInfo[] fields = instance.GetType().GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
+ PropertyInfo[] properties = instance.GetType().GetProperties(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
+
+ // Load fields
+ foreach (FieldInfo field in fields)
+ {
+ PersistStateAttribute? persistAttribute = field.GetCustomAttribute();
+ if (persistAttribute is null)
+ {
+ continue;
+ }
+
+ if (settings.State.TryGetValue(field.Name, out JsonElement jsonElement))
+ {
+ try
+ {
+ object? value = jsonElement.Deserialize(field.FieldType);
+ field.SetValue(instance, value);
+ App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Restored state field: {field.Name}", source: "Plugin");
+ }
+ catch (Exception ex)
+ {
+ App.Logger.LogWarn($"\"{PluginMetadata.Name}\" - Failed to restore field {field.Name}: {ex.Message}", source: "Plugin");
+ }
+ }
+ }
+
+ // Load properties
+ foreach (PropertyInfo property in properties)
+ {
+ PersistStateAttribute? persistAttribute = property.GetCustomAttribute();
+ if (persistAttribute is null || !property.CanWrite)
+ {
+ continue;
+ }
+
+ if (settings.State.TryGetValue(property.Name, out JsonElement jsonElement))
+ {
+ try
+ {
+ object? value = jsonElement.Deserialize(property.PropertyType);
+ property.SetValue(instance, value);
+ App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Restored state property: {property.Name}", source: "Plugin");
+ }
+ catch (Exception ex)
+ {
+ App.Logger.LogWarn($"\"{PluginMetadata.Name}\" - Failed to restore property {property.Name}: {ex.Message}", source: "Plugin");
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ App.Logger.LogError($"\"{PluginMetadata.Name}\" - Error loading state: {ex.Message}", source: "Plugin");
+ }
+ }
+
+ private void SaveState(object instance)
+ {
+ App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Saving plugin state", source: "Plugin");
+
+ try
+ {
+ FieldInfo[] fields = instance.GetType().GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
+ PropertyInfo[] properties = instance.GetType().GetProperties(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
+
+ // Save fields
+ foreach (FieldInfo field in fields)
+ {
+ PersistStateAttribute? persistAttribute = field.GetCustomAttribute();
+ if (persistAttribute is null)
+ {
+ continue;
+ }
+
+ try
+ {
+ object? value = field.GetValue(instance);
+ JsonElement jsonElement = JsonSerializer.SerializeToElement(value, field.FieldType);
+ settings.State[field.Name] = jsonElement;
+ App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Saved state field: {field.Name}", source: "Plugin");
+ }
+ catch (Exception ex)
+ {
+ App.Logger.LogWarn($"\"{PluginMetadata.Name}\" - Failed to save field {field.Name}: {ex.Message}", source: "Plugin");
+ }
+ }
+
+ // Save properties
+ foreach (PropertyInfo property in properties)
+ {
+ PersistStateAttribute? persistAttribute = property.GetCustomAttribute();
+ if (persistAttribute is null || !property.CanRead)
+ {
+ continue;
+ }
+
+ try
+ {
+ object? value = property.GetValue(instance);
+ JsonElement jsonElement = JsonSerializer.SerializeToElement(value, property.PropertyType);
+ settings.State[property.Name] = jsonElement;
+ App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Saved state property: {property.Name}", source: "Plugin");
+ }
+ catch (Exception ex)
+ {
+ App.Logger.LogWarn($"\"{PluginMetadata.Name}\" - Failed to save property {property.Name}: {ex.Message}", source: "Plugin");
+ }
+ }
+
+ App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - State saved successfully ({settings.State.Count} items)", source: "Plugin");
+ }
+ catch (Exception ex)
+ {
+ App.Logger.LogError($"\"{PluginMetadata.Name}\" - Error saving state: {ex.Message}", source: "Plugin");
+ }
+ }
+
+ private async void UpdateTimer_Elapsed(object? sender, ElapsedEventArgs? e)
{
try
{
if (IsRunning && pluginClassInstance is not null)
{
- Bitmap? result = pluginClassInstance.Main();
+ Bitmap? result;
+
+ if (pluginClassInstance is AsyncPlugin asyncPlugin)
+ {
+ CancellationToken token = pluginCancellationTokenSource?.Token ?? CancellationToken.None;
+ result = await asyncPlugin.MainAsync(token);
+ }
+ else
+ {
+ result = pluginClassInstance.Main();
+ }
if (pluginClassInstance.UpdateInterval > 0)
{
@@ -389,11 +819,13 @@ public partial class PluginWindow : Window
_ => BitmapScalingMode.Unspecified
};
- //Update Image
- Dispatcher.Invoke(() =>
+ // Update Image
+ BitmapSource frozenSource = BitmapToImageSource(result);
+
+ _ = Dispatcher.BeginInvoke(() =>
{
RenderOptions.SetBitmapScalingMode(image, renderOptions);
- image.Source = BitmapToImageSource(result);
+ image.Source = frozenSource;
});
}
}
@@ -402,7 +834,16 @@ public partial class PluginWindow : Window
{
IsRunning = false;
App.Logger.LogError($"\"{PluginMetadata.Name}\" - {ex}", source: "Plugin");
- _ = MessageBox.Show("File execution error:\n" + ex, $"Error \"{PluginMetadata.Name}\"", MessageBoxButton.OK, MessageBoxImage.Error);
+ _ = await Dispatcher.InvokeAsync(async () =>
+ {
+ Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
+ {
+ Title = $"Error \"{PluginMetadata.Name}\"",
+ Content = "File execution error:\n" + ex,
+ CloseButtonText = "Ok"
+ };
+ _ = await messageBox.ShowDialogAsync();
+ });
Exit();
return;
}
@@ -426,9 +867,23 @@ public partial class PluginWindow : Window
private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
- App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Stopping plugin", source: "Plugin");
IsRunning = false;
- pluginClassInstance?.Stop();
+
+ // Cleanup hot reload resources
+ if (pluginFileWatcher != null)
+ {
+ pluginFileWatcher.EnableRaisingEvents = false;
+ pluginFileWatcher.Changed -= OnPluginFileChanged;
+ pluginFileWatcher.Dispose();
+ pluginFileWatcher = null;
+ }
+
+ reloadDebounceTimer?.Stop();
+ reloadDebounceTimer?.Dispose();
+ reloadDebounceTimer = null;
+
+ // Stop plugin using the shared method (no need to await in synchronous event handler)
+ _ = StopPlugin(unloadAssembly: true);
}
#region Window Events
diff --git a/src/DesktopMagic/Plugins/WebPluginWindow.xaml b/src/DesktopMagic/Plugins/WebPluginWindow.xaml
new file mode 100644
index 0000000..deb7d11
--- /dev/null
+++ b/src/DesktopMagic/Plugins/WebPluginWindow.xaml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/DesktopMagic/Plugins/WebPluginWindow.xaml.cs b/src/DesktopMagic/Plugins/WebPluginWindow.xaml.cs
new file mode 100644
index 0000000..ff92680
--- /dev/null
+++ b/src/DesktopMagic/Plugins/WebPluginWindow.xaml.cs
@@ -0,0 +1,256 @@
+using DesktopMagic.Helpers;
+using DesktopMagic.Plugins;
+using DesktopMagic.Settings;
+
+using Microsoft.Web.WebView2.Core;
+
+using System;
+using System.IO;
+using System.Windows;
+using System.Windows.Interop;
+using System.Windows.Media;
+
+namespace DesktopMagic;
+
+public partial class WebPluginWindow : Window, IPluginWindow
+{
+ public event Action? PluginLoaded;
+
+ public event Action? OnExit;
+
+ private readonly PluginSettings settings;
+ private bool isInitialized = false;
+
+ public bool IsRunning { get; private set; } = true;
+ public PluginMetadata PluginMetadata { get; private set; }
+ public string PluginFolderPath { get; private set; }
+
+ public WebPluginWindow(PluginMetadata pluginMetadata, PluginSettings settings, string pluginFolderPath)
+ {
+ InitializeComponent();
+
+ Window w = new()
+ {
+ Top = -100,
+ Left = -100,
+ Width = 0,
+ Height = 0,
+
+ WindowStyle = WindowStyle.ToolWindow,
+ ShowInTaskbar = false
+ };
+
+ WindowInteropHelper helper = new WindowInteropHelper(w);
+ _ = helper.EnsureHandle();
+
+ Owner = w;
+
+ settings.PropertyChanged += (e, s) =>
+ {
+ if (s.PropertyName == nameof(PluginSettings.CurrentThemeName))
+ {
+ settings.Theme.PropertyChanged += (se, ev) =>
+ {
+ ThemeChanged();
+ };
+ ThemeChanged();
+ }
+ };
+
+ settings.Theme.PropertyChanged += (se, ev) =>
+ {
+ ThemeChanged();
+ };
+
+ PluginMetadata = pluginMetadata;
+ this.settings = settings;
+
+ Left = settings.Position.X;
+ Top = settings.Position.Y;
+ Width = settings.Size.X;
+ Height = settings.Size.Y;
+
+ PluginFolderPath = pluginFolderPath;
+ }
+
+ public void Exit()
+ {
+ IsRunning = false;
+
+ Dispatcher.Invoke(() =>
+ {
+ OnExit?.Invoke();
+ });
+ }
+
+ public void SetEditMode(bool enabled)
+ {
+ if (enabled)
+ {
+ Topmost = true;
+ panel.Visibility = Visibility.Visible;
+ _ = W32.EnableWindow(webView.Handle, false);
+ WindowPos.SetIsLocked(this, false);
+ tileBar.CaptionHeight = tileBar.CaptionHeight = ActualHeight - 10 < 0 ? 0 : ActualHeight - 10;
+ ResizeMode = ResizeMode.CanResize;
+ }
+ else
+ {
+ Topmost = false;
+ panel.Visibility = Visibility.Collapsed;
+ _ = W32.EnableWindow(webView.Handle, true);
+ WindowPos.SendWpfWindowBack(this);
+ WindowPos.SendWpfWindowBack(this);
+ WindowPos.SetIsLocked(this, true);
+ tileBar.CaptionHeight = 0;
+ ResizeMode = ResizeMode.NoResize;
+ }
+ }
+
+ protected override void OnSourceInitialized(EventArgs e)
+ {
+ base.OnSourceInitialized(e);
+
+ WindowInteropHelper helper = new(this);
+ _ = WindowPos.SetWindowLong(helper.Handle, WindowPos.GWL_EXSTYLE,
+ WindowPos.GetWindowLong(helper.Handle, WindowPos.GWL_EXSTYLE) | WindowPos.WS_EX_NOACTIVATE);
+ }
+
+ private async void Window_ContentRendered(object? sender, EventArgs e)
+ {
+ App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Starting web plugin", source: "WebPlugin");
+
+ try
+ {
+ await InitializeWebView();
+ }
+ catch (Exception ex)
+ {
+ App.Logger.LogError($"\"{PluginMetadata.Name}\" - {ex}", source: "WebPlugin");
+ Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
+ {
+ Title = $"Error \"{PluginMetadata.Name}\"",
+ Content = "WebView2 initialization error:\n" + ex,
+ CloseButtonText = "Ok"
+ };
+ _ = await messageBox.ShowDialogAsync();
+ Exit();
+ }
+ }
+
+ private void ThemeChanged()
+ {
+ webView.Margin = new Thickness(settings.Theme.Margin);
+ border.Background = new SolidColorBrush(MultiColorConverter.ConvertToMediaColor(settings.Theme.BackgroundColor));
+ border.CornerRadius = new CornerRadius(settings.Theme.CornerRadius);
+
+ string cssVariables = $@"
+ :root {{
+ --background-color: {MultiColorConverter.ConvertToHexRgba(settings.Theme.BackgroundColor)};
+ --primary-color: {MultiColorConverter.ConvertToHexRgba(settings.Theme.PrimaryColor)};
+ --secondary-color: {MultiColorConverter.ConvertToHexRgba(settings.Theme.SecondaryColor)};
+ --font-family: {settings.Theme.Font};
+
+ font-family: {settings.Theme.Font};
+ color: {MultiColorConverter.ConvertToHexRgba(settings.Theme.PrimaryColor)};
+ }}
+ ";
+
+ string script = $@"
+ (function() {{
+ let style = document.getElementById('desktop-magic-theme');
+ if (!style) {{
+ style = document.createElement('style');
+ style.id = 'desktop-magic-theme';
+ document.head.appendChild(style);
+ }}
+ style.textContent = `{cssVariables}`;
+ }})();
+ ";
+
+ _ = webView.ExecuteScriptAsync(script);
+ }
+
+ private async System.Threading.Tasks.Task InitializeWebView()
+ {
+ App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Initializing WebView2", source: "WebPlugin");
+
+ string htmlPath = Path.Combine(PluginFolderPath, "main.html");
+
+ if (!File.Exists(htmlPath))
+ {
+ App.Logger.LogError($"\"{PluginMetadata.Name}\" - File \"main.html\" does not exist", source: "WebPlugin");
+ Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
+ {
+ Title = $"Error \"{PluginMetadata.Name}\"",
+ Content = "File \"main.html\" does not exist!",
+ CloseButtonText = "Ok"
+ };
+ _ = await messageBox.ShowDialogAsync();
+ Exit();
+ return;
+ }
+
+ try
+ {
+ string userDataFolder = Path.Combine(Path.GetTempPath(), "DesktopMagic", "WebView2", PluginMetadata.Id.ToString());
+ CoreWebView2Environment environment = await CoreWebView2Environment.CreateAsync(null, userDataFolder);
+ await webView.EnsureCoreWebView2Async(environment);
+
+ webView.CoreWebView2.Settings.AreDefaultContextMenusEnabled = false;
+ webView.CoreWebView2.Settings.AreDevToolsEnabled = false;
+ webView.CoreWebView2.Settings.IsStatusBarEnabled = false;
+ webView.CoreWebView2.Settings.AreDefaultScriptDialogsEnabled = true;
+
+ string htmlUri = new Uri(htmlPath).AbsoluteUri;
+ webView.Source = new Uri(htmlUri);
+
+ isInitialized = true;
+
+ App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - WebView2 initialized successfully", source: "WebPlugin");
+ PluginLoaded?.Invoke();
+
+ busyMask.IsBusy = false;
+ }
+ catch (Exception ex)
+ {
+ App.Logger.LogError($"\"{PluginMetadata.Name}\" - WebView2 initialization failed: {ex}", source: "WebPlugin");
+ throw;
+ }
+ }
+
+ private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
+ {
+ App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Stopping web plugin", source: "WebPlugin");
+ IsRunning = false;
+
+ try
+ {
+ if (isInitialized && webView.CoreWebView2 != null)
+ {
+ webView.Dispose();
+ }
+ }
+ catch (Exception ex)
+ {
+ App.Logger.LogError($"\"{PluginMetadata.Name}\" - {ex}", source: "WebPlugin");
+ }
+ }
+
+ private void Window_LocationChanged(object sender, EventArgs e)
+ {
+ settings.Position = new System.Windows.Point(Left, Top);
+ }
+
+ private void Window_SizeChanged(object sender, SizeChangedEventArgs e)
+ {
+ settings.Size = new System.Windows.Point(Width, Height);
+
+ tileBar.CaptionHeight = ActualHeight - 10;
+ }
+
+ private void WebView_CoreWebView2InitializationCompleted(object sender, CoreWebView2InitializationCompletedEventArgs e)
+ {
+ webView.CoreWebView2.DOMContentLoaded += (_, _) => ThemeChanged();
+ }
+}
diff --git a/src/DesktopMagic/Resources/Images/ImageResources.xaml b/src/DesktopMagic/Resources/Images/ImageResources.xaml
index a9578d0..7dc4db2 100644
--- a/src/DesktopMagic/Resources/Images/ImageResources.xaml
+++ b/src/DesktopMagic/Resources/Images/ImageResources.xaml
@@ -3,4 +3,5 @@
+
\ No newline at end of file
diff --git a/src/DesktopMagic/Resources/Images/modio-logo-bluelight.png b/src/DesktopMagic/Resources/Images/modio-logo-bluelight.png
new file mode 100644
index 0000000..fb2e571
Binary files /dev/null and b/src/DesktopMagic/Resources/Images/modio-logo-bluelight.png differ
diff --git a/src/DesktopMagic/Resources/Strings/StringResources.de.xaml b/src/DesktopMagic/Resources/Strings/StringResources.de.xaml
index 910d844..9218420 100644
--- a/src/DesktopMagic/Resources/Strings/StringResources.de.xaml
+++ b/src/DesktopMagic/Resources/Strings/StringResources.de.xaml
@@ -3,6 +3,9 @@
xmlns:col="clr-namespace:System.Collections;assembly=mscorlib"
xmlns:system="clr-namespace:System;assembly=mscorlib">
+ Entwickelt von Stone_Red
+ Bug Melden
+ Feature Anfragen
Linie
Spiegeln
Ordner
@@ -15,13 +18,15 @@
Mod.io Account E-Mail eingeben
Geben Sie Ihren mod.io Sicherheitscode ein
Bearbeitungsmodus umschalten
- Alle Plugins
- Installierte Plugins
- Neu Laden
+ Alle Plugins Suchen...
+ Installierte Plugins Suchen...
+ Plugins Neu Laden
Plugins Ordner
Plugins Manager
Neues Plugin erstellen
Pluginnamen eingeben
+ Plugins
+ Aktivieren Sie das Plugin, um seine Einstellungen zu konfigurieren.
Farbe:
Standard
Wollen sie das Programm wirklich schließen?
@@ -31,14 +36,18 @@
Datum
CPU Auslastung
Musik Visualisierer
+ Wetter
Signalverstärkung:
Neues Layout
Layout Löschen
Neues Theme
Theme Löschen
- Theme Ändern
+ Theme Bearbeiten
+ Themes
Ok
Abbrechen
+ Möchten Sie dieses Layout wirklich löschen?
+ Möchten Sie dieses Theme wirklich löschen?
Layoutnamen eingeben:
Layout existiert bereits!
Themenamen eingeben:
diff --git a/src/DesktopMagic/Resources/Strings/StringResources.en.xaml b/src/DesktopMagic/Resources/Strings/StringResources.en.xaml
index d19026b..c524080 100644
--- a/src/DesktopMagic/Resources/Strings/StringResources.en.xaml
+++ b/src/DesktopMagic/Resources/Strings/StringResources.en.xaml
@@ -3,6 +3,9 @@
xmlns:col="clr-namespace:System.Collections;assembly=mscorlib"
xmlns:system="clr-namespace:System;assembly=mscorlib">
+ Developed by Stone_Red
+ Report Bug
+ Request Feature
Line
Mirror
Folder
@@ -15,13 +18,15 @@
Enter your mod.io account E-mail
Enter your mod.io security code
Toggle Edit Mode
- All Plugins
- Installed Plugins
- Reload
+ Search All Plugins...
+ Search Installed Plugins...
+ Reload Plugins
Plugins Folder
Plugins Manager
Create New Plugin
Enter plugin name
+ Plugins
+ Enable the plugin to configure its settings.
Color:
Default
Do you really want to close the program?
@@ -31,14 +36,18 @@
Date
CPU Usage
Music Visualizer
+ Weather
Signal Amplification:
New Layout
Delete Layout
New Theme
Delete Theme
- Change Theme
+ Edit Theme
+ Themes
Ok
Cancel
+ Do you really want to delete this layout?
+ Do you really want to delete this theme?
Enter layout name
Layout already exists!
Can't delete last layout!
diff --git a/src/DesktopMagic/Resources/Styles/ToggleSwitchContentLeftStyle.xaml b/src/DesktopMagic/Resources/Styles/ToggleSwitchContentLeftStyle.xaml
new file mode 100644
index 0000000..3420ca0
--- /dev/null
+++ b/src/DesktopMagic/Resources/Styles/ToggleSwitchContentLeftStyle.xaml
@@ -0,0 +1,131 @@
+
+
+ 40
+ 20
+ 1
+ 0,0,8,0
+
+
+
\ No newline at end of file
diff --git a/src/DesktopMagic/Settings/DesktopMagicSettings.cs b/src/DesktopMagic/Settings/DesktopMagicSettings.cs
index 62409e8..9cbb10d 100644
--- a/src/DesktopMagic/Settings/DesktopMagicSettings.cs
+++ b/src/DesktopMagic/Settings/DesktopMagicSettings.cs
@@ -8,7 +8,7 @@ using System.Text.Json.Serialization;
namespace DesktopMagic.Settings;
-internal class DesktopMagicSettings : INotifyPropertyChanged
+public class DesktopMagicSettings : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
@@ -44,7 +44,16 @@ internal class DesktopMagicSettings : INotifyPropertyChanged
public string? CurrentLayoutName
{
- get => currentLayoutName ?? Layouts.FirstOrDefault()?.Name;
+ get
+ {
+ if (!Layouts.Any(l => l.Name == currentLayoutName))
+ {
+ currentLayoutName = null;
+ }
+
+ return currentLayoutName ?? Layouts.FirstOrDefault()?.Name;
+ }
+
set
{
currentLayoutName = value;
diff --git a/src/DesktopMagic/Settings/Layout.cs b/src/DesktopMagic/Settings/Layout.cs
index 420f746..a92dff7 100644
--- a/src/DesktopMagic/Settings/Layout.cs
+++ b/src/DesktopMagic/Settings/Layout.cs
@@ -9,7 +9,7 @@ using System.Text.Json.Serialization;
namespace DesktopMagic.Settings;
-internal class Layout(string name) : INotifyPropertyChanged
+public class Layout(string name) : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
diff --git a/src/DesktopMagic/Settings/PluginSettings.cs b/src/DesktopMagic/Settings/PluginSettings.cs
index 8261f7b..5655032 100644
--- a/src/DesktopMagic/Settings/PluginSettings.cs
+++ b/src/DesktopMagic/Settings/PluginSettings.cs
@@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
+using System.Text.Json;
using System.Text.Json.Serialization;
using System.Windows;
@@ -16,13 +17,14 @@ public class PluginSettings : INotifyPropertyChanged
private string? currentThemeName;
private List settings = [];
+ private Dictionary state = [];
private bool enabled = false;
private Point position = new Point(100, 100);
private Point size = new Point(300, 300);
// Only for internal use to show the name of the plugin in the main window
[JsonIgnore]
- public string Name { get; set; } = string.Empty;
+ public PluginMetadata Metadata { get; set; } = new();
[JsonIgnore]
public Theme Theme
@@ -61,6 +63,23 @@ public class PluginSettings : INotifyPropertyChanged
}
}
+ ///
+ /// Dictionary storing plugin state. Key is the field/property name, value is the JsonElement.
+ /// This is persisted per layout in settings.json.
+ ///
+ public Dictionary State
+ {
+ get => state;
+ set
+ {
+ if (state != value)
+ {
+ state = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
public bool Enabled
{
get => enabled;
diff --git a/src/DesktopMagicPluginAPI/AsyncPlugin.cs b/src/DesktopMagicPluginAPI/AsyncPlugin.cs
new file mode 100644
index 0000000..70db345
--- /dev/null
+++ b/src/DesktopMagicPluginAPI/AsyncPlugin.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Drawing;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace DesktopMagic.Api;
+
+///
+/// Provides an abstract base class for creating asynchronous plugins that can be integrated into the application, supporting periodic updates,
+/// rendering, and user interaction.
+///
+/// Derive from this class to implement custom plugin functionality. The class defines lifecycle methods
+/// such as , , and for activation, deactivation, and periodic
+/// execution. It also provides event handlers for mouse and theme interactions, as well as access to application data
+/// and rendering configuration. Implementations should override relevant methods to respond to user input, update
+/// intervals, and configuration changes as needed.
+public abstract class AsyncPlugin : Plugin
+{
+ ///
+ /// Occurs once when the plugin gets activated. Override for async initialization.
+ ///
+ /// Token signaled when the host requests cancellation.
+ public virtual Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+
+ ///
+ /// Occurs once when the plugin gets deactivated. Override for async cleanup.
+ ///
+ /// Token signaled when the host requests cancellation.
+ public virtual Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+
+ ///
+ /// Occurs when the elapses.
+ ///
+ ///
+ public abstract Task MainAsync(CancellationToken cancellationToken);
+
+ ///
+ /// This method should not be called directly! Override and use for asynchronous plugin operations.
+ ///
+ /// This method only exists to fulfill base class requirements.
+ /// Calling it will always result in an .
+ /// Thrown if this method is called directly. Use MainAsync for plugin execution instead.
+ public sealed override Bitmap? Main()
+ {
+ throw new InvalidOperationException($"AsyncPlugin.Main() should not be called directly. Override {nameof(MainAsync)} instead.");
+ }
+}
\ No newline at end of file
diff --git a/src/DesktopMagicPluginAPI/DesktopMagic.Api.csproj b/src/DesktopMagicPluginAPI/DesktopMagic.Api.csproj
index f19692b..1cd85a0 100644
--- a/src/DesktopMagicPluginAPI/DesktopMagic.Api.csproj
+++ b/src/DesktopMagicPluginAPI/DesktopMagic.Api.csproj
@@ -4,15 +4,17 @@
net8.0-windows
Stone_Red
Stone_Red
- 0.0.0.5
+ 1.0.0.0
https://github.com/Stone-Red-Code/DesktopMagic
LICENSE
true
- 0.0.0.5
+ 1.0.0.0
- 0.0.0.5
+
enable
AnyCPU;x64
+ README.md
+ Square44x44Logo.altform-lightunplated_targetsize-256.png
@@ -36,6 +38,14 @@
True
\
+
+ True
+ \
+
+
+ True
+ \
+
diff --git a/src/DesktopMagicPluginAPI/IPluginData.cs b/src/DesktopMagicPluginAPI/IPluginData.cs
index 3f0aaca..10a6639 100644
--- a/src/DesktopMagicPluginAPI/IPluginData.cs
+++ b/src/DesktopMagicPluginAPI/IPluginData.cs
@@ -36,4 +36,27 @@ public interface IPluginData
/// Updates the plugin window.
///
void UpdateWindow();
+
+ ///
+ /// Logs a message to the application log.
+ ///
+ /// The message to log.
+ /// The log level (Info, Warning, Error).
+ void Log(string message, LogLevel level = LogLevel.Info);
+
+ ///
+ /// Shows a message box to the user.
+ ///
+ /// The message to display.
+ /// The title of the message box.
+ void ShowMessage(string message, string? title = null);
+
+ ///
+ /// Saves the current state of fields and properties marked with
+ ///
+ ///
+ /// State is automatically saved when the plugin stops, but this method can be called
+ /// to save state at any time, such as after important user interactions.
+ ///
+ void SaveState();
}
\ No newline at end of file
diff --git a/src/DesktopMagicPluginAPI/LogLevel.cs b/src/DesktopMagicPluginAPI/LogLevel.cs
new file mode 100644
index 0000000..f2d92dd
--- /dev/null
+++ b/src/DesktopMagicPluginAPI/LogLevel.cs
@@ -0,0 +1,22 @@
+namespace DesktopMagic.Api;
+
+///
+/// Specifies the severity level of a log message.
+///
+public enum LogLevel
+{
+ ///
+ /// Informational message for general information.
+ ///
+ Info,
+
+ ///
+ /// Warning message indicating a potential issue.
+ ///
+ Warning,
+
+ ///
+ /// Error message indicating a failure or critical issue.
+ ///
+ Error
+}
\ No newline at end of file
diff --git a/src/DesktopMagicPluginAPI/Settings/MouseButton.cs b/src/DesktopMagicPluginAPI/MouseButton.cs
similarity index 88%
rename from src/DesktopMagicPluginAPI/Settings/MouseButton.cs
rename to src/DesktopMagicPluginAPI/MouseButton.cs
index 0a5bc71..d0b612b 100644
--- a/src/DesktopMagicPluginAPI/Settings/MouseButton.cs
+++ b/src/DesktopMagicPluginAPI/MouseButton.cs
@@ -1,4 +1,4 @@
-namespace DesktopMagic.Api.Settings;
+namespace DesktopMagic.Api;
///
/// Mouse Buttons
diff --git a/src/DesktopMagicPluginAPI/PersistStateAttribute.cs b/src/DesktopMagicPluginAPI/PersistStateAttribute.cs
new file mode 100644
index 0000000..b5420a7
--- /dev/null
+++ b/src/DesktopMagicPluginAPI/PersistStateAttribute.cs
@@ -0,0 +1,15 @@
+using System;
+
+namespace DesktopMagic.Api;
+
+///
+/// Marks a field or property to be automatically persisted between plugin sessions.
+///
+///
+/// The field or property must be JSON serializable. Complex types should have parameterless constructors.
+/// State is saved when the plugin stops and loaded when it starts.
+///
+[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
+public sealed class PersistStateAttribute : Attribute
+{
+}
diff --git a/src/DesktopMagicPluginAPI/Plugin.cs b/src/DesktopMagicPluginAPI/Plugin.cs
index 911db6c..5677365 100644
--- a/src/DesktopMagicPluginAPI/Plugin.cs
+++ b/src/DesktopMagicPluginAPI/Plugin.cs
@@ -7,8 +7,14 @@ using System.Drawing;
namespace DesktopMagic.Api;
///
-/// The plugin class.
+/// Provides an abstract base class for creating plugins that can be integrated into the application, supporting periodic updates,
+/// rendering, and user interaction.
///
+/// Derive from this class to implement custom plugin functionality. The class defines lifecycle methods
+/// such as , , and for activation, deactivation, and periodic
+/// execution. It also provides event handlers for mouse and theme interactions, as well as access to application data
+/// and rendering configuration. Implementations should override relevant methods to respond to user input, update
+/// intervals, and configuration changes as needed.
public abstract class Plugin
{
[Setting("desktopmagic-horizontal-alignment", "Horizontal Alignment", -999)]
@@ -86,4 +92,18 @@ public abstract class Plugin
public virtual void OnMouseWheel(Point position, int delta)
{
}
-}
\ No newline at end of file
+
+ ///
+ /// Occurs when the application's theme has changed.
+ ///
+ public virtual void OnThemeChanged()
+ {
+ }
+
+ ///
+ /// Invoked when the settings have changed to allow derived classes to respond to configuration updates.
+ ///
+ public virtual void OnSettingsChanged()
+ {
+ }
+}
diff --git a/src/DesktopMagicPluginAPI/Settings/ColorPicker.cs b/src/DesktopMagicPluginAPI/Settings/ColorPicker.cs
new file mode 100644
index 0000000..ada24ee
--- /dev/null
+++ b/src/DesktopMagicPluginAPI/Settings/ColorPicker.cs
@@ -0,0 +1,62 @@
+using System.Drawing;
+using System.Globalization;
+
+namespace DesktopMagic.Api.Settings;
+
+///
+/// Represents a color picker control.
+///
+public sealed class ColorPicker : Setting
+{
+ private Color _value;
+
+ ///
+ /// Gets or sets the color value assigned to the element.
+ ///
+ public Color Value
+ {
+ get => _value;
+ set
+ {
+ if (_value != value)
+ {
+ _value = value;
+ ValueChanged();
+ }
+ }
+ }
+
+ ///
+ /// Initializes a new instance of the class with the provided .
+ ///
+ /// The default color value.
+ public ColorPicker(Color defaultColor)
+ {
+ _value = defaultColor;
+ }
+
+ ///
+ /// Initializes a new instance of the class with a default color of White.
+ ///
+ public ColorPicker() : this(Color.White)
+ {
+ }
+
+ internal override string GetJsonValue()
+ {
+ return $"{Value.A},{Value.R},{Value.G},{Value.B}";
+ }
+
+ internal override void SetJsonValue(string value)
+ {
+ string[] parts = value.Split(',');
+ if (parts.Length == 4 &&
+ byte.TryParse(parts[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out byte a) &&
+ byte.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out byte r) &&
+ byte.TryParse(parts[2], NumberStyles.Integer, CultureInfo.InvariantCulture, out byte g) &&
+ byte.TryParse(parts[3], NumberStyles.Integer, CultureInfo.InvariantCulture, out byte b))
+ {
+ Value = Color.FromArgb(a, r, g, b);
+ }
+ }
+}
diff --git a/src/DesktopMagicPluginAPI/Settings/FileSelector.cs b/src/DesktopMagicPluginAPI/Settings/FileSelector.cs
new file mode 100644
index 0000000..0fde70a
--- /dev/null
+++ b/src/DesktopMagicPluginAPI/Settings/FileSelector.cs
@@ -0,0 +1,65 @@
+namespace DesktopMagic.Api.Settings;
+
+///
+/// Represents a file selector control that allows the user to browse for a file.
+///
+public sealed class FileSelector : Setting
+{
+ private string _value = string.Empty;
+
+ ///
+ /// Gets or sets the selected file path.
+ ///
+ public string Value
+ {
+ get => _value;
+ set
+ {
+ if (_value != value)
+ {
+ _value = value;
+ ValueChanged();
+ }
+ }
+ }
+
+ ///
+ /// Gets or sets the file filter for the file dialog (e.g., "Image Files|*.png;*.jpg|All Files|*.*").
+ ///
+ public string Filter { get; set; }
+
+ ///
+ /// Gets or sets the title of the file dialog.
+ ///
+ public string Title { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the file selector should select folders instead of files.
+ ///
+ public bool SelectFolder { get; set; }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The default file path.
+ /// The file filter for the file dialog.
+ /// The title of the file dialog.
+ /// If true, selects folders instead of files.
+ public FileSelector(string defaultPath = "", string filter = "All Files|*.*", string title = "Select File", bool selectFolder = false)
+ {
+ _value = defaultPath;
+ Filter = filter;
+ Title = title;
+ SelectFolder = selectFolder;
+ }
+
+ internal override string GetJsonValue()
+ {
+ return Value;
+ }
+
+ internal override void SetJsonValue(string value)
+ {
+ Value = value;
+ }
+}
diff --git a/src/MsixPackaging/Package.appxmanifest b/src/MsixPackaging/Package.appxmanifest
index 09355a6..9c328fd 100644
--- a/src/MsixPackaging/Package.appxmanifest
+++ b/src/MsixPackaging/Package.appxmanifest
@@ -9,7 +9,7 @@
+ Version="1.3.0.0" />
Desktop Magic