From 6061efba02a0f83bc881a4357023b5c8aeb6a111 Mon Sep 17 00:00:00 2001
From: Stone_Red <56473591+Stone-Red-Code@users.noreply.github.com>
Date: Sat, 27 Jun 2026 15:53:27 +0200
Subject: [PATCH] Add web plugin creation flow and settings bridge to
WebPluginWindow
---
.../Dialogs/CreatePluginDialog.xaml | 7 +-
.../Dialogs/CreatePluginDialog.xaml.cs | 11 +-
.../Plugins/PluginManager.xaml.cs | 52 ++++
.../Plugins/WebPluginWindow.xaml.cs | 272 +++++++++++++++++-
4 files changed, 337 insertions(+), 5 deletions(-)
diff --git a/src/DesktopMagic/Dialogs/CreatePluginDialog.xaml b/src/DesktopMagic/Dialogs/CreatePluginDialog.xaml
index c5000bc..637cb65 100644
--- a/src/DesktopMagic/Dialogs/CreatePluginDialog.xaml
+++ b/src/DesktopMagic/Dialogs/CreatePluginDialog.xaml
@@ -16,9 +16,9 @@
WindowCornerPreference="Round"
WindowBackdropType="Tabbed"
Height="230"
- Width="300"
+ Width="320"
MinHeight="160"
- MinWidth="300"
+ MinWidth="320"
WindowStartupLocation="CenterOwner"
ExtendsContentIntoTitleBar="True">
@@ -48,7 +48,8 @@
-
+
+
diff --git a/src/DesktopMagic/Dialogs/CreatePluginDialog.xaml.cs b/src/DesktopMagic/Dialogs/CreatePluginDialog.xaml.cs
index da4adb6..9669865 100644
--- a/src/DesktopMagic/Dialogs/CreatePluginDialog.xaml.cs
+++ b/src/DesktopMagic/Dialogs/CreatePluginDialog.xaml.cs
@@ -10,6 +10,8 @@ public partial class CreatePluginDialog : Wpf.Ui.Controls.FluentWindow
set => textBox.Text = value;
}
+ public bool IsWebPlugin { get; private set; }
+
public CreatePluginDialog()
{
InitializeComponent();
@@ -17,8 +19,15 @@ public partial class CreatePluginDialog : Wpf.Ui.Controls.FluentWindow
Resources.MergedDictionaries.Add(App.LanguageDictionary);
}
- private void OkButton_Click(object sender, RoutedEventArgs e)
+ private void DotNetButton_Click(object sender, RoutedEventArgs e)
{
+ IsWebPlugin = false;
+ DialogResult = true;
+ }
+
+ private void WebPluginButton_Click(object sender, RoutedEventArgs e)
+ {
+ IsWebPlugin = true;
DialogResult = true;
}
diff --git a/src/DesktopMagic/Plugins/PluginManager.xaml.cs b/src/DesktopMagic/Plugins/PluginManager.xaml.cs
index bb8f83f..eedca7c 100644
--- a/src/DesktopMagic/Plugins/PluginManager.xaml.cs
+++ b/src/DesktopMagic/Plugins/PluginManager.xaml.cs
@@ -486,6 +486,15 @@ public partial class PluginManager : Page
string pluginMetadataPath = Path.Combine(pluginPath, "metadata.json");
await File.WriteAllTextAsync(pluginMetadataPath, JsonSerializer.Serialize(pluginMetadata));
+ if (inputDialog.IsWebPlugin)
+ {
+ await CreateNewWebPlugin(pluginPath, pluginName);
+ changed = true;
+ await InitializePluginManager();
+ pluginManagerDataContext.IsLoading = false;
+ return;
+ }
+
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);
@@ -608,6 +617,49 @@ public class {pluginSafeName}Plugin : Plugin
pluginManagerDataContext.IsLoading = false;
}
+ private async Task CreateNewWebPlugin(string pluginPath, string pluginName)
+ {
+ App.Logger.LogInfo($"Creating web plugin at: {pluginPath}", source: "PluginManager");
+
+ string mainHtml = $@"
+
+
+
+
+
+
+ {pluginName}
+
+
+";
+
+ var settings = new List>
+ {
+ new() { ["id"] = "message", ["name"] = "Greeting Message", ["type"] = "textbox", ["default"] = "Hello, World!" },
+ new() { ["id"] = "bold", ["name"] = "Bold Text", ["type"] = "checkbox", ["default"] = true },
+ new() { ["id"] = "color", ["name"] = "Text Color", ["type"] = "colorpicker", ["default"] = "#FF5722" }
+ };
+
+ string settingsJson = JsonSerializer.Serialize(settings, new JsonSerializerOptions { WriteIndented = true });
+ await File.WriteAllTextAsync(Path.Combine(pluginPath, "settings.json"), settingsJson);
+ await File.WriteAllTextAsync(Path.Combine(pluginPath, "main.html"), mainHtml);
+
+ App.Logger.LogInfo($"Successfully created web plugin: {pluginName}", source: "PluginManager");
+ }
+
private async void LogInButton_Click(object sender, RoutedEventArgs e)
{
if (pluginManagerDataContext.IsAuthenticated)
diff --git a/src/DesktopMagic/Plugins/WebPluginWindow.xaml.cs b/src/DesktopMagic/Plugins/WebPluginWindow.xaml.cs
index ff92680..1690d5c 100644
--- a/src/DesktopMagic/Plugins/WebPluginWindow.xaml.cs
+++ b/src/DesktopMagic/Plugins/WebPluginWindow.xaml.cs
@@ -1,3 +1,4 @@
+using DesktopMagic.Api.Settings;
using DesktopMagic.Helpers;
using DesktopMagic.Plugins;
using DesktopMagic.Settings;
@@ -5,7 +6,10 @@ using DesktopMagic.Settings;
using Microsoft.Web.WebView2.Core;
using System;
+using System.Collections.Generic;
using System.IO;
+using System.Linq;
+using System.Text.Json;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Media;
@@ -193,6 +197,8 @@ public partial class WebPluginWindow : Window, IPluginWindow
try
{
+ LoadWebPluginSettings();
+
string userDataFolder = Path.Combine(Path.GetTempPath(), "DesktopMagic", "WebView2", PluginMetadata.Id.ToString());
CoreWebView2Environment environment = await CoreWebView2Environment.CreateAsync(null, userDataFolder);
await webView.EnsureCoreWebView2Async(environment);
@@ -202,6 +208,8 @@ public partial class WebPluginWindow : Window, IPluginWindow
webView.CoreWebView2.Settings.IsStatusBarEnabled = false;
webView.CoreWebView2.Settings.AreDefaultScriptDialogsEnabled = true;
+ _ = await webView.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync(GetSettingsBridgeScript());
+
string htmlUri = new Uri(htmlPath).AbsoluteUri;
webView.Source = new Uri(htmlUri);
@@ -251,6 +259,268 @@ public partial class WebPluginWindow : Window, IPluginWindow
private void WebView_CoreWebView2InitializationCompleted(object sender, CoreWebView2InitializationCompletedEventArgs e)
{
- webView.CoreWebView2.DOMContentLoaded += (_, _) => ThemeChanged();
+ webView.CoreWebView2.DOMContentLoaded += (_, _) =>
+ {
+ ThemeChanged();
+ };
+ }
+
+ private void LoadWebPluginSettings()
+ {
+ string settingsPath = Path.Combine(PluginFolderPath, "settings.json");
+ if (!File.Exists(settingsPath))
+ {
+ return;
+ }
+
+ try
+ {
+ string json = File.ReadAllText(settingsPath);
+ using JsonDocument doc = JsonDocument.Parse(json);
+ JsonElement root = doc.RootElement;
+
+ if (root.ValueKind != JsonValueKind.Array)
+ {
+ App.Logger.LogWarn($"\"{PluginMetadata.Name}\" - settings.json must be a JSON array", source: "WebPlugin");
+ return;
+ }
+
+ List settingElements = [];
+ int orderIndex = 0;
+
+ foreach (JsonElement element in root.EnumerateArray())
+ {
+ string id = element.GetProperty("id").GetString() ?? $"setting-{orderIndex}";
+ string name = element.TryGetProperty("name", out JsonElement nameEl) ? nameEl.GetString() ?? id : id;
+ string type = element.TryGetProperty("type", out JsonElement typeEl) ? typeEl.GetString() ?? "textbox" : "textbox";
+
+ Setting? setting = CreateSetting(type, element);
+ if (setting is null)
+ {
+ App.Logger.LogWarn($"\"{PluginMetadata.Name}\" - Unknown setting type \"{type}\" for \"{id}\"", source: "WebPlugin");
+ continue;
+ }
+
+ SettingElement settingElement = new(setting, id, name, orderIndex);
+
+ if (settings.Settings.Exists(e => e.Id == id))
+ {
+ SettingElement saved = settings.Settings.First(e => e.Id == id);
+ settingElement.JsonValue = saved.JsonValue;
+ }
+
+ string capturedId = id;
+ if (setting is Button button)
+ {
+ button.OnClick += () =>
+ {
+ _ = webView.Dispatcher.InvokeAsync(async () =>
+ {
+ try
+ {
+ _ = await webView.ExecuteScriptAsync($"window.desktopMagic?.onClick?.({JsonSerializer.Serialize(capturedId)})");
+ }
+ catch
+ {
+ }
+ });
+ };
+ }
+
+ setting.OnValueChanged += () =>
+ {
+ _ = webView.Dispatcher.InvokeAsync(async () =>
+ {
+ try
+ {
+ await NotifySettingChange(capturedId, setting);
+ }
+ catch
+ {
+ }
+ });
+ };
+
+ settingElements.Add(settingElement);
+ orderIndex++;
+ }
+
+ settings.Settings = [.. settingElements.OrderBy(x => x.OrderIndex)];
+ }
+ catch (Exception ex)
+ {
+ App.Logger.LogWarn($"\"{PluginMetadata.Name}\" - Failed to load settings from settings.json: {ex.Message}", source: "WebPlugin");
+ }
+ }
+
+ private string GetSettingsBridgeScript()
+ {
+ string settingsJson = SerializeSettingsToJson();
+
+ return $@"
+ (function() {{
+ window.desktopMagic = {{}};
+ window.desktopMagic._settings = {settingsJson};
+
+ window.desktopMagic.getSettings = function() {{
+ return JSON.parse(JSON.stringify(window.desktopMagic._settings));
+ }};
+
+ window.desktopMagic.getSetting = function(id) {{
+ return window.desktopMagic._settings ? window.desktopMagic._settings[id] : undefined;
+ }};
+
+ window.desktopMagic.onSettingChanged = null;
+ window.desktopMagic.onButtonClick = null;
+
+ window.desktopMagic.dispatchSettingChanged = function(id, value) {{
+ if (window.desktopMagic._settings) {{
+ window.desktopMagic._settings[id] = value;
+ }}
+ if (typeof window.desktopMagic.onSettingChanged === 'function') {{
+ window.desktopMagic.onSettingChanged(id, value);
+ }}
+ }};
+
+ window.desktopMagic.onClick = function(id) {{
+ if (typeof window.desktopMagic.onButtonClick === 'function') {{
+ window.desktopMagic.onButtonClick(id);
+ }}
+ }};
+ }})();
+ ";
+ }
+
+ private async System.Threading.Tasks.Task NotifySettingChange(string id, Setting setting)
+ {
+ object? value = GetSettingValue(setting);
+ string serializedValue = JsonSerializer.Serialize(value);
+ string serializedId = JsonSerializer.Serialize(id);
+ _ = await webView.ExecuteScriptAsync($"window.desktopMagic?.dispatchSettingChanged?.({serializedId}, {serializedValue})");
+ }
+
+ private string SerializeSettingsToJson()
+ {
+ Dictionary dict = [];
+ foreach (SettingElement element in settings.Settings)
+ {
+ dict[element.Id] = GetSettingValue(element.Input);
+ }
+ return JsonSerializer.Serialize(dict);
+ }
+
+ private static object? GetSettingValue(Setting setting)
+ {
+ return setting switch
+ {
+ TextBox tb => tb.Value,
+ CheckBox cb => cb.Value,
+ Slider sl => sl.Value,
+ IntegerUpDown iud => iud.Value,
+ ComboBox cb => cb.Value,
+ ColorPicker cp => $"#{cp.Value.A:X2}{cp.Value.R:X2}{cp.Value.G:X2}{cp.Value.B:X2}",
+ Button btn => btn.Value,
+ Label lbl => lbl.Value,
+ FileSelector fs => fs.Value,
+ _ => null
+ };
+ }
+
+ private static Setting? CreateSetting(string type, JsonElement element)
+ {
+ return type.ToLowerInvariant() switch
+ {
+ "textbox" => new TextBox(
+ element.TryGetProperty("default", out JsonElement tbDefault) ? tbDefault.GetString() ?? "" : ""),
+
+ "checkbox" => new CheckBox(
+ element.TryGetProperty("default", out JsonElement cbDefault) && cbDefault.ValueKind == JsonValueKind.True),
+
+ "slider" => new Slider(
+ element.TryGetProperty("min", out JsonElement slMin) ? slMin.GetDouble() : 0,
+ element.TryGetProperty("max", out JsonElement slMax) ? slMax.GetDouble() : 100,
+ element.TryGetProperty("default", out JsonElement slDefault) ? slDefault.GetDouble() : 0),
+
+ "integer" => new IntegerUpDown(
+ element.TryGetProperty("min", out JsonElement intMin) ? intMin.GetInt32() : 0,
+ element.TryGetProperty("max", out JsonElement intMax) ? intMax.GetInt32() : 100,
+ element.TryGetProperty("default", out JsonElement intDefault) ? intDefault.GetInt32() : 0),
+
+ "combobox" => CreateComboBox(element),
+
+ "colorpicker" => new ColorPicker(
+ element.TryGetProperty("default", out JsonElement cpDefault)
+ ? ParseHexColor(cpDefault.GetString() ?? "#FFFFFFFF")
+ : System.Drawing.Color.White),
+
+ "button" => new Button(
+ element.TryGetProperty("default", out JsonElement btnDefault) ? btnDefault.GetString() ?? "" : ""),
+
+ "label" => new Label(
+ element.TryGetProperty("default", out JsonElement lblDefault) ? lblDefault.GetString() ?? "" : "",
+ element.TryGetProperty("bold", out JsonElement boldEl) && boldEl.ValueKind == JsonValueKind.True),
+
+ "file" => new FileSelector(
+ element.TryGetProperty("default", out JsonElement fsDefault) ? fsDefault.GetString() ?? "" : "",
+ element.TryGetProperty("filter", out JsonElement filterEl) ? filterEl.GetString() ?? "All Files|*.*" : "All Files|*.*",
+ element.TryGetProperty("title", out JsonElement titleEl) ? titleEl.GetString() ?? "Select File" : "Select File",
+ element.TryGetProperty("selectFolder", out JsonElement sfEl) && sfEl.ValueKind == JsonValueKind.True),
+
+ _ => null
+ };
+ }
+
+ private static ComboBox CreateComboBox(JsonElement element)
+ {
+ List items = [];
+ if (element.TryGetProperty("items", out JsonElement itemsEl) && itemsEl.ValueKind == JsonValueKind.Array)
+ {
+ foreach (JsonElement item in itemsEl.EnumerateArray())
+ {
+ items.Add(item.GetString() ?? "");
+ }
+ }
+
+ ComboBox comboBox = new([.. items]);
+
+ if (element.TryGetProperty("default", out JsonElement cbDefault))
+ {
+ string defaultVal = cbDefault.GetString() ?? "";
+ if (!string.IsNullOrEmpty(defaultVal) && items.Contains(defaultVal))
+ {
+ comboBox.Value = defaultVal;
+ }
+ }
+
+ return comboBox;
+ }
+
+ private static System.Drawing.Color ParseHexColor(string hex)
+ {
+ if (string.IsNullOrEmpty(hex))
+ {
+ return System.Drawing.Color.White;
+ }
+
+ if (hex.StartsWith("#"))
+ {
+ hex = hex[1..];
+ }
+
+ if (hex.Length == 6)
+ {
+ hex = "FF" + hex;
+ }
+
+ if (hex.Length == 8 &&
+ byte.TryParse(hex[..2], System.Globalization.NumberStyles.HexNumber, null, out byte a) &&
+ byte.TryParse(hex[2..4], System.Globalization.NumberStyles.HexNumber, null, out byte r) &&
+ byte.TryParse(hex[4..6], System.Globalization.NumberStyles.HexNumber, null, out byte g) &&
+ byte.TryParse(hex[6..8], System.Globalization.NumberStyles.HexNumber, null, out byte b))
+ {
+ return System.Drawing.Color.FromArgb(a, r, g, b);
+ }
+
+ return System.Drawing.Color.White;
}
}