mirror of
https://github.com/Stone-Red-Code/DesktopMagic.git
synced 2026-09-04 00:46:12 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68ca15a81f | ||
|
|
0b278ed31a | ||
|
|
346a7ad5a1 | ||
|
|
ea07f0db3a | ||
|
|
667c913e57 | ||
|
|
4d0119e4b3 | ||
|
|
f957edd886 | ||
|
|
1a6cd5a744 | ||
|
|
deddea2888 | ||
|
|
e18ab1ec56 | ||
|
|
a8236bd8da | ||
|
|
cb8df79a5f | ||
|
|
907d743c0d | ||
|
|
835ef23719 | ||
|
|
d2da426baf | ||
|
|
82fe362559 | ||
|
|
439eca0ef0 | ||
|
|
fca25aeb59 | ||
|
|
dd9953cb7d | ||
|
|
990793fb01 | ||
|
|
c4517fb8ac | ||
|
|
92ecd2e93d | ||
|
|
056c42d4df | ||
|
|
c64b70eb92 | ||
|
|
0804738e45 | ||
|
|
727324ca9a | ||
|
|
389256a380 | ||
|
|
ec7917625c | ||
|
|
6aa107233c | ||
|
|
3edb2cff84 | ||
|
|
0bb5b10dba | ||
|
|
2b363bcd18 | ||
|
|
03da9189a5 | ||
|
|
0a57b8a3d6 | ||
|
|
16c5c98f1a | ||
|
|
9e9c49214a | ||
|
|
43c77e6a36 | ||
|
|
4778f5673f | ||
|
|
e3d6eb4c8c | ||
|
|
c90be28f38 | ||
|
|
a564b4db5e | ||
|
|
2cece7dbdc | ||
|
|
ba5bfc7df3 | ||
|
|
d316108868 | ||
|
|
9a96c60af0 | ||
|
|
a8fe2c80f1 | ||
|
|
ac7826898a | ||
|
|
780b506103 | ||
|
|
10733383d3 | ||
|
|
09ce072ec6 | ||
|
|
fcacf36187 | ||
|
|
980ef6473e |
Binary file not shown.
@@ -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")
|
||||
|
||||
@@ -1,20 +1,25 @@
|
||||
<Application x:Class="DesktopMagic.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
|
||||
xmlns:helpers="clr-namespace:DesktopMagic.Helpers"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
StartupUri="MainWindow.xaml">
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="Resources\Strings\StringResources.en.xaml" />
|
||||
<materialDesign:BundledTheme BaseTheme="Light" PrimaryColor="Grey" SecondaryColor="LightBlue" />
|
||||
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesign3.Defaults.xaml" />
|
||||
<ui:ThemesDictionary Theme="Light" />
|
||||
<ui:ControlsDictionary />
|
||||
<ResourceDictionary Source="pack://application:,,,/BusyIndicator;component/Theme/Default.xaml" />
|
||||
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.ObsoleteBrushes.xaml" />
|
||||
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.ObsoleteStyles.xaml" />
|
||||
<ResourceDictionary Source="Resources\Images\ImageResources.xaml" />
|
||||
<ResourceDictionary Source="Resources\Styles\ToggleSwitchContentLeftStyle.xaml" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
<Thickness x:Key="DefaultMargin" Top="6" Left="6" Right="6" Bottom ="6"></Thickness>
|
||||
|
||||
<!-- Converters -->
|
||||
<helpers:UnderscoreEscapingConverter x:Key="UnderscoreEscapingConverter" />
|
||||
|
||||
<!-- Margins -->
|
||||
<Thickness x:Key="DefaultMargin" Top="5" Left="5" Right="5" Bottom ="5" />
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
@@ -5,6 +5,8 @@ using System.IO;
|
||||
using System.Threading;
|
||||
using System.Windows;
|
||||
|
||||
using Wpf.Ui;
|
||||
|
||||
namespace DesktopMagic;
|
||||
|
||||
/// <summary>
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<Bitmap?> 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"
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -7,11 +7,11 @@
|
||||
<ApplicationIcon>icon.ico</ApplicationIcon>
|
||||
<RunPostBuildEvent>OnBuildSuccess</RunPostBuildEvent>
|
||||
<Authors>Stone_Red</Authors>
|
||||
<Version>1.2.0.0</Version>
|
||||
<Version>1.3.0.0</Version>
|
||||
<PackageProjectUrl>https://github.com/Stone-Red-Code/DesktopMagic</PackageProjectUrl>
|
||||
<RepositoryUrl>https://github.com/Stone-Red-Code/DesktopMagic</RepositoryUrl>
|
||||
<AssemblyVersion>1.2.0.0</AssemblyVersion>
|
||||
<FileVersion>1.2.0.0</FileVersion>
|
||||
<AssemblyVersion>1.3.0.0</AssemblyVersion>
|
||||
<FileVersion>1.3.0.0</FileVersion>
|
||||
<TargetFramework>net8.0-windows7.0</TargetFramework>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<Nullable>enable</Nullable>
|
||||
@@ -33,6 +33,7 @@
|
||||
<None Remove="icon_Dark.ico" />
|
||||
<None Remove="Resources\Images\modio-cog-blue.png" />
|
||||
<None Remove="Resources\Images\modio-logo-bluedark.png" />
|
||||
<None Remove="Resources\Images\modio-logo-bluelight.png" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -46,9 +47,12 @@
|
||||
<PackageReference Include="CuteUtils" Version="1.0.0" />
|
||||
<PackageReference Include="Interop.IWshRuntimeLibrary" Version="1.0.1" />
|
||||
<PackageReference Include="MaterialDesignThemes" Version="5.1.0" />
|
||||
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.2592.51" />
|
||||
<PackageReference Include="Modio" Version="1.0.0" />
|
||||
<PackageReference Include="NAudio" Version="2.2.1" />
|
||||
<PackageReference Include="System.Management" Version="8.0.0" />
|
||||
<PackageReference Include="WPF-UI" Version="4.1.0" />
|
||||
<PackageReference Include="WPF-UI.Tray" Version="4.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -63,6 +67,7 @@
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Resource>
|
||||
<Resource Include="Resources\Images\modio-logo-bluedark.png" />
|
||||
<Resource Include="Resources\Images\modio-logo-bluelight.png" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,50 +1,74 @@
|
||||
<Window x:Class="DesktopMagic.Dialogs.ColorDialog"
|
||||
<ui:FluentWindow x:Class="DesktopMagic.Dialogs.ColorDialog"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
|
||||
xmlns:tray="http://schemas.lepo.co/wpfui/2022/xaml/tray"
|
||||
xmlns:pages="clr-namespace:DesktopMagic.Pages"
|
||||
xmlns:plugins="clr-namespace:DesktopMagic.Plugins"
|
||||
mc:Ignorable="d"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
xmlns:local="clr-namespace:DesktopMagic.Dialogs"
|
||||
mc:Ignorable="d"
|
||||
ResizeMode="NoResize"
|
||||
Title="ColorDialog"
|
||||
xmlns:dataContext="clr-namespace:DesktopMagic.DataContexts"
|
||||
d:DataContext="{d:DesignInstance Type=dataContext:MainWindowDataContext}"
|
||||
ui:Design.Background="{DynamicResource ApplicationBackgroundBrush}"
|
||||
ui:Design.Foreground="{DynamicResource TextFillColorPrimaryBrush}"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
|
||||
WindowCornerPreference="Round"
|
||||
WindowBackdropType="Tabbed"
|
||||
Height="300"
|
||||
Width="450"
|
||||
MinHeight="300"
|
||||
MinWidth="450"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
SizeToContent="WidthAndHeight">
|
||||
ExtendsContentIntoTitleBar="True">
|
||||
<Grid>
|
||||
<StackPanel Margin="15">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<ui:TitleBar ShowMinimize="False" ShowMaximize="False">
|
||||
<ui:TitleBar.Icon>
|
||||
<ui:ImageIcon Source="{StaticResource Icon}" />
|
||||
</ui:TitleBar.Icon>
|
||||
</ui:TitleBar>
|
||||
|
||||
<StackPanel Grid.Row="1" Margin="15">
|
||||
<Label x:Name="label" Content="Label" HorizontalAlignment="Left" VerticalAlignment="Top" />
|
||||
<Grid Margin="{StaticResource DefaultMargin}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<DockPanel>
|
||||
<Label Content="A:" Width="20" />
|
||||
<Slider x:Name="alphaSlider" VerticalAlignment="Center" Maximum="255" ValueChanged="ColorSliders_ValueChanged" TickFrequency="1" IsSnapToTickEnabled="True" Background="#00030000" />
|
||||
<DockPanel Grid.Row="0">
|
||||
<Label Content="A:" Width="20" />
|
||||
<Slider x:Name="alphaSlider" VerticalAlignment="Center" Maximum="255" ValueChanged="ColorSliders_ValueChanged" TickFrequency="1" IsSnapToTickEnabled="True" />
|
||||
</DockPanel>
|
||||
<DockPanel Grid.Row="1">
|
||||
<Label Content="R:" Width="20" />
|
||||
<Slider x:Name="redSlider" VerticalAlignment="Center" Maximum="255" ValueChanged="ColorSliders_ValueChanged" TickFrequency="1" IsSnapToTickEnabled="True" />
|
||||
</DockPanel>
|
||||
<DockPanel Grid.Row="2">
|
||||
<Label Content="G:" Width="20" />
|
||||
<Slider x:Name="greenSlider" VerticalAlignment="Center" Maximum="255" ValueChanged="ColorSliders_ValueChanged" TickFrequency="1" IsSnapToTickEnabled="True" />
|
||||
</DockPanel>
|
||||
<DockPanel Grid.Row="3">
|
||||
<Label Content="B:" Width="20" />
|
||||
<Slider x:Name="blueSlider" VerticalAlignment="Center" Maximum="255" ValueChanged="ColorSliders_ValueChanged" TickFrequency="1" IsSnapToTickEnabled="True" />
|
||||
</DockPanel>
|
||||
</Grid>
|
||||
<DockPanel Margin="0 5 0 0">
|
||||
<Border x:Name="colorRechtangle" CornerRadius="4" BorderThickness="1" BorderBrush="{DynamicResource ControlElevationBorderBrush}" Background="White" Height="30" Width="30" Margin="0,0,10,0" />
|
||||
<TextBox x:Name="colorHexTextBox" MaxLength="9" CharacterCasing="Upper" TextWrapping="Wrap" TextChanged="ColorHexTextBox_TextChanged" RenderTransformOrigin="0.5,0.5" VerticalContentAlignment="Center"/>
|
||||
</DockPanel>
|
||||
<DockPanel>
|
||||
<Label Content="R:" Width="20" />
|
||||
<Slider x:Name="redSlider" VerticalAlignment="Center" Maximum="255" ValueChanged="ColorSliders_ValueChanged" TickFrequency="1" IsSnapToTickEnabled="True" Background="#00030000" />
|
||||
</DockPanel>
|
||||
<DockPanel>
|
||||
<Label Content="G:" Width="20" />
|
||||
<Slider x:Name="greenSlider" VerticalAlignment="Center" Maximum="255" ValueChanged="ColorSliders_ValueChanged" TickFrequency="1" IsSnapToTickEnabled="True" Background="#0000FF00" />
|
||||
</DockPanel>
|
||||
<DockPanel>
|
||||
<Label Content="B:" Width="20" />
|
||||
<Slider x:Name="blueSlider" VerticalAlignment="Center" Maximum="255" ValueChanged="ColorSliders_ValueChanged" TickFrequency="1" IsSnapToTickEnabled="True" Background="#000000FF" />
|
||||
</DockPanel>
|
||||
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Border CornerRadius="1" BorderThickness="1.5" BorderBrush="Gray" Height="23" Width="23" Margin="0,0,5,0">
|
||||
<Rectangle x:Name="colorRechtangle" Fill="White" />
|
||||
</Border>
|
||||
<TextBox x:Name="colorHexTextBox" MaxLength="9" CharacterCasing="Upper" Height="23" TextWrapping="Wrap" Width="350" TextChanged="ColorHexTextBox_TextChanged" RenderTransformOrigin="0.5,0.5" VerticalContentAlignment="Center">
|
||||
<materialDesign:TextFieldAssist.CharacterCounterStyle>
|
||||
<Style TargetType="TextBlock" />
|
||||
</materialDesign:TextFieldAssist.CharacterCounterStyle>
|
||||
</TextBox>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,10,0,0">
|
||||
<Button x:Name="okButton" Content="{DynamicResource ok }" HorizontalAlignment="Left" Margin="0,0,10,0" VerticalAlignment="Top" Width="100" Click="OkButton_Click" Cursor="Hand" />
|
||||
<Button x:Name="cancelButton" Content="{DynamicResource cancel }" HorizontalAlignment="Left" Margin="0,0,0,0" VerticalAlignment="Top" Width="100" Click="CancelButton_Click" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right" Margin="5">
|
||||
<Button x:Name="okButton" Content="{DynamicResource ok }" HorizontalAlignment="Left" Margin="0 0 5 0" VerticalAlignment="Top" Width="100" Click="OkButton_Click" Cursor="Hand" />
|
||||
<Button x:Name="cancelButton" Content="{DynamicResource cancel }" HorizontalAlignment="Left" Margin="0 0 0 0" VerticalAlignment="Top" Width="100" Click="CancelButton_Click" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Window>
|
||||
</ui:FluentWindow>
|
||||
@@ -4,12 +4,14 @@ using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
|
||||
using Wpf.Ui.Controls;
|
||||
|
||||
namespace DesktopMagic.Dialogs;
|
||||
|
||||
/// <summary>
|
||||
/// Interaction logic for ColorDialog.xaml
|
||||
/// </summary>
|
||||
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
|
||||
{
|
||||
|
||||
@@ -1,22 +1,47 @@
|
||||
<Window x:Class="DesktopMagic.Dialogs.InputDialog"
|
||||
<ui:FluentWindow x:Class="DesktopMagic.Dialogs.InputDialog"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
|
||||
xmlns:tray="http://schemas.lepo.co/wpfui/2022/xaml/tray"
|
||||
xmlns:pages="clr-namespace:DesktopMagic.Pages"
|
||||
xmlns:plugins="clr-namespace:DesktopMagic.Plugins"
|
||||
mc:Ignorable="d"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:local="clr-namespace:DesktopMagic.Dialogs"
|
||||
mc:Ignorable="d"
|
||||
Title="InputDialog"
|
||||
ResizeMode="NoResize"
|
||||
xmlns:dataContext="clr-namespace:DesktopMagic.DataContexts"
|
||||
d:DataContext="{d:DesignInstance Type=dataContext:MainWindowDataContext}"
|
||||
ui:Design.Background="{DynamicResource ApplicationBackgroundBrush}"
|
||||
ui:Design.Foreground="{DynamicResource TextFillColorPrimaryBrush}"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
|
||||
WindowCornerPreference="Round"
|
||||
WindowBackdropType="Tabbed"
|
||||
Height="200"
|
||||
Width="300"
|
||||
MinHeight="160"
|
||||
MinWidth="300"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
SizeToContent="WidthAndHeight">
|
||||
ExtendsContentIntoTitleBar="True">
|
||||
<Grid>
|
||||
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center" Margin="15">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<ui:TitleBar ShowMinimize="False" ShowMaximize="False">
|
||||
<ui:TitleBar.Icon>
|
||||
<ui:ImageIcon Source="{StaticResource Icon}" />
|
||||
</ui:TitleBar.Icon>
|
||||
</ui:TitleBar>
|
||||
|
||||
<StackPanel Grid.Row="1" VerticalAlignment="Center" HorizontalAlignment="Stretch" Margin="15">
|
||||
<Label x:Name="label" Content="Label" HorizontalAlignment="Left" VerticalAlignment="Top" />
|
||||
<TextBox x:Name="textBox" HorizontalAlignment="Left" Height="23" TextWrapping="Wrap" VerticalAlignment="Top" Width="280" VerticalContentAlignment="Center" />
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,10,0,0">
|
||||
<Button x:Name="okButton" Content="{DynamicResource ok }" HorizontalAlignment="Left" Margin="0,0,10,0" VerticalAlignment="Top" Width="100" Click="OkButton_Click" Cursor="Hand" />
|
||||
<Button x:Name="cancelButton" Content="{DynamicResource cancel }" HorizontalAlignment="Left" Margin="0,0,0,0" VerticalAlignment="Top" Width="100" Click="CancelButton_Click" />
|
||||
</StackPanel>
|
||||
<ui:TextBox x:Name="textBox" HorizontalAlignment="Stretch" Margin="5" TextWrapping="Wrap" VerticalAlignment="Top" VerticalContentAlignment="Center" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right" Margin="5">
|
||||
<Button x:Name="okButton" Content="{DynamicResource ok }" HorizontalAlignment="Left" Margin="0 0 5 0" VerticalAlignment="Top" Width="100" Click="OkButton_Click" Cursor="Hand" />
|
||||
<Button x:Name="cancelButton" Content="{DynamicResource cancel }" HorizontalAlignment="Left" Margin="0 0 0 0" VerticalAlignment="Top" Width="100" Click="CancelButton_Click" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Window>
|
||||
</ui:FluentWindow>
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace DesktopMagic.Dialogs;
|
||||
|
||||
public partial class InputDialog : Window
|
||||
public partial class InputDialog : Wpf.Ui.Controls.FluentWindow
|
||||
{
|
||||
public string ResponseText
|
||||
{
|
||||
|
||||
@@ -1,22 +1,45 @@
|
||||
<Window x:Class="DesktopMagic.Dialogs.ThemeDialog"
|
||||
<ui:FluentWindow x:Class="DesktopMagic.Dialogs.ThemeDialog"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
|
||||
xmlns:tray="http://schemas.lepo.co/wpfui/2022/xaml/tray"
|
||||
xmlns:pages="clr-namespace:DesktopMagic.Pages"
|
||||
xmlns:plugins="clr-namespace:DesktopMagic.Plugins"
|
||||
mc:Ignorable="d"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
xmlns:local="clr-namespace:DesktopMagic.Dialogs"
|
||||
mc:Ignorable="d"
|
||||
ResizeMode="NoResize"
|
||||
Title="ThemeDialog"
|
||||
xmlns:dataContext="clr-namespace:DesktopMagic.DataContexts"
|
||||
d:DataContext="{d:DesignInstance Type=dataContext:MainWindowDataContext}"
|
||||
ui:Design.Background="{DynamicResource ApplicationBackgroundBrush}"
|
||||
ui:Design.Foreground="{DynamicResource TextFillColorPrimaryBrush}"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
|
||||
WindowCornerPreference="Round"
|
||||
WindowBackdropType="Tabbed"
|
||||
Height="290"
|
||||
Width="550"
|
||||
MinHeight="290"
|
||||
MinWidth="550"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
SizeToContent="WidthAndHeight">
|
||||
ExtendsContentIntoTitleBar="True">
|
||||
<Grid>
|
||||
<StackPanel Margin="15">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<ui:TitleBar ShowMinimize="False" ShowMaximize="False">
|
||||
<ui:TitleBar.Icon>
|
||||
<ui:ImageIcon Source="{StaticResource Icon}" />
|
||||
</ui:TitleBar.Icon>
|
||||
</ui:TitleBar>
|
||||
|
||||
<StackPanel Grid.Row="1" Margin="15">
|
||||
<Label x:Name="label" Content="Label" HorizontalAlignment="Left" VerticalAlignment="Top" />
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid Grid.Column="0" Margin="{StaticResource DefaultMargin}" HorizontalAlignment="Stretch">
|
||||
<Grid.ColumnDefinitions>
|
||||
@@ -33,44 +56,37 @@
|
||||
<ComboBox x:Name="fontComboBox" Grid.Column="1" Grid.Row="0" HorizontalAlignment="Stretch" VerticalAlignment="Top" SelectionChanged="FontComboBox_SelectionChanged" Margin="0,0,0,5" />
|
||||
|
||||
<TextBlock Text="Corner Radius: " Grid.Column="0" Grid.Row="1" VerticalAlignment="Center"></TextBlock>
|
||||
<TextBox x:Name="cornerRadiusTextBox" Grid.Column="1" Grid.Row="1" TextChanged="CornerRadiusTextBox_TextChanged" materialDesign:HintAssist.Hint="Enter a number">
|
||||
<materialDesign:TextFieldAssist.CharacterCounterStyle>
|
||||
<Style TargetType="TextBlock" />
|
||||
</materialDesign:TextFieldAssist.CharacterCounterStyle>
|
||||
</TextBox>
|
||||
<ui:NumberBox x:Name="cornerRadiusTextBox" Minimum="0" Grid.Column="1" Grid.Row="1" ValueChanged="CornerRadiusTextBox_ValueChanged" PlaceholderText="Enter a number" Margin="0,0,0,5" />
|
||||
|
||||
<TextBlock Text="Margin: " Grid.Column="0" Grid.Row="3" VerticalAlignment="Center"></TextBlock>
|
||||
<TextBox x:Name="marginTextBox" Grid.Column="1" Grid.Row="3" TextChanged="MarginTextBox_TextChanged" materialDesign:HintAssist.Hint="Enter a number">
|
||||
<materialDesign:TextFieldAssist.CharacterCounterStyle>
|
||||
<Style TargetType="TextBlock" />
|
||||
</materialDesign:TextFieldAssist.CharacterCounterStyle>
|
||||
</TextBox>
|
||||
<ui:NumberBox x:Name="marginTextBox" Minimum="0" Grid.Column="1" Grid.Row="3" ValueChanged="MarginTextBox_ValueChanged" PlaceholderText="Enter a number"/>
|
||||
</Grid>
|
||||
<StackPanel Grid.Column="1" Margin="{StaticResource DefaultMargin}" HorizontalAlignment="Stretch">
|
||||
<DockPanel Margin="0,0,0,5">
|
||||
<Border CornerRadius="1" BorderThickness="1.5" BorderBrush="Gray" Height="23" Width="23" Margin="0,0,5,0">
|
||||
<Rectangle x:Name="primaryColorRechtangle" Fill="White" />
|
||||
</Border>
|
||||
<Button Content="Change Primary Color" Height="23" FontSize="12" FontWeight="Regular" Click="ChangePrimaryColorButton_Click" />
|
||||
<Grid Grid.Column="1" Margin="{StaticResource DefaultMargin}" HorizontalAlignment="Stretch">
|
||||
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<DockPanel Grid.Row="0">
|
||||
<Border x:Name="primaryColorRechtangle" CornerRadius="4" BorderThickness="1" BorderBrush="{DynamicResource ControlElevationBorderBrush}" Background="White" Height="30" Width="30" Margin="0,0,10,0" />
|
||||
<Button Content="Change Primary Color" FontWeight="Regular" Click="ChangePrimaryColorButton_Click" HorizontalAlignment="Stretch" />
|
||||
</DockPanel>
|
||||
<DockPanel Margin="0,0,0,5">
|
||||
<Border CornerRadius="1" BorderThickness="1.5" BorderBrush="Gray" Height="23" Width="23" Margin="0,0,5,0">
|
||||
<Rectangle x:Name="secondaryColorRechtangle" Fill="White" />
|
||||
</Border>
|
||||
<Button Content="Change Secondary Color" Height="23" FontSize="12" FontWeight="Regular" Click="ChangeSecondaryColorButton_Click" />
|
||||
<DockPanel Grid.Row="1">
|
||||
<Border x:Name="secondaryColorRechtangle" CornerRadius="4" BorderThickness="1" BorderBrush="{DynamicResource ControlElevationBorderBrush}" Background="White" Height="30" Width="30" Margin="0,0,10,0" />
|
||||
<Button Content="Change Secondary Color" FontWeight="Regular" Click="ChangeSecondaryColorButton_Click" HorizontalAlignment="Stretch" />
|
||||
</DockPanel>
|
||||
<DockPanel>
|
||||
<Border CornerRadius="1" BorderThickness="1.5" BorderBrush="Gray" Height="23" Width="23" Margin="0,0,5,0">
|
||||
<Rectangle x:Name="backgroundColorRechtangle" Fill="White" />
|
||||
</Border>
|
||||
<Button Content="Change Background Color" Height="23" FontSize="12" FontWeight="Regular" Click="ChangeBackgroundColorButton_Click" />
|
||||
<DockPanel Grid.Row="2">
|
||||
<Border x:Name="backgroundColorRechtangle" CornerRadius="4" BorderThickness="1" BorderBrush="{DynamicResource ControlElevationBorderBrush}" Background="White" Height="30" Width="30" Margin="0,0,10,0" />
|
||||
<Button Content="Change Background Color" FontWeight="Regular" Click="ChangeBackgroundColorButton_Click" HorizontalAlignment="Stretch" />
|
||||
</DockPanel>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,10,0,0">
|
||||
<Button x:Name="okButton" Content="{DynamicResource ok }" HorizontalAlignment="Left" Margin="0,0,10,0" VerticalAlignment="Top" Width="100" Click="OkButton_Click" Cursor="Hand" />
|
||||
<Button x:Name="cancelButton" Content="{DynamicResource cancel }" HorizontalAlignment="Left" Margin="0,0,0,0" VerticalAlignment="Top" Width="100" Click="CancelButton_Click" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right" Margin="5">
|
||||
<Button x:Name="okButton" Content="{DynamicResource ok }" HorizontalAlignment="Left" Margin="0 0 5 0" VerticalAlignment="Top" Width="100" Click="OkButton_Click" Cursor="Hand" />
|
||||
<Button x:Name="cancelButton" Content="{DynamicResource cancel }" HorizontalAlignment="Left" Margin="0 0 0 0" VerticalAlignment="Top" Width="100" Click="CancelButton_Click" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Window>
|
||||
</ui:FluentWindow>
|
||||
@@ -10,7 +10,7 @@ namespace DesktopMagic.Dialogs;
|
||||
/// <summary>
|
||||
/// Interaction logic for ColorDialog.xaml
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 = "<Pending>")]
|
||||
[assembly: SuppressMessage("Critical Code Smell", "S2696:Instance members should not write to \"static\" fields", Justification = "<Pending>", Scope = "member", Target = "~P:DesktopMagic.DataContexts.MainWindowDataContext.Settings")]
|
||||
[assembly: SuppressMessage("Critical Code Smell", "S2696:Instance members should not write to \"static\" fields", Justification = "<Pending>", 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 = "<Pending>", 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 = "<Pending>", 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)")]
|
||||
|
||||
@@ -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("#", "");
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -1,156 +1,107 @@
|
||||
<Window x:Class="DesktopMagic.MainWindow"
|
||||
<ui:FluentWindow x:Class="DesktopMagic.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
xmlns:busyIndicator="https://github.com/moh3ngolshani/BusyIndicator"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
|
||||
xmlns:tray="http://schemas.lepo.co/wpfui/2022/xaml/tray"
|
||||
xmlns:pages="clr-namespace:DesktopMagic.Pages"
|
||||
xmlns:plugins="clr-namespace:DesktopMagic.Plugins"
|
||||
mc:Ignorable="d"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="clr-namespace:DesktopMagic"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:dataContext="clr-namespace:DesktopMagic.DataContexts"
|
||||
d:DataContext="{d:DesignInstance Type=dataContext:MainWindowDataContext}"
|
||||
ui:Design.Background="{DynamicResource ApplicationBackgroundBrush}"
|
||||
ui:Design.Foreground="{DynamicResource TextFillColorPrimaryBrush}"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
|
||||
WindowCornerPreference="Round"
|
||||
WindowBackdropType="Tabbed"
|
||||
Closing="Window_Closing"
|
||||
Closed="Window_Closed"
|
||||
ShowInTaskbar="True"
|
||||
Icon="{StaticResource Icon}"
|
||||
Background="{DynamicResource MaterialDesignPaper}"
|
||||
FontFamily="{DynamicResource MaterialDesignFont}"
|
||||
Height="520"
|
||||
Width="570"
|
||||
MinHeight="520"
|
||||
MinWidth="570"
|
||||
Height="560"
|
||||
Width="900"
|
||||
MinHeight="600"
|
||||
MinWidth="940"
|
||||
ExtendsContentIntoTitleBar="True"
|
||||
WindowState="Minimized"
|
||||
Loaded="Window_Loaded">
|
||||
|
||||
<busyIndicator:BusyMask x:Name="BusyIndicator" IsBusy="{Binding IsLoading}" IndicatorType="Cogs" BusyContent="Please wait..." BusyContentMargin="0,20,0,0" IsBusyAtStartup="False">
|
||||
<Grid>
|
||||
<TextBlock TextWrapping="Wrap" Text="0" VerticalAlignment="Top" FontFamily="Comic Sans MS" Foreground="Transparent" />
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="1*" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid Margin="{StaticResource DefaultMargin}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="1*" />
|
||||
<ColumnDefinition Width="1*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<DockPanel Margin="0,0,2.5,0">
|
||||
<Grid DockPanel.Dock="Top">
|
||||
<Rectangle Grid.Row="1" Fill="#FFECECEC" Height="24" />
|
||||
<CheckBox x:Name="editCheckBox" VerticalAlignment="Center" Grid.Row="1" Content="{DynamicResource editLayout}" Click="EditCheckBox_Click" Foreground="Black" BorderBrush="#FF323232" Style="{StaticResource MaterialDesignDarkCheckBox}" IsChecked="True" />
|
||||
</Grid>
|
||||
<ScrollViewer Background="#FFBBBBBB" PreviewMouseWheel="ScrollViewer_PreviewMouseWheel">
|
||||
<ItemsControl ItemsSource="{Binding Settings.CurrentLayout.Plugins}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<CheckBox Click="PluginCheckBox_Click" Content="{Binding Value.Name}" Tag="{Binding Key}" IsChecked="{Binding Value.Enabled}" Style="{StaticResource MaterialDesignDarkCheckBox}" />
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</DockPanel>
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<DockPanel Grid.Column="1" Margin="2.5,0,0,0">
|
||||
<ComboBox x:Name="optionsComboBox" DisplayMemberPath="Name" DockPanel.Dock="Top" Height="24" SelectedIndex="0" SelectionChanged="OptionsComboBox_SelectionChanged" Background="#FFECECEC" Padding="4" VerticalAlignment="Center" />
|
||||
<ScrollViewer Background="#FFBBBBBB">
|
||||
<Grid>
|
||||
<StackPanel x:Name="optionsPanel" Margin="3,3,3,0" HorizontalAlignment="Stretch" Visibility="Collapsed">
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
</DockPanel>
|
||||
</Grid>
|
||||
<Grid Grid.Row="1">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="110" />
|
||||
</Grid.RowDefinitions>
|
||||
<ui:TitleBar x:Name="TitleBar" Title="{Binding Title}">
|
||||
<ui:TitleBar.Icon>
|
||||
<ui:ImageIcon Source="{StaticResource Icon}" />
|
||||
</ui:TitleBar.Icon>
|
||||
</ui:TitleBar>
|
||||
|
||||
<tray:NotifyIcon Grid.Row="0" FocusOnLeftClick="True" MenuOnRightClick="True" TooltipText="{Binding AppName}" Icon="{StaticResource Icon}" LeftClick="NotifyIcon_LeftClick">
|
||||
<tray:NotifyIcon.Menu>
|
||||
<ContextMenu>
|
||||
<MenuItem Header="Settings" Click="SettingsMenuItem_Click" />
|
||||
<MenuItem Header="Edit Layout" Click="EditLayoutMenuItem_Click" />
|
||||
<MenuItem Header="Quit" Click="QuitMenuItem_Click" />
|
||||
</ContextMenu>
|
||||
</tray:NotifyIcon.Menu>
|
||||
</tray:NotifyIcon>
|
||||
|
||||
<Grid Grid.Row="1">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<ui:NavigationView x:Name="NavigationView" Grid.Row="0" IsBackButtonVisible="Collapsed" IsPaneToggleVisible="True" OpenPaneLength="200" Loaded="NavigationView_Loaded">
|
||||
<ui:NavigationView.MenuItems>
|
||||
<ui:NavigationViewItem Content="{DynamicResource plugins}" TargetPageType="{x:Type pages:MainPage}">
|
||||
<ui:NavigationViewItem.Icon>
|
||||
<ui:SymbolIcon Symbol="Apps24" />
|
||||
</ui:NavigationViewItem.Icon>
|
||||
</ui:NavigationViewItem>
|
||||
<ui:NavigationViewItem Content="{DynamicResource themes}" TargetPageType="{x:Type pages:ThemePage}">
|
||||
<ui:NavigationViewItem.Icon>
|
||||
<ui:SymbolIcon Symbol="Color24" />
|
||||
</ui:NavigationViewItem.Icon>
|
||||
</ui:NavigationViewItem>
|
||||
<ui:NavigationViewItem Content="{DynamicResource pluginManager}" TargetPageType="{x:Type plugins:PluginManager}">
|
||||
<ui:NavigationViewItem.Icon>
|
||||
<ui:SymbolIcon Symbol="AppsAddIn24" />
|
||||
</ui:NavigationViewItem.Icon>
|
||||
</ui:NavigationViewItem>
|
||||
</ui:NavigationView.MenuItems>
|
||||
|
||||
<ui:NavigationView.FooterMenuItems>
|
||||
<ui:NavigationViewItem Content="{DynamicResource requestFeature}" Click="ReportBugNavigationViewItem_Click">
|
||||
<ui:NavigationViewItem.Icon>
|
||||
<ui:SymbolIcon Symbol="ChatHelp24"/>
|
||||
</ui:NavigationViewItem.Icon>
|
||||
</ui:NavigationViewItem>
|
||||
<ui:NavigationViewItem Content="{DynamicResource reportBug}" Click="ReportBugNavigationViewItem_Click">
|
||||
<ui:NavigationViewItem.Icon>
|
||||
<ui:SymbolIcon Symbol="Bug24"/>
|
||||
</ui:NavigationViewItem.Icon>
|
||||
</ui:NavigationViewItem>
|
||||
</ui:NavigationView.FooterMenuItems>
|
||||
</ui:NavigationView>
|
||||
|
||||
<Border Grid.Row="1" Background="{DynamicResource ControlFillColorDefaultBrush}" BorderBrush="{DynamicResource ControlStrokeColorDefaultBrush}" BorderThickness="0,1,0,0" Padding="5 2 5 5">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition />
|
||||
<ColumnDefinition />
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<ListBox x:Name="themesListBox" Grid.Row="0" Grid.ColumnSpan="1" Margin="{StaticResource DefaultMargin}" ItemsSource="{Binding Settings.Themes}" DisplayMemberPath="Name" SelectedValue="{Binding Settings.CurrentLayout.CurrentThemeName}" SelectedValuePath="Name" />
|
||||
<StackPanel Grid.Row="0" Grid.Column="1" VerticalAlignment="Center" Margin="{StaticResource DefaultMargin}">
|
||||
<Button x:Name="addThemeButton" Margin="0,0,0,5" HorizontalAlignment="Stretch" Click="AddThemeButton_Click" FontWeight="Regular">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<materialDesign:PackIcon Kind="Add" Margin="0 0 10 0" />
|
||||
<TextBlock Text="{DynamicResource newTheme}" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button x:Name="deleteThemeButton" Margin="0,0,0,5" HorizontalAlignment="Stretch" Click="DeleteThemeButton_Click" FontWeight="Regular">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<materialDesign:PackIcon Kind="DeleteOutline" Margin="0 0 10 0" />
|
||||
<TextBlock Text="{DynamicResource deleteTheme}" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button x:Name="changeThemeButton" Margin="0,0,0,5" HorizontalAlignment="Stretch" Click="ChangeThemeButton_Click" FontWeight="Regular">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<materialDesign:PackIcon Kind="PaletteOutline" Margin="0 0 10 0" />
|
||||
<TextBlock Text="{DynamicResource changeTheme}" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<StackPanel Orientation="Horizontal" Grid.Column="0">
|
||||
<ui:HyperlinkButton FontWeight="Bold" Content="{DynamicResource developedBy}" NavigateUri="https://me.stone-red.net"/>
|
||||
</StackPanel>
|
||||
<Grid Grid.Row="1" Grid.ColumnSpan="2" VerticalAlignment="Bottom">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition />
|
||||
<ColumnDefinition />
|
||||
<ColumnDefinition />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Rectangle Fill="#FFC5C5C5" Stroke="#FFC5C5C5" Grid.ColumnSpan="3" />
|
||||
<StackPanel Margin="{StaticResource DefaultMargin}" VerticalAlignment="Bottom">
|
||||
<ComboBox x:Name="layoutsComboBox" ItemsSource="{Binding Settings.Layouts}" DisplayMemberPath="Name" SelectedValue="{Binding Settings.CurrentLayoutName}" SelectedValuePath="Name" HorizontalAlignment="Left" Margin="0,0,0,5" Width="170" Height="20" SelectionChanged="LayoutsComboBox_SelectionChanged" />
|
||||
<Button x:Name="newLayoutButton" Margin="0,0,0,5" HorizontalAlignment="Left" VerticalAlignment="Bottom" Width="170" Click="NewLayoutButton_Click" FontWeight="Regular">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<materialDesign:PackIcon Kind="Add" Margin="0 0 10 0" />
|
||||
<TextBlock Text="{DynamicResource newLayout}" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button x:Name="removeLayoutButton" HorizontalAlignment="Left" VerticalAlignment="Bottom" Width="170" Click="RemoveLayoutButton_Click" FontWeight="Regular">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<materialDesign:PackIcon Kind="DeleteOutline" Margin="0 0 10 0" />
|
||||
<TextBlock Text="{DynamicResource deleteLayout}" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="1" Margin="{StaticResource DefaultMargin}" VerticalAlignment="Bottom">
|
||||
<Button x:Name="updatePluginsButton" Margin="0,0,0,5" HorizontalAlignment="Center" Width="170" Click="UpdatePluginsButton_Click" FontWeight="Regular">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<materialDesign:PackIcon Kind="Reload" Margin="0 0 10 0" />
|
||||
<TextBlock Text="{DynamicResource updatePlugins}" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button x:Name="githubButton" HorizontalAlignment="Center" Width="170" Click="GitHubButton_Click" FontWeight="Regular" Cursor="Hand">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<materialDesign:PackIcon Kind="Github" Margin="0 0 10 0" />
|
||||
<TextBlock Text="GitHub" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="2" Margin="{StaticResource DefaultMargin}" VerticalAlignment="Bottom">
|
||||
<CheckBox x:Name="autoStartCheckBox" Content="{DynamicResource autoStart}" Foreground="Black" BorderBrush="#FF323232" Margin="0,0,0,5" Width="170" Height="20" Style="{StaticResource MaterialDesignDarkCheckBox}" IsChecked="{Binding IsAutoStartEnabled}" />
|
||||
<Button x:Name="downloadPluginsButton" Margin="0,0,0,5" HorizontalAlignment="Right" Width="170" Click="PluginManagerButton_Click" FontWeight="Regular" Cursor="Hand">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<materialDesign:PackIcon Kind="ExtensionOutline" Margin="0 0 10 0" />
|
||||
<TextBlock Text="{DynamicResource pluginManager}" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button x:Name="openPluginsFolderButton" HorizontalAlignment="Right" Width="170" Click="OpenPluginsFolderButton_Click" FontWeight="Regular">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<materialDesign:PackIcon Kind="FolderOutline" Margin="0 0 10 0" />
|
||||
<TextBlock Text="{DynamicResource pluginsFolder}" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<ui:ToggleSwitch x:Name="autoStartCheckBox" Style="{StaticResource ToggleSwitchContentLeftStyle}" Grid.Column="2" Margin="0 0 5 0" Content="{DynamicResource autoStart}" IsChecked="{Binding IsAutoStartEnabled}" VerticalAlignment="Center"/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</busyIndicator:BusyMask>
|
||||
</Window>
|
||||
<ContentPresenter x:Name="rootContentDialog" Grid.Row="1" Grid.RowSpan="2" />
|
||||
</Grid>
|
||||
</ui:FluentWindow>
|
||||
+135
-603
@@ -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<PluginMetadata, Type> 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<PluginWindow> Windows { get; } = [];
|
||||
public static List<string> 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<uint, InternalPluginData> 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<PluginMetadata>(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<DesktopMagicSettings>(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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Singleton Manager class that handles global state, plugin management, and shared operations
|
||||
/// </summary>
|
||||
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<uint, InternalPluginData> _plugins = [];
|
||||
private readonly Dictionary<PluginMetadata, Type> _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<IPluginWindow> PluginWindows { get; } = [];
|
||||
|
||||
public IReadOnlyDictionary<uint, InternalPluginData> 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<bool>? EditModeChanged;
|
||||
|
||||
private Manager()
|
||||
{
|
||||
// Private constructor for singleton
|
||||
}
|
||||
|
||||
#region Plugin Management
|
||||
|
||||
public Dictionary<uint, InternalPluginData> 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<PluginMetadata>(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<InternalPluginData>? 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<DesktopMagicSettings>(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<uint> 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
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<Page x:Class="DesktopMagic.Pages.MainPage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="clr-namespace:DesktopMagic.Pages"
|
||||
xmlns:dataContext="clr-namespace:DesktopMagic.DataContexts"
|
||||
mc:Ignorable="d"
|
||||
d:DataContext="{d:DesignInstance Type=dataContext:MainWindowDataContext}"
|
||||
d:DesignHeight="450" d:DesignWidth="800"
|
||||
ScrollViewer.CanContentScroll="False"
|
||||
Title="MainPage">
|
||||
|
||||
<Grid Margin="{StaticResource DefaultMargin}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<ui:Card Padding="5" Margin="0 0 0 5">
|
||||
<ui:ToggleSwitch x:Name="editCheckBox" Height="30" VerticalAlignment="Center" Margin="5 0 0 0" Content="{DynamicResource editLayout}" Click="EditCheckBox_Click" />
|
||||
</ui:Card>
|
||||
|
||||
<ScrollViewer Background="#FFBBBBBB" PreviewMouseWheel="ScrollViewer_PreviewMouseWheel" Grid.Row="1" VerticalAlignment="Stretch" VerticalContentAlignment="Top">
|
||||
<ItemsControl ItemsSource="{Binding Settings.CurrentLayout.Plugins}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<ui:CardExpander Tag="{Binding}" Margin="0 0 0 5" Expanded="OptionsCardExpander_Expanded">
|
||||
<ui:CardExpander.Header>
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<ui:TextBlock Grid.Row="0" Grid.Column="0" FontTypography="Body" Text="{Binding Value.Metadata.Name}" />
|
||||
<Label Grid.Row="1" Grid.Column="0" Foreground="{ui:ThemeResource TextFillColorTertiaryBrush}" FontSize="12" ContentStringFormat="{DynamicResource authorFormat}">
|
||||
<Label.Style>
|
||||
<Style TargetType="Label" BasedOn="{StaticResource {x:Type Label}}">
|
||||
<Setter Property="Content" Value="{Binding Value.Metadata.Author, Converter={StaticResource UnderscoreEscapingConverter}}" />
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding Value.Metadata.Author}" Value="{x:Null}">
|
||||
<Setter Property="Content" Value="{DynamicResource unknown}" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Label.Style>
|
||||
</Label>
|
||||
<ui:ToggleSwitch Grid.Row="0" Grid.RowSpan="2" Grid.Column="1" Click="PluginCheckBox_Click" IsChecked="{Binding Value.Enabled}" Tag="{Binding Key}" Margin="0,0,16,0" VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</ui:CardExpander.Header>
|
||||
</ui:CardExpander>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
|
||||
<ui:Card Grid.Row="2" Grid.ColumnSpan="2" Margin="0 5 0 0" Padding="5">
|
||||
<Grid >
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="1*" />
|
||||
<ColumnDefinition Width="5" />
|
||||
<ColumnDefinition Width="0.5*" />
|
||||
<ColumnDefinition Width="5" />
|
||||
<ColumnDefinition Width="0.5*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<ComboBox x:Name="layoutsComboBox" ItemsSource="{Binding Settings.Layouts}" DisplayMemberPath="Name" SelectedValue="{Binding Settings.CurrentLayoutName}" SelectedValuePath="Name" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Padding="4" SelectionChanged="LayoutsComboBox_SelectionChanged" />
|
||||
<Button x:Name="newLayoutButton" Grid.Column="2" HorizontalAlignment="Stretch" Click="NewLayoutButton_Click" FontWeight="Regular">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<ui:SymbolIcon Symbol="Add24" Margin="0 0 10 0" />
|
||||
<TextBlock Text="{DynamicResource newLayout}" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button x:Name="removeLayoutButton" Grid.Column="4" HorizontalAlignment="Stretch" Click="RemoveLayoutButton_Click" FontWeight="Regular">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<ui:SymbolIcon Symbol="Delete24" Margin="0 0 10 0" />
|
||||
<TextBlock Text="{DynamicResource deleteLayout}" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</Grid>
|
||||
</ui:Card>
|
||||
</Grid>
|
||||
</Page>
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Interaction logic for MainPage.xaml
|
||||
/// </summary>
|
||||
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<uint, PluginSettings> 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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<Page x:Class="DesktopMagic.Pages.ThemePage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="clr-namespace:DesktopMagic.Pages"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="450" d:DesignWidth="800"
|
||||
Title="ThemePage">
|
||||
|
||||
<Grid Margin="{StaticResource DefaultMargin}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<ui:ListView x:Name="themesListBox" Grid.Row="0" Grid.ColumnSpan="1" ItemsSource="{Binding Settings.Themes}" SelectedValue="{Binding Settings.CurrentLayout.CurrentThemeName}" SelectedValuePath="Name" SelectedIndex="2" SelectionMode="Single">
|
||||
<ui:ListView.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Margin="8" VerticalAlignment="Center" FontWeight="SemiBold" Text="{Binding Name, Mode=OneWay}" />
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center" Orientation="Horizontal" Margin="{StaticResource DefaultMargin}">
|
||||
<Button x:Name="editThemeButton" HorizontalAlignment="Stretch" Click="EditThemeButton_Click" FontWeight="Regular" Tag="{Binding}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<ui:SymbolIcon Symbol="Color24" Margin="0 0 10 0" />
|
||||
<TextBlock Text="{DynamicResource editTheme}" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ui:ListView.ItemTemplate>
|
||||
</ui:ListView>
|
||||
|
||||
<ui:Card Grid.Row="1" Grid.ColumnSpan="2" Margin="0 5 0 0" Padding="5">
|
||||
<Grid >
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="1*" />
|
||||
<ColumnDefinition Width="5" />
|
||||
<ColumnDefinition Width="0.5*" />
|
||||
<ColumnDefinition Width="5" />
|
||||
<ColumnDefinition Width="0.5*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Button x:Name="newThemeButton" Grid.Column="2" HorizontalAlignment="Stretch" Click="AddThemeButton_Click" FontWeight="Regular">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<ui:SymbolIcon Symbol="Add24" Margin="0 0 10 0" />
|
||||
<TextBlock Text="{DynamicResource newTheme}" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button x:Name="deleteThemeButton" Grid.Column="4" HorizontalAlignment="Stretch" Click="DeleteThemeButton_Click" FontWeight="Regular">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<ui:SymbolIcon Symbol="Delete24" Margin="0 0 10 0" />
|
||||
<TextBlock Text="{DynamicResource deleteTheme}" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</Grid>
|
||||
</ui:Card>
|
||||
</Grid>
|
||||
</Page>
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Interaction logic for ThemePage.xaml
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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}">
|
||||
<Grid Background="White" Margin="10">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="2*" />
|
||||
<RowDefinition Height="auto" />
|
||||
<RowDefinition Height="3*" />
|
||||
</Grid.RowDefinitions>
|
||||
<ui:Card Padding="5">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="2*" />
|
||||
<RowDefinition Height="auto" />
|
||||
<RowDefinition Height="auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="auto" />
|
||||
<ColumnDefinition />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Border HorizontalAlignment="Center" Margin="10" Width="100" Height="100" BorderThickness="1" BorderBrush="LightGray">
|
||||
<Image Source="{Binding Logo, FallbackValue={StaticResource ModioCogBlue}, TargetNullValue={StaticResource ModioCogBlue}}" Width="100" Height="100" />
|
||||
</Border>
|
||||
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center">
|
||||
<Label Content="{Binding Author}" HorizontalAlignment="Center" ContentStringFormat="{DynamicResource authorFormat}" />
|
||||
<Label Content="{Binding DateAdded}" HorizontalAlignment="Center" ContentStringFormat="{DynamicResource addedFormat}" />
|
||||
<Label Content="{Binding DateUpdated}" HorizontalAlignment="Center" ContentStringFormat="{DynamicResource updatedFormat}" />
|
||||
<Label Content="{Binding Version}" HorizontalAlignment="Center" ContentStringFormat="{DynamicResource versionFormat}" />
|
||||
</StackPanel>
|
||||
|
||||
<Grid Grid.Row="1" Grid.ColumnSpan="2">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition />
|
||||
<ColumnDefinition Width="auto" />
|
||||
<ColumnDefinition />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Image Width="100" Height="100" Margin="5" Source="{Binding Logo, FallbackValue={StaticResource ModioCogBlue}, TargetNullValue={StaticResource ModioCogBlue}}" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" />
|
||||
|
||||
<Button Grid.Column="0" Command="{Binding InstallUninstallButtonData.Command}" IsEnabled="{Binding InstallUninstallButtonData.IsEnabled}" VerticalAlignment="Bottom" HorizontalAlignment="Stretch" Margin="10 5 5 0">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<materialDesign:PackIcon Kind="{Binding InstallUninstallButtonData.IconKind}" Margin="0 0 10 0" />
|
||||
<TextBlock Text="{Binding InstallUninstallButtonData.Text}" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Grid.Column="1" Command="{Binding OpenButtonData.Command}" IsEnabled="{Binding OpenButtonData.IsEnabled}" VerticalAlignment="Bottom" HorizontalAlignment="Stretch" Margin="5 5 10 0">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<materialDesign:PackIcon Kind="{Binding OpenButtonData.IconKind}" Margin="0 0 10 0" />
|
||||
<TextBlock Text="{Binding OpenButtonData.Text}" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center">
|
||||
<Label Content="{Binding Author, Converter={StaticResource UnderscoreEscapingConverter}}" HorizontalAlignment="Center" ContentStringFormat="{DynamicResource authorFormat}" />
|
||||
<Label Content="{Binding DateAdded}" HorizontalAlignment="Center" ContentStringFormat="{DynamicResource addedFormat}" />
|
||||
<Label Content="{Binding DateUpdated}" HorizontalAlignment="Center" ContentStringFormat="{DynamicResource updatedFormat}" />
|
||||
<Label Content="{Binding Version}" HorizontalAlignment="Center" ContentStringFormat="{DynamicResource versionFormat}" />
|
||||
</StackPanel>
|
||||
|
||||
<Grid Grid.Row="1" Grid.ColumnSpan="2" Margin="0 5">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition />
|
||||
<ColumnDefinition />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Button Grid.Column="0" Command="{Binding InstallUninstallButtonData.Command}" IsEnabled="{Binding InstallUninstallButtonData.IsEnabled}" VerticalAlignment="Bottom" HorizontalAlignment="Stretch">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<ui:SymbolIcon Symbol="Delete24" Margin="0 0 10 0" />
|
||||
<TextBlock Text="{Binding InstallUninstallButtonData.Text}" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Grid.Column="1" Command="{Binding OpenButtonData.Command}" IsEnabled="{Binding OpenButtonData.IsEnabled}" VerticalAlignment="Bottom" HorizontalAlignment="Stretch" Margin="5 0 5 0">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<ui:SymbolIcon Symbol="{Binding OpenButtonData.Icon}" Margin="0 0 10 0" />
|
||||
<TextBlock Text="{Binding OpenButtonData.Text}" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<StackPanel Grid.Row="2" Grid.ColumnSpan="2" Margin="5">
|
||||
<ui:TextBlock Text="{Binding Name, Converter={StaticResource UnderscoreEscapingConverter}}" FontTypography="Title" Padding="0" Margin="0 0 0 5" />
|
||||
<TextBlock Text="{Binding Description}" TextWrapping="Wrap" Margin="0" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<StackPanel Grid.Row="2" Grid.ColumnSpan="2" Margin="10">
|
||||
<Label Content="{Binding Name}" FontSize="20" Padding="0" />
|
||||
<TextBlock Text="{Binding Description}" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</ui:Card>
|
||||
</UserControl>
|
||||
@@ -1,25 +1,21 @@
|
||||
<Window x:Class="DesktopMagic.Plugins.PluginManager"
|
||||
<Page x:Class="DesktopMagic.Plugins.PluginManager"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
|
||||
xmlns:busyIndicator="https://github.com/moh3ngolshani/BusyIndicator"
|
||||
xmlns:local="clr-namespace:DesktopMagic.Plugins"
|
||||
xmlns:dataContexts="clr-namespace:DesktopMagic.DataContexts"
|
||||
mc:Ignorable="d"
|
||||
d:DataContext="{d:DesignInstance Type=dataContexts:PluginManagerDataContext}"
|
||||
Title="{DynamicResource pluginManager}"
|
||||
Height="450"
|
||||
Width="800"
|
||||
MinHeight="520"
|
||||
MinWidth="700"
|
||||
WindowStyle="ToolWindow"
|
||||
Background="{DynamicResource MaterialDesignPaper}"
|
||||
FontFamily="{DynamicResource MaterialDesignFont}">
|
||||
d:DesignHeight="450"
|
||||
d:DesignWidth="800"
|
||||
ScrollViewer.CanContentScroll="False"
|
||||
Title="{DynamicResource pluginManager}">
|
||||
|
||||
<busyIndicator:BusyMask x:Name="BusyIndicator" IsBusy="{Binding IsLoading}" IndicatorType="Cogs" BusyContent="Please wait..." BusyContentMargin="0,20,0,0" IsBusyAtStartup="False">
|
||||
<Grid Margin="5">
|
||||
<busyIndicator:BusyMask x:Name="BusyIndicator" IsBusy="{Binding IsLoading}" IndicatorType="Cogs" BusyContent="Please wait..." BusyContentMargin="0,20,0,0" IsBusyAtStartup="False" Foreground="{DynamicResource TextFillColorPrimaryBrush}" Background="{DynamicResource ControlStrokeColorDefaultBrush}">
|
||||
<Grid Margin="{StaticResource DefaultMargin}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="auto" />
|
||||
<RowDefinition Height="auto" />
|
||||
@@ -28,21 +24,29 @@
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition />
|
||||
<ColumnDefinition Width="5" />
|
||||
<ColumnDefinition />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Label Content="{DynamicResource allPlugins}" FontSize="20" />
|
||||
<ui:Card Grid.ColumnSpan="3" Padding="5" Margin="0 0 0 5">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition />
|
||||
<ColumnDefinition Width="10" />
|
||||
<ColumnDefinition />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<TextBox Grid.Row="1" Text="{Binding AllPluginsSearchText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" TextChanged="AllPluginsSearchTextBox_TextChanged" materialDesign:HintAssist.Hint="Search..." materialDesign:TextFieldAssist.HasLeadingIcon="True" materialDesign:TextFieldAssist.LeadingIcon="Search" />
|
||||
<ui:TextBox Grid.Column="0" Text="{Binding AllPluginsSearchText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" TextChanged="AllPluginsSearchTextBox_TextChanged" PlaceholderText="{DynamicResource searchAllPlugins}" />
|
||||
<ui:TextBox Grid.Column="2" Text="{Binding InstalledPluginsSearchText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" TextChanged="InstalledPluginsSearchTextBox_TextChanged" PlaceholderText="{DynamicResource searchInstalledPlugins}" />
|
||||
</Grid>
|
||||
</ui:Card>
|
||||
|
||||
<busyIndicator:BusyMask Grid.Row="2" IsBusy="{Binding IsSearching}" IndicatorType="Ring" BusyContent="Searching..." BusyContentMargin="0,50,0,0" IsBusyAtStartup="False">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<busyIndicator:BusyMask Grid.Row="2" IsBusy="{Binding IsSearching}" IndicatorType="Ring" BusyContent="Searching..." BusyContentMargin="0,50,0,0" IsBusyAtStartup="False" Foreground="{DynamicResource TextFillColorPrimaryBrush}" Background="{DynamicResource ControlStrokeColorDefaultBrush}">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto" VerticalAlignment="Stretch" Padding="0" VerticalContentAlignment="Top" Margin="0 0 2.5 0">
|
||||
<ItemsControl ItemsSource="{Binding AllPlugins}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Border BorderBrush="LightGray" BorderThickness="1">
|
||||
<local:ModEntry />
|
||||
</Border>
|
||||
<local:ModEntry Margin="0 0 0 5" />
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
<ItemsControl.Style>
|
||||
@@ -64,17 +68,11 @@
|
||||
</ScrollViewer>
|
||||
</busyIndicator:BusyMask>
|
||||
|
||||
<Label Grid.Column="1" Content="{DynamicResource installedPlugins}" FontSize="20" />
|
||||
|
||||
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding InstalledPluginsSearchText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" TextChanged="InstalledPluginsSearchTextBox_TextChanged" materialDesign:HintAssist.Hint="Search..." materialDesign:TextFieldAssist.HasLeadingIcon="True" materialDesign:TextFieldAssist.LeadingIcon="Search" />
|
||||
|
||||
<ScrollViewer Grid.Row="2" Grid.Column="1" VerticalScrollBarVisibility="Auto">
|
||||
<ItemsControl ItemsSource="{Binding InstalledPlugins}">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto" Grid.Row="2" Grid.Column="2" VerticalAlignment="Stretch" Padding="0" VerticalContentAlignment="Top">
|
||||
<ItemsControl ItemsSource="{Binding InstalledPlugins}" Margin="2.5 0 0 0">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Border BorderBrush="LightGray" BorderThickness="1">
|
||||
<local:ModEntry />
|
||||
</Border>
|
||||
<local:ModEntry Margin="0 0 0 5"/>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
<ItemsControl.Style>
|
||||
@@ -95,23 +93,39 @@
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
|
||||
<Border Grid.Row="3" Grid.ColumnSpan="2" Padding="0 5 0 0" BorderThickness="0 2 0 0" BorderBrush="DarkGray">
|
||||
<DockPanel>
|
||||
<Image Source="{StaticResource ModioLogoBlueDark}" Cursor="Hand" Height="30" MouseUp="Image_MouseUp" HorizontalAlignment="Left" />
|
||||
<Button DockPanel.Dock="Right" HorizontalAlignment="Right" Click="LogInButton_Click">
|
||||
<ui:Card Grid.Row="3" Grid.ColumnSpan="3" Padding="5" Margin="0 5 0 0">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="15" />
|
||||
<ColumnDefinition Width="1.05*" />
|
||||
<ColumnDefinition Width="5.5" />
|
||||
<ColumnDefinition Width="1.05*" />
|
||||
<ColumnDefinition Width="5" />
|
||||
<ColumnDefinition Width="1.05*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Image Source="{Binding ModIoIcon}" Cursor="Hand" Height="30" MouseUp="Image_MouseUp" HorizontalAlignment="Left" />
|
||||
<Button Grid.Column="2" HorizontalAlignment="Stretch" Click="CreatePluginButton_Click">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<materialDesign:PackIcon Kind="Login" Margin="0 0 10 0" />
|
||||
<TextBlock Text="{Binding LoginButtonText}" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button DockPanel.Dock="Right" HorizontalAlignment="Right" Margin="0 0 10 0" Click="CreatePluginButton_Click">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<materialDesign:PackIcon Kind="Add" Margin="0 0 10 0" />
|
||||
<ui:SymbolIcon Symbol="Add24" Margin="0 0 10 0" />
|
||||
<TextBlock Text="{DynamicResource createNewPlugin}" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
<Button Grid.Column="4" HorizontalAlignment="Stretch" Click="ReloadPluginsButton_Click">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<ui:SymbolIcon Symbol="ArrowClockwise24" Margin="0 0 10 0" />
|
||||
<TextBlock Text="{DynamicResource reloadPlugins}" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Grid.Column="6" HorizontalAlignment="Stretch" Click="LogInButton_Click">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<ui:SymbolIcon Symbol="PersonKey20" Margin="0 0 10 0" />
|
||||
<TextBlock Text="{Binding LoginButtonText}" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</Grid>
|
||||
</ui:Card>
|
||||
</Grid>
|
||||
</busyIndicator:BusyMask>
|
||||
</Window>
|
||||
</Page>
|
||||
@@ -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;
|
||||
/// <summary>
|
||||
/// Interaction logic for PluginManager.xaml
|
||||
/// </summary>
|
||||
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<uint> 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<PluginMetadata>(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<Mod> 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<uint> pluginIds = [];
|
||||
|
||||
foreach (string pluginPath in Directory.GetDirectories(pluginsPath))
|
||||
{
|
||||
string pluginMetadataPath = Path.Combine(pluginPath, "metadata.json");
|
||||
if (!File.Exists(pluginMetadataPath))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
PluginMetadata? pluginMetadata = JsonSerializer.Deserialize<PluginMetadata>(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<Mod> 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<Mod> 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<Mod> 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<Mod> mods = await modIoClient.User.GetSubscriptions(ModFilter.GameId.Eq(ModIoGameId)).ToList();
|
||||
App.Logger.LogInfo("Syncing plugins with mod.io subscriptions", source: "PluginManager");
|
||||
|
||||
List<PluginEntryDataContext> pluginsToRemove = [];
|
||||
|
||||
foreach (PluginEntryDataContext plugin in pluginManagerDataContext.InstalledPlugins)
|
||||
try
|
||||
{
|
||||
if (!plugin.IsLocalPlugin && !mods.Any(m => m.Id == plugin.Id))
|
||||
{
|
||||
pluginsToRemove.Add(plugin);
|
||||
}
|
||||
}
|
||||
IReadOnlyList<Mod> 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<PluginEntryDataContext> 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<Mod> 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<Mod> 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");
|
||||
}
|
||||
}
|
||||
@@ -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<string> 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)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<Window x:Class="DesktopMagic.PluginWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:busyIndicator="https://github.com/moh3ngolshani/BusyIndicator"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:local="clr-namespace:DesktopMagic"
|
||||
@@ -8,25 +9,28 @@
|
||||
Title="PluginWindow" Height="450" Width="800"
|
||||
Background="Transparent"
|
||||
WindowStyle="None"
|
||||
ShowInTaskbar="False"
|
||||
AllowsTransparency="True"
|
||||
LocationChanged="Window_LocationChanged"
|
||||
SizeChanged="Window_SizeChanged"
|
||||
Closing="Window_Closing"
|
||||
ContentRendered="Window_ContentRendered"
|
||||
x:Name="window">
|
||||
<Grid>
|
||||
<Rectangle x:Name="panel" Fill="#4CBAFFEF" Stroke="White" Margin="0,0,0,0" Visibility="Collapsed" />
|
||||
<busyIndicator:BusyMask x:Name="busyMask" IsBusyAtStartup="True" Background="Transparent" BusyContent="" BusyContentMargin="-5" IndicatorType="Cogs">
|
||||
<Grid>
|
||||
<Rectangle x:Name="panel" Fill="#4CBAFFEF" Stroke="White" Margin="0,0,0,0" Visibility="Collapsed" />
|
||||
|
||||
<Border SizeChanged="Border_SizeChanged" x:Name="border" />
|
||||
<Viewbox SizeChanged="ViewBox_SizeChanged" x:Name="viewBox" StretchDirection="Both" Stretch="Uniform">
|
||||
<Viewbox.Clip>
|
||||
<RectangleGeometry x:Name="rectangleGeometry" RadiusX="{Binding ElementName=border, Path=CornerRadius.TopLeft}" RadiusY="{Binding ElementName=border, Path=CornerRadius.TopLeft}" />
|
||||
</Viewbox.Clip>
|
||||
<Border x:Name="imageBorder" BorderBrush="#BAFFEF">
|
||||
<Image x:Name="image" RenderOptions.EdgeMode="Aliased" Margin="0,0,0,0" VerticalAlignment="Top" RenderTransformOrigin="0.5,0.5" Stretch="Uniform" MouseDown="Window_MouseDown" MouseMove="Window_MouseMove" MouseWheel="Window_MouseWheel" />
|
||||
</Border>
|
||||
</Viewbox>
|
||||
</Grid>
|
||||
<Border SizeChanged="Border_SizeChanged" x:Name="border" />
|
||||
<Viewbox SizeChanged="ViewBox_SizeChanged" x:Name="viewBox" StretchDirection="Both" Stretch="Uniform">
|
||||
<Viewbox.Clip>
|
||||
<RectangleGeometry x:Name="rectangleGeometry" RadiusX="{Binding ElementName=border, Path=CornerRadius.TopLeft}" RadiusY="{Binding ElementName=border, Path=CornerRadius.TopLeft}" />
|
||||
</Viewbox.Clip>
|
||||
<Border x:Name="imageBorder" BorderBrush="#BAFFEF">
|
||||
<Image x:Name="image" RenderOptions.EdgeMode="Aliased" Margin="0,0,0,0" VerticalAlignment="Top" RenderTransformOrigin="0.5,0.5" Stretch="Uniform" MouseDown="Window_MouseDown" MouseMove="Window_MouseMove" MouseWheel="Window_MouseWheel" />
|
||||
</Border>
|
||||
</Viewbox>
|
||||
</Grid>
|
||||
</busyIndicator:BusyMask>
|
||||
<WindowChrome.WindowChrome>
|
||||
<WindowChrome x:Name="tileBar" CaptionHeight="30" ResizeBorderThickness="{x:Static SystemParameters.WindowResizeBorderThickness}" />
|
||||
</WindowChrome.WindowChrome>
|
||||
|
||||
@@ -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<SettingElement> 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<PersistStateAttribute>();
|
||||
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<PersistStateAttribute>();
|
||||
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<PersistStateAttribute>();
|
||||
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<PersistStateAttribute>();
|
||||
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
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
<Window x:Class="DesktopMagic.WebPluginWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:busyIndicator="https://github.com/moh3ngolshani/BusyIndicator"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:local="clr-namespace:DesktopMagic"
|
||||
xmlns:wv2="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf"
|
||||
mc:Ignorable="d"
|
||||
Title="WebPluginWindow" Height="450" Width="800"
|
||||
Background="Transparent"
|
||||
WindowStyle="None"
|
||||
ShowInTaskbar="False"
|
||||
AllowsTransparency="True"
|
||||
LocationChanged="Window_LocationChanged"
|
||||
SizeChanged="Window_SizeChanged"
|
||||
Closing="Window_Closing"
|
||||
ContentRendered="Window_ContentRendered"
|
||||
x:Name="window">
|
||||
|
||||
<busyIndicator:BusyMask x:Name="busyMask" IsBusyAtStartup="True" Background="Transparent" BusyContent="" BusyContentMargin="-5" IndicatorType="Cogs">
|
||||
<Grid>
|
||||
<Rectangle x:Name="panel" Fill="#4CBAFFEF" Stroke="White" Margin="0,0,0,0" Visibility="Collapsed" />
|
||||
|
||||
<Border x:Name="border" Background="Transparent" CornerRadius="10">
|
||||
<Grid>
|
||||
<wv2:WebView2 x:Name="webView" DefaultBackgroundColor="Transparent" CoreWebView2InitializationCompleted="WebView_CoreWebView2InitializationCompleted" />
|
||||
<!--
|
||||
WORKAROUND: This rectangle exists to allow interaction with the WebView2 when the window is transparent
|
||||
https://github.com/MicrosoftEdge/WebView2Feedback/issues/997
|
||||
-->
|
||||
<Rectangle Fill="#01000000" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</busyIndicator:BusyMask>
|
||||
<WindowChrome.WindowChrome>
|
||||
<WindowChrome x:Name="tileBar" CaptionHeight="30" ResizeBorderThickness="{x:Static SystemParameters.WindowResizeBorderThickness}" />
|
||||
</WindowChrome.WindowChrome>
|
||||
</Window>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -3,4 +3,5 @@
|
||||
<BitmapImage x:Key="Icon" UriSource="/icon.ico" />
|
||||
<BitmapImage x:Key="ModioCogBlue" UriSource="modio-cog-blue.png" />
|
||||
<BitmapImage x:Key="ModioLogoBlueDark" UriSource="modio-logo-bluedark.png" />
|
||||
<BitmapImage x:Key="ModioLogoBlueLight" UriSource="modio-logo-bluelight.png" />
|
||||
</ResourceDictionary>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 70 KiB |
@@ -3,6 +3,9 @@
|
||||
xmlns:col="clr-namespace:System.Collections;assembly=mscorlib"
|
||||
xmlns:system="clr-namespace:System;assembly=mscorlib">
|
||||
|
||||
<system:String x:Key="developedBy">Entwickelt von Stone_Red</system:String>
|
||||
<system:String x:Key="reportBug">Bug Melden</system:String>
|
||||
<system:String x:Key="requestFeature">Feature Anfragen</system:String>
|
||||
<system:String x:Key="lineMode">Linie</system:String>
|
||||
<system:String x:Key="mirrorMode">Spiegeln</system:String>
|
||||
<system:String x:Key="folder">Ordner</system:String>
|
||||
@@ -15,13 +18,15 @@
|
||||
<system:String x:Key="enterModIoEmail">Mod.io Account E-Mail eingeben</system:String>
|
||||
<system:String x:Key="enterModIoAccessToken">Geben Sie Ihren mod.io Sicherheitscode ein</system:String>
|
||||
<system:String x:Key="toggleEditMode">Bearbeitungsmodus umschalten</system:String>
|
||||
<system:String x:Key="allPlugins">Alle Plugins</system:String>
|
||||
<system:String x:Key="installedPlugins">Installierte Plugins</system:String>
|
||||
<system:String x:Key="updatePlugins">Neu Laden</system:String>
|
||||
<system:String x:Key="searchAllPlugins">Alle Plugins Suchen...</system:String>
|
||||
<system:String x:Key="searchInstalledPlugins">Installierte Plugins Suchen...</system:String>
|
||||
<system:String x:Key="reloadPlugins">Plugins Neu Laden</system:String>
|
||||
<system:String x:Key="pluginsFolder">Plugins Ordner</system:String>
|
||||
<system:String x:Key="pluginManager">Plugins Manager</system:String>
|
||||
<system:String x:Key="createNewPlugin">Neues Plugin erstellen</system:String>
|
||||
<system:String x:Key="enterPluginName">Pluginnamen eingeben</system:String>
|
||||
<system:String x:Key="plugins">Plugins</system:String>
|
||||
<system:String x:Key="enablePluginToConfigure">Aktivieren Sie das Plugin, um seine Einstellungen zu konfigurieren.</system:String>
|
||||
<system:String x:Key="color">Farbe:</system:String>
|
||||
<system:String x:Key="default">Standard</system:String>
|
||||
<system:String x:Key="wantToCloseProgram">Wollen sie das Programm wirklich schließen?</system:String>
|
||||
@@ -31,14 +36,18 @@
|
||||
<system:String x:Key="date">Datum</system:String>
|
||||
<system:String x:Key="cpuUsage">CPU Auslastung</system:String>
|
||||
<system:String x:Key="musicVisualizer">Musik Visualisierer</system:String>
|
||||
<system:String x:Key="weather">Wetter</system:String>
|
||||
<system:String x:Key="amplifier">Signalverstärkung:</system:String>
|
||||
<system:String x:Key="newLayout">Neues Layout</system:String>
|
||||
<system:String x:Key="deleteLayout">Layout Löschen</system:String>
|
||||
<system:String x:Key="newTheme">Neues Theme</system:String>
|
||||
<system:String x:Key="deleteTheme">Theme Löschen</system:String>
|
||||
<system:String x:Key="changeTheme">Theme Ändern</system:String>
|
||||
<system:String x:Key="editTheme">Theme Bearbeiten</system:String>
|
||||
<system:String x:Key="themes">Themes</system:String>
|
||||
<system:String x:Key="ok">Ok</system:String>
|
||||
<system:String x:Key="cancel">Abbrechen</system:String>
|
||||
<system:String x:Key="confirmDeleteLayout">Möchten Sie dieses Layout wirklich löschen?</system:String>
|
||||
<system:String x:Key="confirmDeleteTheme">Möchten Sie dieses Theme wirklich löschen?</system:String>
|
||||
<system:String x:Key="enterLayoutName">Layoutnamen eingeben:</system:String>
|
||||
<system:String x:Key="layoutAlreadyExists">Layout existiert bereits!</system:String>
|
||||
<system:String x:Key="enterThemeName">Themenamen eingeben:</system:String>
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
xmlns:col="clr-namespace:System.Collections;assembly=mscorlib"
|
||||
xmlns:system="clr-namespace:System;assembly=mscorlib">
|
||||
|
||||
<system:String x:Key="developedBy">Developed by Stone_Red</system:String>
|
||||
<system:String x:Key="reportBug">Report Bug</system:String>
|
||||
<system:String x:Key="requestFeature">Request Feature</system:String>
|
||||
<system:String x:Key="lineMode">Line</system:String>
|
||||
<system:String x:Key="mirrorMode">Mirror</system:String>
|
||||
<system:String x:Key="folder">Folder</system:String>
|
||||
@@ -15,13 +18,15 @@
|
||||
<system:String x:Key="enterModIoEmail">Enter your mod.io account E-mail</system:String>
|
||||
<system:String x:Key="enterModIoAccessToken">Enter your mod.io security code</system:String>
|
||||
<system:String x:Key="toggleEditMode">Toggle Edit Mode</system:String>
|
||||
<system:String x:Key="allPlugins">All Plugins</system:String>
|
||||
<system:String x:Key="installedPlugins">Installed Plugins</system:String>
|
||||
<system:String x:Key="updatePlugins">Reload</system:String>
|
||||
<system:String x:Key="searchAllPlugins">Search All Plugins...</system:String>
|
||||
<system:String x:Key="searchInstalledPlugins">Search Installed Plugins...</system:String>
|
||||
<system:String x:Key="reloadPlugins">Reload Plugins</system:String>
|
||||
<system:String x:Key="pluginsFolder">Plugins Folder</system:String>
|
||||
<system:String x:Key="pluginManager">Plugins Manager</system:String>
|
||||
<system:String x:Key="createNewPlugin">Create New Plugin</system:String>
|
||||
<system:String x:Key="enterPluginName">Enter plugin name</system:String>
|
||||
<system:String x:Key="plugins">Plugins</system:String>
|
||||
<system:String x:Key="enablePluginToConfigure">Enable the plugin to configure its settings.</system:String>
|
||||
<system:String x:Key="color">Color:</system:String>
|
||||
<system:String x:Key="default">Default</system:String>
|
||||
<system:String x:Key="wantToCloseProgram">Do you really want to close the program?</system:String>
|
||||
@@ -31,14 +36,18 @@
|
||||
<system:String x:Key="date">Date</system:String>
|
||||
<system:String x:Key="cpuUsage">CPU Usage</system:String>
|
||||
<system:String x:Key="musicVisualizer">Music Visualizer</system:String>
|
||||
<system:String x:Key="weather">Weather</system:String>
|
||||
<system:String x:Key="amplifier">Signal Amplification:</system:String>
|
||||
<system:String x:Key="newLayout">New Layout</system:String>
|
||||
<system:String x:Key="deleteLayout">Delete Layout</system:String>
|
||||
<system:String x:Key="newTheme">New Theme</system:String>
|
||||
<system:String x:Key="deleteTheme">Delete Theme</system:String>
|
||||
<system:String x:Key="changeTheme">Change Theme</system:String>
|
||||
<system:String x:Key="editTheme">Edit Theme</system:String>
|
||||
<system:String x:Key="themes">Themes</system:String>
|
||||
<system:String x:Key="ok">Ok</system:String>
|
||||
<system:String x:Key="cancel">Cancel</system:String>
|
||||
<system:String x:Key="confirmDeleteLayout">Do you really want to delete this layout?</system:String>
|
||||
<system:String x:Key="confirmDeleteTheme">Do you really want to delete this theme?</system:String>
|
||||
<system:String x:Key="enterLayoutName">Enter layout name</system:String>
|
||||
<system:String x:Key="layoutAlreadyExists">Layout already exists!</system:String>
|
||||
<system:String x:Key="cannotDeleteLastLayout">Can't delete last layout!</system:String>
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
<ResourceDictionary
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
|
||||
xmlns:system="clr-namespace:System;assembly=System.Runtime">
|
||||
|
||||
<system:Double x:Key="ToggleButtonWidth">40</system:Double>
|
||||
<system:Double x:Key="ToggleButtonHeight">20</system:Double>
|
||||
<Thickness x:Key="ToggleSwitchBorderThemeThickness">1</Thickness>
|
||||
<Thickness x:Key="ToggleSwitchContentLeftMargin">0,0,8,0</Thickness>
|
||||
|
||||
<Style x:Key="ToggleSwitchContentLeftStyle" TargetType="{x:Type ui:ToggleSwitch}">
|
||||
<Setter Property="FocusVisualStyle" Value="{DynamicResource DefaultControlFocusVisualStyle}" />
|
||||
<Setter Property="Background" Value="{DynamicResource ToggleSwitchFillOn}" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource ToggleSwitchContentForeground}" />
|
||||
<Setter Property="BorderBrush" Value="Transparent" />
|
||||
<Setter Property="BorderThickness" Value="{StaticResource ToggleSwitchBorderThemeThickness}" />
|
||||
<Setter Property="Padding" Value="0" />
|
||||
<Setter Property="HorizontalAlignment" Value="Left" />
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Left" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
<Setter Property="FontSize" Value="{DynamicResource ControlContentThemeFontSize}" />
|
||||
<Setter Property="OverridesDefaultStyle" Value="True" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="{x:Type ui:ToggleSwitch}">
|
||||
<Grid Margin="{TemplateBinding Padding}" Background="Transparent">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<ContentPresenter
|
||||
x:Name="ContentPresenter"
|
||||
Grid.Column="0"
|
||||
Margin="{StaticResource ToggleSwitchContentLeftMargin}"
|
||||
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||
Content="{TemplateBinding Content}"
|
||||
ContentTemplate="{TemplateBinding ContentTemplate}"
|
||||
RecognizesAccessKey="True"
|
||||
TextElement.Foreground="{TemplateBinding Foreground}" />
|
||||
|
||||
<Grid
|
||||
Grid.Column="1"
|
||||
Width="{StaticResource ToggleButtonWidth}"
|
||||
Height="{StaticResource ToggleButtonHeight}">
|
||||
<Rectangle
|
||||
x:Name="ToggleRectangle"
|
||||
Width="{StaticResource ToggleButtonWidth}"
|
||||
Height="{StaticResource ToggleButtonHeight}"
|
||||
Fill="{DynamicResource ToggleSwitchFillOff}"
|
||||
RadiusX="10" RadiusY="10"
|
||||
Stroke="{DynamicResource ToggleSwitchStrokeOff}"
|
||||
StrokeThickness="1" />
|
||||
<Rectangle
|
||||
x:Name="ActiveToggleRectangle"
|
||||
Width="{StaticResource ToggleButtonWidth}"
|
||||
Height="{StaticResource ToggleButtonHeight}"
|
||||
Fill="{TemplateBinding Background}"
|
||||
Opacity="0.0"
|
||||
RadiusX="10" RadiusY="10" />
|
||||
<Rectangle
|
||||
x:Name="ToggleEllipse"
|
||||
Width="12" Height="12"
|
||||
Fill="{DynamicResource ToggleSwitchKnobFillOff}"
|
||||
RadiusX="7" RadiusY="7">
|
||||
<Rectangle.RenderTransform>
|
||||
<TranslateTransform X="-9" />
|
||||
</Rectangle.RenderTransform>
|
||||
</Rectangle>
|
||||
<Rectangle
|
||||
x:Name="ActiveToggleEllipse"
|
||||
Width="12" Height="12"
|
||||
Fill="{DynamicResource ToggleSwitchKnobFillOn}"
|
||||
Opacity="0.0"
|
||||
RadiusX="7" RadiusY="7">
|
||||
<Rectangle.RenderTransform>
|
||||
<TranslateTransform X="-9" />
|
||||
</Rectangle.RenderTransform>
|
||||
</Rectangle>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsChecked" Value="True">
|
||||
<Trigger.EnterActions>
|
||||
<BeginStoryboard Name="SlideRight">
|
||||
<Storyboard>
|
||||
<DoubleAnimation Storyboard.TargetName="ToggleRectangle" Storyboard.TargetProperty="Opacity" To="0.0" Duration="0:0:0.16" />
|
||||
<DoubleAnimation Storyboard.TargetName="ActiveToggleRectangle" Storyboard.TargetProperty="Opacity" To="1.0" Duration="0:0:0.16" />
|
||||
<DoubleAnimation Storyboard.TargetName="ToggleEllipse" Storyboard.TargetProperty="Opacity" To="0.0" Duration="0:0:0.16" />
|
||||
<DoubleAnimation Storyboard.TargetName="ActiveToggleEllipse" Storyboard.TargetProperty="Opacity" To="1.0" Duration="0:0:0.16" />
|
||||
<DoubleAnimation Storyboard.TargetName="ActiveToggleEllipse" Storyboard.TargetProperty="(Rectangle.RenderTransform).(TranslateTransform.X)" To="9" Duration="0:0:0.167" />
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</Trigger.EnterActions>
|
||||
<Trigger.ExitActions>
|
||||
<BeginStoryboard>
|
||||
<Storyboard>
|
||||
<DoubleAnimation Storyboard.TargetName="ToggleRectangle" Storyboard.TargetProperty="Opacity" To="1.0" Duration="0:0:0.16" />
|
||||
<DoubleAnimation Storyboard.TargetName="ActiveToggleRectangle" Storyboard.TargetProperty="Opacity" To="0.0" Duration="0:0:0.16" />
|
||||
<DoubleAnimation Storyboard.TargetName="ToggleEllipse" Storyboard.TargetProperty="Opacity" To="1.0" Duration="0:0:0.16" />
|
||||
<DoubleAnimation Storyboard.TargetName="ActiveToggleEllipse" Storyboard.TargetProperty="Opacity" To="0.0" Duration="0:0:0.16" />
|
||||
<DoubleAnimation Storyboard.TargetName="ActiveToggleEllipse" Storyboard.TargetProperty="(Rectangle.RenderTransform).(TranslateTransform.X)" To="-9" Duration="0:0:0.167" />
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</Trigger.ExitActions>
|
||||
</Trigger>
|
||||
|
||||
<MultiTrigger>
|
||||
<MultiTrigger.Conditions>
|
||||
<Condition Property="Content" Value="{x:Null}" />
|
||||
<Condition Property="IsChecked" Value="True" />
|
||||
</MultiTrigger.Conditions>
|
||||
<Setter TargetName="ContentPresenter" Property="Content" Value="{Binding OnContent, RelativeSource={RelativeSource TemplatedParent}}" />
|
||||
</MultiTrigger>
|
||||
<MultiTrigger>
|
||||
<MultiTrigger.Conditions>
|
||||
<Condition Property="Content" Value="{x:Null}" />
|
||||
<Condition Property="IsChecked" Value="False" />
|
||||
</MultiTrigger.Conditions>
|
||||
<Setter TargetName="ContentPresenter" Property="Content" Value="{Binding OffContent, RelativeSource={RelativeSource TemplatedParent}}" />
|
||||
</MultiTrigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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<SettingElement> settings = [];
|
||||
private Dictionary<string, JsonElement> 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
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dictionary storing plugin state. Key is the field/property name, value is the JsonElement.
|
||||
/// This is persisted per layout in settings.json.
|
||||
/// </summary>
|
||||
public Dictionary<string, JsonElement> State
|
||||
{
|
||||
get => state;
|
||||
set
|
||||
{
|
||||
if (state != value)
|
||||
{
|
||||
state = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool Enabled
|
||||
{
|
||||
get => enabled;
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace DesktopMagic.Api;
|
||||
|
||||
/// <summary>
|
||||
/// Provides an abstract base class for creating asynchronous plugins that can be integrated into the application, supporting periodic updates,
|
||||
/// rendering, and user interaction.
|
||||
/// </summary>
|
||||
/// <remarks>Derive from this class to implement custom plugin functionality. The class defines lifecycle methods
|
||||
/// such as <see cref="Plugin.Start"/>, <see cref="Plugin.Stop"/>, and <see cref="Main"/> 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.</remarks>
|
||||
public abstract class AsyncPlugin : Plugin
|
||||
{
|
||||
/// <summary>
|
||||
/// Occurs once when the plugin gets activated. Override for async initialization.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Token signaled when the host requests cancellation.</param>
|
||||
public virtual Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs once when the plugin gets deactivated. Override for async cleanup.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Token signaled when the host requests cancellation.</param>
|
||||
public virtual Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the <see cref="Plugin.UpdateInterval"/> elapses.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public abstract Task<Bitmap?> MainAsync(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// This method should not be called directly! Override and use <see cref="MainAsync"/> for asynchronous plugin operations.
|
||||
/// </summary>
|
||||
/// <remarks>This method only exists to fulfill base class requirements.</remarks>
|
||||
/// <returns>Calling it will always result in an <see cref="InvalidOperationException"/>.</returns>
|
||||
/// <exception cref="InvalidOperationException">Thrown if this method is called directly. Use MainAsync for plugin execution instead.</exception>
|
||||
public sealed override Bitmap? Main()
|
||||
{
|
||||
throw new InvalidOperationException($"AsyncPlugin.Main() should not be called directly. Override {nameof(MainAsync)} instead.");
|
||||
}
|
||||
}
|
||||
@@ -4,15 +4,17 @@
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<Company>Stone_Red</Company>
|
||||
<Product>Stone_Red</Product>
|
||||
<Version>0.0.0.5</Version>
|
||||
<Version>1.0.0.0</Version>
|
||||
<RepositoryUrl>https://github.com/Stone-Red-Code/DesktopMagic</RepositoryUrl>
|
||||
<PackageLicenseFile>LICENSE</PackageLicenseFile>
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
<AssemblyVersion>0.0.0.5</AssemblyVersion>
|
||||
<AssemblyVersion>1.0.0.0</AssemblyVersion>
|
||||
<PackageProjectUrl></PackageProjectUrl>
|
||||
<FileVersion>0.0.0.5</FileVersion>
|
||||
<FileVersion></FileVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<Platforms>AnyCPU;x64</Platforms>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<PackageIcon>Square44x44Logo.altform-lightunplated_targetsize-256.png</PackageIcon>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
@@ -36,6 +38,14 @@
|
||||
<Pack>True</Pack>
|
||||
<PackagePath>\</PackagePath>
|
||||
</None>
|
||||
<None Include="..\..\README.md">
|
||||
<Pack>True</Pack>
|
||||
<PackagePath>\</PackagePath>
|
||||
</None>
|
||||
<None Include="..\MsixPackaging\Images\Square44x44Logo.altform-lightunplated_targetsize-256.png">
|
||||
<Pack>True</Pack>
|
||||
<PackagePath>\</PackagePath>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -36,4 +36,27 @@ public interface IPluginData
|
||||
/// Updates the plugin window.
|
||||
/// </summary>
|
||||
void UpdateWindow();
|
||||
|
||||
/// <summary>
|
||||
/// Logs a message to the application log.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to log.</param>
|
||||
/// <param name="level">The log level (Info, Warning, Error).</param>
|
||||
void Log(string message, LogLevel level = LogLevel.Info);
|
||||
|
||||
/// <summary>
|
||||
/// Shows a message box to the user.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to display.</param>
|
||||
/// <param name="title">The title of the message box.</param>
|
||||
void ShowMessage(string message, string? title = null);
|
||||
|
||||
/// <summary>
|
||||
/// Saves the current state of fields and properties marked with <see cref="PersistStateAttribute"/>
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
void SaveState();
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace DesktopMagic.Api;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the severity level of a log message.
|
||||
/// </summary>
|
||||
public enum LogLevel
|
||||
{
|
||||
/// <summary>
|
||||
/// Informational message for general information.
|
||||
/// </summary>
|
||||
Info,
|
||||
|
||||
/// <summary>
|
||||
/// Warning message indicating a potential issue.
|
||||
/// </summary>
|
||||
Warning,
|
||||
|
||||
/// <summary>
|
||||
/// Error message indicating a failure or critical issue.
|
||||
/// </summary>
|
||||
Error
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace DesktopMagic.Api.Settings;
|
||||
namespace DesktopMagic.Api;
|
||||
|
||||
/// <summary>
|
||||
/// Mouse Buttons
|
||||
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
|
||||
namespace DesktopMagic.Api;
|
||||
|
||||
/// <summary>
|
||||
/// Marks a field or property to be automatically persisted between plugin sessions.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
|
||||
public sealed class PersistStateAttribute : Attribute
|
||||
{
|
||||
}
|
||||
@@ -7,8 +7,14 @@ using System.Drawing;
|
||||
namespace DesktopMagic.Api;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <remarks>Derive from this class to implement custom plugin functionality. The class defines lifecycle methods
|
||||
/// such as <see cref="Start"/>, <see cref="Stop"/>, and <see cref="Main"/> 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.</remarks>
|
||||
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)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the application's theme has changed.
|
||||
/// </summary>
|
||||
public virtual void OnThemeChanged()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when the settings have changed to allow derived classes to respond to configuration updates.
|
||||
/// </summary>
|
||||
public virtual void OnSettingsChanged()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
using System.Drawing;
|
||||
using System.Globalization;
|
||||
|
||||
namespace DesktopMagic.Api.Settings;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a color picker control.
|
||||
/// </summary>
|
||||
public sealed class ColorPicker : Setting
|
||||
{
|
||||
private Color _value;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the color value assigned to the <see cref="ColorPicker"/> element.
|
||||
/// </summary>
|
||||
public Color Value
|
||||
{
|
||||
get => _value;
|
||||
set
|
||||
{
|
||||
if (_value != value)
|
||||
{
|
||||
_value = value;
|
||||
ValueChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ColorPicker"/> class with the provided <paramref name="defaultColor"/>.
|
||||
/// </summary>
|
||||
/// <param name="defaultColor">The default color value.</param>
|
||||
public ColorPicker(Color defaultColor)
|
||||
{
|
||||
_value = defaultColor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ColorPicker"/> class with a default color of White.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
namespace DesktopMagic.Api.Settings;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a file selector control that allows the user to browse for a file.
|
||||
/// </summary>
|
||||
public sealed class FileSelector : Setting
|
||||
{
|
||||
private string _value = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the selected file path.
|
||||
/// </summary>
|
||||
public string Value
|
||||
{
|
||||
get => _value;
|
||||
set
|
||||
{
|
||||
if (_value != value)
|
||||
{
|
||||
_value = value;
|
||||
ValueChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the file filter for the file dialog (e.g., "Image Files|*.png;*.jpg|All Files|*.*").
|
||||
/// </summary>
|
||||
public string Filter { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the title of the file dialog.
|
||||
/// </summary>
|
||||
public string Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the file selector should select folders instead of files.
|
||||
/// </summary>
|
||||
public bool SelectFolder { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FileSelector"/> class.
|
||||
/// </summary>
|
||||
/// <param name="defaultPath">The default file path.</param>
|
||||
/// <param name="filter">The file filter for the file dialog.</param>
|
||||
/// <param name="title">The title of the file dialog.</param>
|
||||
/// <param name="selectFolder">If true, selects folders instead of files.</param>
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@
|
||||
<Identity
|
||||
Name="StoneRed.DesktopMagic"
|
||||
Publisher="CN=6D1FD759-1951-428A-9B03-964461C7DBF5"
|
||||
Version="1.2.0.0" />
|
||||
Version="1.3.0.0" />
|
||||
|
||||
<Properties>
|
||||
<DisplayName>Desktop Magic</DisplayName>
|
||||
|
||||
Reference in New Issue
Block a user