Add hot reload support for web plugins with file watcher and debounce

This commit is contained in:
Stone_Red
2026-06-27 19:07:53 +02:00
parent 6061efba02
commit 4999ae6e88
@@ -10,6 +10,7 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text.Json; using System.Text.Json;
using System.Threading.Tasks;
using System.Windows; using System.Windows;
using System.Windows.Interop; using System.Windows.Interop;
using System.Windows.Media; using System.Windows.Media;
@@ -24,6 +25,9 @@ public partial class WebPluginWindow : Window, IPluginWindow
private readonly PluginSettings settings; private readonly PluginSettings settings;
private bool isInitialized = false; private bool isInitialized = false;
private FileSystemWatcher? fileWatcher;
private System.Timers.Timer? reloadDebounceTimer;
private bool isReloading = false;
public bool IsRunning { get; private set; } = true; public bool IsRunning { get; private set; } = true;
public PluginMetadata PluginMetadata { get; private set; } public PluginMetadata PluginMetadata { get; private set; }
@@ -75,6 +79,11 @@ public partial class WebPluginWindow : Window, IPluginWindow
Height = settings.Size.Y; Height = settings.Size.Y;
PluginFolderPath = pluginFolderPath; PluginFolderPath = pluginFolderPath;
if (pluginMetadata.SupportsUnloading && !string.IsNullOrEmpty(pluginFolderPath))
{
InitializeHotReload();
}
} }
public void Exit() public void Exit()
@@ -232,6 +241,15 @@ public partial class WebPluginWindow : Window, IPluginWindow
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Stopping web plugin", source: "WebPlugin"); App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Stopping web plugin", source: "WebPlugin");
IsRunning = false; IsRunning = false;
if (fileWatcher != null)
{
fileWatcher.EnableRaisingEvents = false;
fileWatcher.Dispose();
}
reloadDebounceTimer?.Stop();
reloadDebounceTimer?.Dispose();
try try
{ {
if (isInitialized && webView.CoreWebView2 != null) if (isInitialized && webView.CoreWebView2 != null)
@@ -257,10 +275,21 @@ public partial class WebPluginWindow : Window, IPluginWindow
tileBar.CaptionHeight = ActualHeight - 10; tileBar.CaptionHeight = ActualHeight - 10;
} }
private bool firstLoad = true;
private void WebView_CoreWebView2InitializationCompleted(object sender, CoreWebView2InitializationCompletedEventArgs e) private void WebView_CoreWebView2InitializationCompleted(object sender, CoreWebView2InitializationCompletedEventArgs e)
{ {
webView.CoreWebView2.DOMContentLoaded += (_, _) => webView.CoreWebView2.DOMContentLoaded += (_, _) =>
{ {
if (firstLoad)
{
firstLoad = false;
if (fileWatcher != null)
{
fileWatcher.EnableRaisingEvents = true;
}
}
ThemeChanged(); ThemeChanged();
}; };
} }
@@ -320,8 +349,9 @@ public partial class WebPluginWindow : Window, IPluginWindow
{ {
_ = await webView.ExecuteScriptAsync($"window.desktopMagic?.onClick?.({JsonSerializer.Serialize(capturedId)})"); _ = await webView.ExecuteScriptAsync($"window.desktopMagic?.onClick?.({JsonSerializer.Serialize(capturedId)})");
} }
catch catch (Exception ex)
{ {
App.Logger.LogError($"\"{PluginMetadata.Name}\" - Failed to notify button click for \"{capturedId}\": {ex}", source: "WebPlugin");
} }
}); });
}; };
@@ -335,8 +365,9 @@ public partial class WebPluginWindow : Window, IPluginWindow
{ {
await NotifySettingChange(capturedId, setting); await NotifySettingChange(capturedId, setting);
} }
catch catch (Exception ex)
{ {
App.Logger.LogError($"\"{PluginMetadata.Name}\" - Failed to notify setting change for \"{capturedId}\": {ex}", source: "WebPlugin");
} }
}); });
}; };
@@ -391,6 +422,111 @@ public partial class WebPluginWindow : Window, IPluginWindow
"; ";
} }
private void InitializeHotReload()
{
try
{
fileWatcher = new FileSystemWatcher(PluginFolderPath)
{
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Size,
EnableRaisingEvents = false
};
fileWatcher.Changed += OnPluginFileChanged;
reloadDebounceTimer = new System.Timers.Timer(500)
{
AutoReset = false
};
reloadDebounceTimer.Elapsed += async (s, e) =>
{
await ReloadWebPlugin();
};
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Hot reload enabled", source: "WebPlugin");
}
catch (Exception ex)
{
App.Logger.LogWarn($"\"{PluginMetadata.Name}\" - Failed to initialize hot reload: {ex.Message}", source: "WebPlugin");
}
}
private void OnPluginFileChanged(object sender, FileSystemEventArgs e)
{
if (isReloading)
{
return;
}
string? fileName = e.Name;
if (string.IsNullOrEmpty(fileName))
{
return;
}
string ext = Path.GetExtension(fileName).ToLowerInvariant();
if (ext is not ".html" and not ".json" and not ".js" and not ".css")
{
return;
}
reloadDebounceTimer?.Stop();
reloadDebounceTimer?.Start();
}
private async Task ReloadWebPlugin()
{
if (isReloading || !IsRunning)
{
return;
}
isReloading = true;
try
{
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Reloading web plugin", source: "WebPlugin");
if (fileWatcher != null)
{
fileWatcher.EnableRaisingEvents = false;
}
LoadWebPluginSettings();
await Dispatcher.Invoke(async () =>
{
if (isInitialized && webView.CoreWebView2 != null)
{
_ = await webView.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync(GetSettingsBridgeScript());
busyMask.IsBusy = true;
string htmlPath = Path.Combine(PluginFolderPath, "main.html");
string htmlUri = new Uri(htmlPath).AbsoluteUri;
webView.CoreWebView2.Navigate(htmlUri);
busyMask.IsBusy = false;
}
});
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Web plugin reloaded", source: "WebPlugin");
}
catch (Exception ex)
{
App.Logger.LogError($"\"{PluginMetadata.Name}\" - Failed to reload web plugin: {ex}", source: "WebPlugin");
}
finally
{
isReloading = false;
if (fileWatcher != null && IsRunning)
{
fileWatcher.EnableRaisingEvents = true;
}
}
}
private async System.Threading.Tasks.Task NotifySettingChange(string id, Setting setting) private async System.Threading.Tasks.Task NotifySettingChange(string id, Setting setting)
{ {
object? value = GetSettingValue(setting); object? value = GetSettingValue(setting);
@@ -430,11 +566,9 @@ public partial class WebPluginWindow : Window, IPluginWindow
{ {
return type.ToLowerInvariant() switch return type.ToLowerInvariant() switch
{ {
"textbox" => new TextBox( "textbox" => new TextBox(element.TryGetProperty("default", out JsonElement tbDefault) ? tbDefault.GetString() ?? "" : ""),
element.TryGetProperty("default", out JsonElement tbDefault) ? tbDefault.GetString() ?? "" : ""),
"checkbox" => new CheckBox( "checkbox" => new CheckBox(element.TryGetProperty("default", out JsonElement cbDefault) && cbDefault.ValueKind == JsonValueKind.True),
element.TryGetProperty("default", out JsonElement cbDefault) && cbDefault.ValueKind == JsonValueKind.True),
"slider" => new Slider( "slider" => new Slider(
element.TryGetProperty("min", out JsonElement slMin) ? slMin.GetDouble() : 0, element.TryGetProperty("min", out JsonElement slMin) ? slMin.GetDouble() : 0,
@@ -453,8 +587,7 @@ public partial class WebPluginWindow : Window, IPluginWindow
? ParseHexColor(cpDefault.GetString() ?? "#FFFFFFFF") ? ParseHexColor(cpDefault.GetString() ?? "#FFFFFFFF")
: System.Drawing.Color.White), : System.Drawing.Color.White),
"button" => new Button( "button" => new Button(element.TryGetProperty("default", out JsonElement btnDefault) ? btnDefault.GetString() ?? "" : ""),
element.TryGetProperty("default", out JsonElement btnDefault) ? btnDefault.GetString() ?? "" : ""),
"label" => new Label( "label" => new Label(
element.TryGetProperty("default", out JsonElement lblDefault) ? lblDefault.GetString() ?? "" : "", element.TryGetProperty("default", out JsonElement lblDefault) ? lblDefault.GetString() ?? "" : "",
@@ -502,7 +635,7 @@ public partial class WebPluginWindow : Window, IPluginWindow
return System.Drawing.Color.White; return System.Drawing.Color.White;
} }
if (hex.StartsWith("#")) if (hex.StartsWith('#'))
{ {
hex = hex[1..]; hex = hex[1..];
} }