From c6e3b0f52fad95580d8a03e2398ef1ac3cd136b4 Mon Sep 17 00:00:00 2001
From: CialloKing <147636459+CialloKing@users.noreply.github.com>
Date: Sun, 26 Jul 2026 00:10:47 +0800
Subject: [PATCH 01/24] =?UTF-8?q?=E9=9B=86=E6=88=90=20ba-click-fx=20?=
=?UTF-8?q?=E7=89=B9=E6=95=88=E6=B8=B2=E6=9F=93=E5=99=A8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.github/workflows/pr-build-check.yml | 9 +
.github/workflows/publish.yml | 15 +-
src/AssemblyInfo.cs | 2 +
src/BASpark.csproj | 16 +-
src/MainWindow.xaml | 4 +-
src/MainWindow.xaml.cs | 556 ++++++++++--
src/OverlayManager.cs | 110 ++-
src/Web/fx-adapter.js | 647 ++++++++++++++
src/Web/index.html | 789 +----------------
src/Web/index.legacy.html | 793 ++++++++++++++++++
src/Web/vendor/ba-click-fx.iife.js | 284 +++++++
src/WebRendererDocumentBuilder.cs | 61 ++
src/WebViewProcessFailurePolicy.cs | 27 +
src/WebViewUnresponsiveTracker.cs | 38 +
src/licenses/ba-click-fx/LICENSE | 21 +
.../ba-click-fx/THIRD_PARTY_NOTICES.md | 40 +
src/licenses/ba-click-fx/VERSION.txt | 5 +
.../PackagedRendererResourcesTests.cs | 68 ++
.../WebRendererDocumentBuilderTests.cs | 89 ++
.../WebViewProcessFailurePolicyTests.cs | 39 +
.../WebViewUnresponsiveTrackerTests.cs | 56 ++
tests/Web/fx-adapter.test.mjs | 292 +++++++
tests/Web/vendor-contract.test.mjs | 66 ++
23 files changed, 3167 insertions(+), 860 deletions(-)
create mode 100644 src/Web/fx-adapter.js
create mode 100644 src/Web/index.legacy.html
create mode 100644 src/Web/vendor/ba-click-fx.iife.js
create mode 100644 src/WebRendererDocumentBuilder.cs
create mode 100644 src/WebViewProcessFailurePolicy.cs
create mode 100644 src/WebViewUnresponsiveTracker.cs
create mode 100644 src/licenses/ba-click-fx/LICENSE
create mode 100644 src/licenses/ba-click-fx/THIRD_PARTY_NOTICES.md
create mode 100644 src/licenses/ba-click-fx/VERSION.txt
create mode 100644 tests/BASpark.Tests/PackagedRendererResourcesTests.cs
create mode 100644 tests/BASpark.Tests/WebRendererDocumentBuilderTests.cs
create mode 100644 tests/BASpark.Tests/WebViewProcessFailurePolicyTests.cs
create mode 100644 tests/BASpark.Tests/WebViewUnresponsiveTrackerTests.cs
create mode 100644 tests/Web/fx-adapter.test.mjs
create mode 100644 tests/Web/vendor-contract.test.mjs
diff --git a/.github/workflows/pr-build-check.yml b/.github/workflows/pr-build-check.yml
index 39894e6..bf7cb9d 100644
--- a/.github/workflows/pr-build-check.yml
+++ b/.github/workflows/pr-build-check.yml
@@ -20,6 +20,15 @@ jobs:
with:
dotnet-version: '8.0.x'
+ - name: Setup Node.js 20
+ uses: actions/setup-node@v4
+ with:
+ node-version: '20'
+
+ - name: Run Web tests
+ shell: pwsh
+ run: node --test tests/Web/fx-adapter.test.mjs tests/Web/vendor-contract.test.mjs
+
- name: Run tests
shell: pwsh
run: dotnet test BASpark.sln -c Release --verbosity normal
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index aa5a75b..da66f7e 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -23,6 +23,19 @@ jobs:
with:
dotnet-version: '8.0.x'
+ - name: Setup Node.js 20
+ uses: actions/setup-node@v4
+ with:
+ node-version: '20'
+
+ - name: Run Web tests
+ shell: pwsh
+ run: node --test tests/Web/fx-adapter.test.mjs tests/Web/vendor-contract.test.mjs
+
+ - name: Run .NET tests
+ shell: pwsh
+ run: dotnet test BASpark.sln -c Release --verbosity normal
+
- name: Build Packaged Files
shell: pwsh
run: |
@@ -61,4 +74,4 @@ jobs:
draft: false
prerelease: false
env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
\ No newline at end of file
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/src/AssemblyInfo.cs b/src/AssemblyInfo.cs
index cc29e7f..4b843c8 100644
--- a/src/AssemblyInfo.cs
+++ b/src/AssemblyInfo.cs
@@ -1,5 +1,7 @@
+using System.Runtime.CompilerServices;
using System.Windows;
+[assembly: InternalsVisibleTo("BASpark.Tests")]
[assembly:ThemeInfo(
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
//(used if a resource is not found in the page,
diff --git a/src/BASpark.csproj b/src/BASpark.csproj
index 2e05bcd..f7c44ec 100644
--- a/src/BASpark.csproj
+++ b/src/BASpark.csproj
@@ -34,7 +34,21 @@
+
+
+
+
+
+
+
+
-
\ No newline at end of file
+
diff --git a/src/MainWindow.xaml b/src/MainWindow.xaml
index e89f783..13de1b6 100644
--- a/src/MainWindow.xaml
+++ b/src/MainWindow.xaml
@@ -11,7 +11,7 @@
ResizeMode="NoResize"
Left="0" Top="0"
IsHitTestVisible="False">
-
+
-
\ No newline at end of file
+
diff --git a/src/MainWindow.xaml.cs b/src/MainWindow.xaml.cs
index 14ca5cb..4a8de1d 100644
--- a/src/MainWindow.xaml.cs
+++ b/src/MainWindow.xaml.cs
@@ -4,6 +4,7 @@
using System.Globalization;
using System.Linq;
using System.Runtime.InteropServices;
+using System.Text.Json;
using System.Windows;
using System.Windows.Forms;
using System.Windows.Interop;
@@ -80,11 +81,20 @@ private struct RECT
private bool? _lastReportedAlwaysTrail;
private const string InputModeMouse = "mouse";
private const string InputModeTouch = "touch";
+ private const string PrimaryRendererResourcePath = "Web/index.html";
+ private const string LegacyRendererResourcePath = "Web/index.legacy.html";
+ private const string RendererVendorResourcePath = "Web/vendor/ba-click-fx.iife.js";
+ private const string RendererAdapterResourcePath = "Web/fx-adapter.js";
private System.Windows.Threading.DispatcherTimer? _topmostTimer;
+ private System.Windows.Threading.DispatcherTimer? _rendererReadyTimeoutTimer;
+ private EventHandler? _navigationStartingHandler;
private EventHandler? _navigationCompletedHandler;
private EventHandler? _processFailedHandler;
+ private EventHandler? _webMessageReceivedHandler;
private CoreWebView2? _coreWebView;
+ private ulong? _currentNavigationId;
+ private string? _rendererGeneration;
private WinEventDelegate? _winEventDelegate;
private IntPtr _winEventHook = IntPtr.Zero;
private long _lastEnsureTopmostTicks;
@@ -94,6 +104,11 @@ private struct RECT
private bool _hiddenForExternalScreenshotCapture;
private bool _hiddenByEnvironmentSuppression;
private bool _overlayRuntimePaused;
+ private bool _rendererReady;
+ private bool _usingLegacyRenderer;
+ private bool _legacyFallbackAttempted;
+ private bool _processRecoveryPending;
+ private readonly WebViewUnresponsiveTracker _unresponsiveTracker = new();
private delegate void WinEventDelegate(
IntPtr hWinEventHook,
@@ -195,7 +210,9 @@ private void SafeEnsureTopmost()
public void UpdateColor(string color)
{
- ExecuteScript($"if(window.updateColor) window.updateColor('{color}');");
+ // JSON serialization keeps registry-backed color text from becoming executable script.
+ string colorJson = JsonSerializer.Serialize(color);
+ ExecuteScript($"if(window.updateColor) window.updateColor({colorJson});");
}
public void UpdateEffectSettings(double scale, double opacity, double trailSpeed, double clickSpeed)
@@ -387,7 +404,10 @@ private async System.Threading.Tasks.Task InitWebView()
try
{
var env = await WebView2EnvironmentHolder.GetOrCreateAsync().ConfigureAwait(true);
- if (_isClosing) return;
+ if (_isClosing)
+ {
+ return;
+ }
if (webView.CoreWebView2 == null)
{
@@ -405,55 +425,325 @@ private async System.Threading.Tasks.Task InitWebView()
}
}
- if (_isClosing || !TryGetCoreWebView2(out CoreWebView2? coreWebView)) return;
+ if (_isClosing || !TryGetCoreWebView2(out CoreWebView2? coreWebView))
+ {
+ return;
+ }
+ DetachCoreWebViewEvents();
_coreWebView = coreWebView;
coreWebView.Settings.IsZoomControlEnabled = false;
coreWebView.Settings.AreDefaultContextMenusEnabled = false;
coreWebView.Settings.IsStatusBarEnabled = false;
- if (_processFailedHandler == null)
+ _processFailedHandler = OnWebViewProcessFailed;
+ _navigationStartingHandler = OnNavigationStarting;
+ _navigationCompletedHandler = OnNavigationCompleted;
+ _webMessageReceivedHandler = OnWebMessageReceived;
+ coreWebView.ProcessFailed += _processFailedHandler;
+ coreWebView.NavigationStarting += _navigationStartingHandler;
+ coreWebView.NavigationCompleted += _navigationCompletedHandler;
+ coreWebView.WebMessageReceived += _webMessageReceivedHandler;
+
+ NavigateCurrentRenderer(coreWebView);
+ }
+ catch (Exception ex) when (IsExpectedWebViewShutdownException(ex))
+ {
+ }
+ catch (Exception ex)
+ {
+ System.Windows.MessageBox.Show(Localization.Format("WebView2_InitFailed", ex.Message));
+ }
+ }
+
+ private void NavigateCurrentRenderer(CoreWebView2 coreWebView)
+ {
+ StopRendererReadyTimeout();
+ _rendererReady = _usingLegacyRenderer;
+
+ if (_usingLegacyRenderer)
+ {
+ _rendererGeneration = null;
+ string legacyHtml = ReadResourceText(LegacyRendererResourcePath);
+ NavigateHtml(coreWebView, legacyHtml);
+ return;
+ }
+
+ _rendererGeneration = null;
+ try
+ {
+ string rendererGeneration = Guid.NewGuid().ToString("N");
+ string rendererHtml = BuildPrimaryRendererHtml(rendererGeneration);
+ _rendererGeneration = rendererGeneration;
+ NavigateHtml(coreWebView, rendererHtml);
+ }
+ catch (Exception ex) when (!IsExpectedWebViewShutdownException(ex))
+ {
+ AppLogger.Error(
+ $"Failed to prepare BA click renderer on '{_screenDeviceName}'.",
+ ex);
+ FallbackToLegacyRenderer("primary renderer resource preparation failed");
+ return;
+ }
+
+ // A renderer that cannot announce readiness must not leave an invisible overlay active.
+ if (!_rendererReady)
+ {
+ StartRendererReadyTimeout();
+ }
+ }
+
+ private static string BuildPrimaryRendererHtml(string rendererGeneration)
+ {
+ string generationJson = JsonSerializer.Serialize(rendererGeneration);
+ string adapterScript =
+ $"window.__basparkRendererGeneration = {generationJson};\n" +
+ ReadResourceText(RendererAdapterResourcePath);
+
+ return WebRendererDocumentBuilder.Build(
+ ReadResourceText(PrimaryRendererResourcePath),
+ ReadResourceText(RendererVendorResourcePath),
+ adapterScript);
+ }
+
+ private static string ReadResourceText(string resourcePath)
+ {
+ string assemblyName = typeof(MainWindow).Assembly.GetName().Name ?? "BASpark";
+ // Explicit component lookup also works when a diagnostic host references BASpark.
+ var resourceUri = new Uri(
+ $"pack://application:,,,/{assemblyName};component/{resourcePath}",
+ UriKind.Absolute);
+ var streamInfo = System.Windows.Application.GetResourceStream(resourceUri);
+ if (streamInfo == null)
+ {
+ throw new InvalidOperationException($"Embedded renderer resource '{resourcePath}' was not found.");
+ }
+
+ using var reader = new System.IO.StreamReader(streamInfo.Stream);
+ return reader.ReadToEnd();
+ }
+
+ private void NavigateHtml(CoreWebView2 coreWebView, string html)
+ {
+ // NavigationStarting assigns the authoritative ID before any matching completion event.
+ _currentNavigationId = null;
+ coreWebView.NavigateToString(html);
+ }
+
+ private void OnNavigationStarting(object? sender, CoreWebView2NavigationStartingEventArgs e)
+ {
+ if (_isClosing || !ReferenceEquals(sender, _coreWebView))
+ {
+ return;
+ }
+
+ _currentNavigationId = e.NavigationId;
+ }
+
+ private void OnNavigationCompleted(object? sender, CoreWebView2NavigationCompletedEventArgs e)
+ {
+ if (_isClosing ||
+ !ReferenceEquals(sender, _coreWebView) ||
+ _currentNavigationId != e.NavigationId)
+ {
+ return;
+ }
+
+ if (!e.IsSuccess)
+ {
+ if (!_usingLegacyRenderer)
+ {
+ FallbackToLegacyRenderer($"navigation failed: {e.WebErrorStatus}");
+ }
+ else if (e.WebErrorStatus != CoreWebView2WebErrorStatus.OperationCanceled)
{
- _processFailedHandler = OnWebViewProcessFailed;
- coreWebView.ProcessFailed += _processFailedHandler;
+ AppLogger.Error(
+ $"Legacy renderer navigation failed on '{_screenDeviceName}': {e.WebErrorStatus}.");
}
+ return;
+ }
- if (_navigationCompletedHandler != null)
+ // Navigation creates a new JS global object, so every page needs the complete host state.
+ _lastReportedInputMode = null;
+ _lastReportedAlwaysTrail = null;
+ UpdateColor(ConfigManager.ParticleColor);
+ ConfigManager.GetAnimationSpeedsForOverlay(out double trailSp, out double clickSp);
+ UpdateEffectSettings(ConfigManager.EffectScale, ConfigManager.EffectOpacity, trailSp, clickSp);
+ SyncInputContext(InputModeMouse);
+ if (_overlayRuntimePaused)
+ {
+ ExecuteScript("if(window.setRenderingPaused) window.setRenderingPaused(true);");
+ _ = TrySuspendWebViewAsync();
+ }
+ }
+
+ private void OnWebMessageReceived(object? sender, CoreWebView2WebMessageReceivedEventArgs e)
+ {
+ if (_isClosing || _usingLegacyRenderer || !ReferenceEquals(sender, _coreWebView))
+ {
+ return;
+ }
+
+ string messageJson;
+ try
+ {
+ messageJson = e.TryGetWebMessageAsString();
+ }
+ catch (ArgumentException)
+ {
+ // Accept object messages defensively while the documented adapter contract remains JSON text.
+ messageJson = e.WebMessageAsJson;
+ }
+
+ try
+ {
+ using JsonDocument document = JsonDocument.Parse(messageJson);
+ JsonElement root = document.RootElement;
+ if (root.ValueKind != JsonValueKind.Object)
{
- try { coreWebView.NavigationCompleted -= _navigationCompletedHandler; } catch { /* best-effort */ }
+ return;
+ }
+ if (!string.Equals(GetJsonString(root, "source"), "baspark-fx", StringComparison.Ordinal))
+ {
+ return;
+ }
+ if (!string.Equals(
+ GetJsonString(root, "generation"),
+ _rendererGeneration,
+ StringComparison.Ordinal))
+ {
+ return;
}
- var streamInfo = System.Windows.Application.GetResourceStream(new Uri("pack://application:,,,/Web/index.html"));
- if (streamInfo != null)
+ string? type = GetJsonString(root, "type");
+ string backend = GetJsonString(root, "backend") ??
+ GetJsonString(root, "resolvedBloomBackend") ??
+ "unknown";
+
+ if (string.Equals(type, "ready", StringComparison.Ordinal))
{
- using var reader = new System.IO.StreamReader(streamInfo.Stream);
- string htmlContent = reader.ReadToEnd();
- coreWebView.NavigateToString(htmlContent);
- _navigationCompletedHandler = (s, e) =>
+ bool firstReadyMessage = !_rendererReady;
+ _rendererReady = true;
+ _unresponsiveTracker.Reset();
+ StopRendererReadyTimeout();
+ if (firstReadyMessage)
{
- if (_isClosing) return;
-
- _lastReportedInputMode = null;
- _lastReportedAlwaysTrail = null;
- UpdateColor(ConfigManager.ParticleColor);
- ConfigManager.GetAnimationSpeedsForOverlay(out double trailSp, out double clickSp);
- UpdateEffectSettings(ConfigManager.EffectScale, ConfigManager.EffectOpacity, trailSp, clickSp);
- SyncInputContext(InputModeMouse);
- if (_overlayRuntimePaused)
- {
- ExecuteScript("if(window.setRenderingPaused) window.setRenderingPaused(true);");
- _ = TrySuspendWebViewAsync();
- }
- };
- coreWebView.NavigationCompleted += _navigationCompletedHandler;
+ AppLogger.Info(
+ $"BA click renderer ready on '{_screenDeviceName}' (backend: {backend}).");
+ }
+ return;
}
+
+ if (string.Equals(type, "backend", StringComparison.Ordinal))
+ {
+ string requested = GetJsonString(root, "requestedBloomBackend") ?? "unknown";
+ AppLogger.Info(
+ $"BA click renderer backend on '{_screenDeviceName}': {backend} (requested: {requested}).");
+ return;
+ }
+
+ if (string.Equals(type, "error", StringComparison.Ordinal))
+ {
+ string phase = GetJsonString(root, "phase") ?? "unknown";
+ string message = GetJsonString(root, "message") ?? "unspecified renderer error";
+ AppLogger.Error(
+ $"BA click renderer error on '{_screenDeviceName}' during '{phase}': {message}.");
+ FallbackToLegacyRenderer($"renderer error during '{phase}'");
+ }
+ }
+ catch (JsonException ex)
+ {
+ AppLogger.Warn(
+ $"Ignored malformed renderer message on '{_screenDeviceName}': {ex.Message}");
+ }
+ }
+
+ private static string? GetJsonString(JsonElement element, string propertyName)
+ {
+ if (!element.TryGetProperty(propertyName, out JsonElement value) ||
+ value.ValueKind != JsonValueKind.String)
+ {
+ return null;
+ }
+
+ return value.GetString();
+ }
+
+ private void StartRendererReadyTimeout()
+ {
+ StopRendererReadyTimeout();
+ _rendererReadyTimeoutTimer = new System.Windows.Threading.DispatcherTimer
+ {
+ Interval = TimeSpan.FromSeconds(2)
+ };
+ _rendererReadyTimeoutTimer.Tick += OnRendererReadyTimeout;
+ _rendererReadyTimeoutTimer.Start();
+ }
+
+ private void OnRendererReadyTimeout(object? sender, EventArgs e)
+ {
+ if (!ReferenceEquals(sender, _rendererReadyTimeoutTimer))
+ {
+ return;
+ }
+
+ StopRendererReadyTimeout();
+ if (_isClosing || _rendererReady || _usingLegacyRenderer)
+ {
+ return;
+ }
+
+ AppLogger.Warn(
+ $"BA click renderer ready timeout on '{_screenDeviceName}'; switching to legacy renderer.");
+ FallbackToLegacyRenderer("ready timeout");
+ }
+
+ private void StopRendererReadyTimeout()
+ {
+ if (_rendererReadyTimeoutTimer == null)
+ {
+ return;
+ }
+
+ _rendererReadyTimeoutTimer.Stop();
+ _rendererReadyTimeoutTimer.Tick -= OnRendererReadyTimeout;
+ _rendererReadyTimeoutTimer = null;
+ }
+
+ private void FallbackToLegacyRenderer(string reason)
+ {
+ if (_isClosing || _usingLegacyRenderer || _legacyFallbackAttempted)
+ {
+ return;
+ }
+
+ if (!TryGetCoreWebView2(out CoreWebView2? coreWebView))
+ {
+ return;
+ }
+
+ // The one-way fallback prevents a broken primary/legacy pair from navigating forever.
+ _legacyFallbackAttempted = true;
+ _usingLegacyRenderer = true;
+ _rendererReady = true;
+ _rendererGeneration = null;
+ StopRendererReadyTimeout();
+ AppLogger.Warn(
+ $"Falling back to legacy renderer on '{_screenDeviceName}' ({reason}).");
+
+ try
+ {
+ string legacyHtml = ReadResourceText(LegacyRendererResourcePath);
+ NavigateHtml(coreWebView, legacyHtml);
}
catch (Exception ex) when (IsExpectedWebViewShutdownException(ex))
{
}
- catch (Exception ex)
+ catch (Exception ex) when (!IsExpectedWebViewShutdownException(ex))
{
- System.Windows.MessageBox.Show(Localization.Format("WebView2_InitFailed", ex.Message));
+ AppLogger.Error(
+ $"Failed to load legacy renderer on '{_screenDeviceName}'.",
+ ex);
}
}
@@ -465,21 +755,174 @@ private static bool IsWebViewEnvironmentConflictException(Exception ex)
private void OnWebViewProcessFailed(object? sender, CoreWebView2ProcessFailedEventArgs e)
{
- if (_isClosing) return;
+ if (_isClosing ||
+ sender is not CoreWebView2 failedCoreWebView ||
+ !ReferenceEquals(failedCoreWebView, _coreWebView))
+ {
+ return;
+ }
+
+ CoreWebView2ProcessFailedKind failureKind = e.ProcessFailedKind;
+ WebViewProcessRecoveryAction recoveryAction =
+ WebViewProcessFailurePolicy.GetRecoveryAction(failureKind);
+ if (failureKind == CoreWebView2ProcessFailedKind.RenderProcessUnresponsive)
+ {
+ bool shouldRecover = _unresponsiveTracker.Register(
+ failedCoreWebView,
+ DateTime.UtcNow.Ticks);
+ if (!shouldRecover)
+ {
+ AppLogger.Warn(
+ $"WebView2 renderer unresponsive on '{_screenDeviceName}' " +
+ $"({_unresponsiveTracker.ConsecutiveReports}/3); waiting for runtime recovery.");
+ return;
+ }
+
+ recoveryAction = WebViewProcessRecoveryAction.RecreateWebViewControl;
+ }
+ if (recoveryAction == WebViewProcessRecoveryAction.None)
+ {
+ AppLogger.Warn(
+ $"WebView2 process event on '{_screenDeviceName}' ({failureKind}); runtime recovery is left intact.");
+ return;
+ }
+
+ if (_processRecoveryPending)
+ {
+ return;
+ }
+ _processRecoveryPending = true;
Dispatcher.BeginInvoke(new Action(() =>
{
- if (_isClosing) return;
- if (_coreWebView != null && _processFailedHandler != null)
+ try
{
- try { _coreWebView.ProcessFailed -= _processFailedHandler; } catch { /* ignore: event unsubscribe is best-effort */ }
+ if (_isClosing || !ReferenceEquals(failedCoreWebView, _coreWebView))
+ {
+ return;
+ }
+
+ AppLogger.Warn(
+ $"WebView2 process failed on '{_screenDeviceName}' ({failureKind}); recovering renderer.");
+ if (recoveryAction == WebViewProcessRecoveryAction.RecreateWebViewControl)
+ {
+ RecreateWebViewControl();
+ }
+ else
+ {
+ NavigateCurrentRenderer(failedCoreWebView);
+ }
+ }
+ catch (Exception ex) when (IsExpectedWebViewShutdownException(ex))
+ {
+ // Process-failure recovery races with shutdown and must never escape the UI dispatcher.
+ if (!_isClosing)
+ {
+ AppLogger.Warn(
+ $"WebView2 recovery was interrupted on '{_screenDeviceName}': {ex.Message}");
+ }
+ }
+ catch (Exception ex)
+ {
+ AppLogger.Error(
+ $"WebView2 recovery failed on '{_screenDeviceName}'.",
+ ex);
+ }
+ finally
+ {
+ _processRecoveryPending = false;
}
- _processFailedHandler = null;
- _coreWebView = null;
- _ = InitWebView();
}));
}
+ private void RecreateWebViewControl()
+ {
+ StopRendererReadyTimeout();
+ // The browser process is already disconnected; Dispose owns native event cleanup.
+ ClearCoreWebViewEventState();
+ _coreWebView = null;
+
+ Microsoft.Web.WebView2.Wpf.WebView2 oldWebView = webView;
+ webViewHost.Children.Remove(oldWebView);
+ UnregisterName("webView");
+ oldWebView.Dispose();
+
+ var replacementWebView = new Microsoft.Web.WebView2.Wpf.WebView2
+ {
+ Name = "webView",
+ DefaultBackgroundColor = System.Drawing.Color.Transparent
+ };
+
+ RegisterName("webView", replacementWebView);
+ webView = replacementWebView;
+ webViewHost.Children.Add(replacementWebView);
+ _ = InitWebView();
+ }
+
+ private void DetachCoreWebViewEvents()
+ {
+ CoreWebView2? coreWebView = _coreWebView;
+ if (coreWebView != null)
+ {
+ if (_navigationCompletedHandler != null)
+ {
+ try
+ {
+ coreWebView.NavigationCompleted -= _navigationCompletedHandler;
+ }
+ catch (Exception ex) when (IsExpectedWebViewShutdownException(ex))
+ {
+ }
+ }
+
+ if (_navigationStartingHandler != null)
+ {
+ try
+ {
+ coreWebView.NavigationStarting -= _navigationStartingHandler;
+ }
+ catch (Exception ex) when (IsExpectedWebViewShutdownException(ex))
+ {
+ }
+ }
+
+ if (_processFailedHandler != null)
+ {
+ try
+ {
+ coreWebView.ProcessFailed -= _processFailedHandler;
+ }
+ catch (Exception ex) when (IsExpectedWebViewShutdownException(ex))
+ {
+ }
+ }
+
+ if (_webMessageReceivedHandler != null)
+ {
+ try
+ {
+ coreWebView.WebMessageReceived -= _webMessageReceivedHandler;
+ }
+ catch (Exception ex) when (IsExpectedWebViewShutdownException(ex))
+ {
+ }
+ }
+ }
+
+ ClearCoreWebViewEventState();
+ }
+
+ private void ClearCoreWebViewEventState()
+ {
+ _navigationStartingHandler = null;
+ _navigationCompletedHandler = null;
+ _processFailedHandler = null;
+ _webMessageReceivedHandler = null;
+ _currentNavigationId = null;
+ _rendererGeneration = null;
+ _unresponsiveTracker.Reset();
+ }
+
private static bool IsCursorVisible()
{
CURSORINFO pci = new CURSORINFO();
@@ -607,6 +1050,14 @@ public void EmitUp(bool touchLike)
ExecuteWithInputContext(inputMode, "if(window.externalUp) window.externalUp();");
}
+ public void EmitCancel()
+ {
+ ExecuteScript(
+ "if(window.externalCancel){window.externalCancel();}" +
+ "else if(window.spark&&window.spark.clearEffects){window.spark.clearEffects();}" +
+ "else if(window.externalUp){window.externalUp();}");
+ }
+
private void UpdateOverlayBounds()
{
Rectangle bounds = GetScreenBounds();
@@ -667,6 +1118,7 @@ private bool TryConvertScreenToOverlayPoint(int screenX, int screenY, out System
protected override void OnClosing(System.ComponentModel.CancelEventArgs e)
{
_isClosing = true;
+ StopRendererReadyTimeout();
base.OnClosing(e);
}
@@ -680,32 +1132,8 @@ protected override void OnClosed(EventArgs e)
_topmostTimer = null;
}
- if (_coreWebView != null)
- {
- if (_navigationCompletedHandler != null)
- {
- try
- {
- _coreWebView.NavigationCompleted -= _navigationCompletedHandler;
- }
- catch (Exception ex) when (IsExpectedWebViewShutdownException(ex))
- {
- }
- _navigationCompletedHandler = null;
- }
-
- if (_processFailedHandler != null)
- {
- try
- {
- _coreWebView.ProcessFailed -= _processFailedHandler;
- }
- catch (Exception ex) when (IsExpectedWebViewShutdownException(ex))
- {
- }
- _processFailedHandler = null;
- }
- }
+ StopRendererReadyTimeout();
+ DetachCoreWebViewEvents();
_coreWebView = null;
if (_winEventHook != IntPtr.Zero)
diff --git a/src/OverlayManager.cs b/src/OverlayManager.cs
index 6f59a6a..a9cec0c 100644
--- a/src/OverlayManager.cs
+++ b/src/OverlayManager.cs
@@ -82,6 +82,7 @@ private struct MONITORINFO
private readonly Dictionary _overlays = new(StringComparer.OrdinalIgnoreCase);
private IKeyboardMouseEvents? _globalHook;
private MainWindow? _activePointerOverlay;
+ private MainWindow? _lastTrailOverlay;
private long _lastMoveTicks;
private long _lastClickTicks;
private long _moveIntervalTicks = 250000;
@@ -550,7 +551,19 @@ private void OnMouseDownExt(object? sender, MouseEventExtArgs e)
}
MainWindow? target = ResolveTargetOverlay(cursorX, cursorY);
- if (target == null) return;
+ if (target == null)
+ {
+ return;
+ }
+
+ if (ConfigManager.EnableAlwaysTrailEffect)
+ {
+ SwitchAlwaysTrailOverlay(target);
+ }
+ else
+ {
+ SwitchAlwaysTrailOverlay(null);
+ }
_isPrimaryPointerDown = true;
_isTouchLikeInput = !CursorIsVisible();
@@ -567,14 +580,28 @@ private void OnMouseDownExt(object? sender, MouseEventExtArgs e)
private void OnMouseMoveExt(object? sender, MouseEventExtArgs e)
{
// 拖尾仅在点击特效开启或常驻拖尾开启时渲染
- if (!ConfigManager.IsTrailEffectActive) return;
- if (!CanRenderEffects()) return;
+ if (!ConfigManager.IsTrailEffectActive)
+ {
+ SwitchAlwaysTrailOverlay(null);
+ return;
+ }
+ if (!CanRenderEffects())
+ {
+ return;
+ }
bool cursorVisible = CursorIsVisible();
- if (!cursorVisible && !_isPrimaryPointerDown) return;
+ if (!cursorVisible && !_isPrimaryPointerDown)
+ {
+ SwitchAlwaysTrailOverlay(null);
+ return;
+ }
long currentTicks = DateTime.Now.Ticks;
- if (currentTicks - _lastMoveTicks < _moveIntervalTicks) return;
+ if (currentTicks - _lastMoveTicks < _moveIntervalTicks)
+ {
+ return;
+ }
_lastMoveTicks = currentTicks;
if (!TryGetPhysicalCursorPosition(out int cursorX, out int cursorY))
@@ -583,7 +610,12 @@ private void OnMouseMoveExt(object? sender, MouseEventExtArgs e)
cursorY = e.Y;
}
- var target = _activePointerOverlay ?? ResolveTargetOverlay(cursorX, cursorY);
+ MainWindow? target = _activePointerOverlay ?? ResolveTargetOverlay(cursorX, cursorY);
+ if (!_isPrimaryPointerDown)
+ {
+ SwitchAlwaysTrailOverlay(
+ ConfigManager.EnableAlwaysTrailEffect ? target : null);
+ }
target?.EmitMove(cursorX, cursorY, _isTouchLikeInput || !cursorVisible);
}
@@ -603,9 +635,7 @@ private void OnMouseUpExt(object? sender, MouseEventExtArgs e)
}
_activePointerOverlay?.EmitUp(_isTouchLikeInput);
- _isPrimaryPointerDown = false;
- _isTouchLikeInput = false;
- _activePointerOverlay = null;
+ ResetPrimaryPointerState();
}
///
@@ -637,9 +667,16 @@ private bool CanRenderEffects(bool skipSuppressionCheck = false)
private void ReleasePointerStateSilent()
{
- _isPrimaryPointerDown = false;
- _isTouchLikeInput = false;
- _activePointerOverlay = null;
+ MainWindow? activePointerOverlay = _activePointerOverlay;
+ activePointerOverlay?.EmitCancel();
+ if (_lastTrailOverlay != null &&
+ !ReferenceEquals(_lastTrailOverlay, activePointerOverlay))
+ {
+ _lastTrailOverlay.EmitCancel();
+ }
+
+ ResetPrimaryPointerState();
+ _lastTrailOverlay = null;
}
private void ReleasePointerState()
@@ -651,7 +688,26 @@ private void ReleasePointerState()
}
_activePointerOverlay?.EmitUp(_isTouchLikeInput);
- ReleasePointerStateSilent();
+ ResetPrimaryPointerState();
+ }
+
+ private void ResetPrimaryPointerState()
+ {
+ _isPrimaryPointerDown = false;
+ _isTouchLikeInput = false;
+ _activePointerOverlay = null;
+ }
+
+ private void SwitchAlwaysTrailOverlay(MainWindow? target)
+ {
+ if (ReferenceEquals(_lastTrailOverlay, target))
+ {
+ return;
+ }
+
+ // The old renderer must forget its last point before coordinates switch to another screen.
+ _lastTrailOverlay?.EmitCancel();
+ _lastTrailOverlay = target;
}
private MainWindow? ResolveTargetOverlay(int x, int y)
@@ -680,6 +736,14 @@ private void RebuildWindows(bool forceRebuild)
.Select(item => item.Screen)
.ToDictionary(screen => screen.DeviceName, screen => screen, StringComparer.OrdinalIgnoreCase);
+ bool topologyChanged = forceRebuild ||
+ _overlays.Keys.Except(targetScreens.Keys, StringComparer.OrdinalIgnoreCase).Any() ||
+ targetScreens.Keys.Except(_overlays.Keys, StringComparer.OrdinalIgnoreCase).Any();
+ if (topologyChanged)
+ {
+ ReleasePointerStateSilent();
+ }
+
if (forceRebuild)
{
CloseWindows();
@@ -718,7 +782,25 @@ private void CloseOverlay(string deviceName)
return;
}
- try { overlay.Close(); } catch (Exception ex) { AppLogger.Warn($"Failed to close overlay for '{deviceName}': {ex.Message}"); }
+ // Cancel before disposal because a closed WebView can no longer clear retained trail state.
+ overlay.EmitCancel();
+ if (ReferenceEquals(_activePointerOverlay, overlay))
+ {
+ ResetPrimaryPointerState();
+ }
+ if (ReferenceEquals(_lastTrailOverlay, overlay))
+ {
+ _lastTrailOverlay = null;
+ }
+
+ try
+ {
+ overlay.Close();
+ }
+ catch (Exception ex)
+ {
+ AppLogger.Warn($"Failed to close overlay for '{deviceName}': {ex.Message}");
+ }
_overlays.Remove(deviceName);
}
diff --git a/src/Web/fx-adapter.js b/src/Web/fx-adapter.js
new file mode 100644
index 0000000..00277f4
--- /dev/null
+++ b/src/Web/fx-adapter.js
@@ -0,0 +1,647 @@
+(function ()
+{
+ 'use strict';
+
+ const POINTER_ID = 1;
+ const DEFAULT_COLOR = '#2dafff';
+ const HOST_GENERATION =
+ typeof window.__basparkRendererGeneration === 'string'
+ ? window.__basparkRendererGeneration
+ : '';
+ const DEFAULT_SETTINGS = Object.freeze(
+ {
+ scale: 1.5,
+ opacity: 1,
+ trailSpeed: 1,
+ clickSpeed: 1,
+ });
+ const DOM_CONTENT_LOADED_OPTIONS =
+ {
+ once: true,
+ };
+ const state =
+ {
+ fx: null,
+ initialized: false,
+ paused: false,
+ inputMode: 'mouse',
+ alwaysTrailEnabled: false,
+ effectiveAlwaysTrail: false,
+ activePointerKind: null,
+ color: DEFAULT_COLOR,
+ settings:
+ {
+ ...DEFAULT_SETTINGS,
+ },
+ lastBoomX: -1,
+ lastBoomY: -1,
+ lastBoomTime: 0,
+ lastMoveX: -1,
+ lastMoveY: -1,
+ };
+
+ function clamp(value, minimum, maximum)
+ {
+ return Math.max(minimum, Math.min(maximum, value));
+ }
+
+ function errorMessage(error)
+ {
+ if (error instanceof Error)
+ {
+ return error.message;
+ }
+
+ return String(error);
+ }
+
+ function postHostMessage(type, detail = null)
+ {
+ const message =
+ {
+ source: 'baspark-fx',
+ generation: HOST_GENERATION,
+ type,
+ ...(detail || Object.create(null)),
+ };
+
+ try
+ {
+ if (
+ window.chrome &&
+ window.chrome.webview &&
+ typeof window.chrome.webview.postMessage === 'function'
+ )
+ {
+ // 宿主统一按 JSON 字符串解析,避免 WebView2 对象封送行为随版本变化。
+ window.chrome.webview.postMessage(JSON.stringify(message));
+ }
+ }
+ catch (error)
+ {
+ console.error('[BASpark FX] 无法向宿主发送消息:', error);
+ }
+ }
+
+ function reportError(phase, error)
+ {
+ const message = errorMessage(error);
+
+ console.error(`[BASpark FX] ${phase}:`, error);
+ postHostMessage(
+ 'error',
+ {
+ phase,
+ message,
+ });
+ }
+
+ function invokeFx(phase, action)
+ {
+ try
+ {
+ return action();
+ }
+ catch (error)
+ {
+ reportError(phase, error);
+ return false;
+ }
+ }
+
+ function resetInputCache()
+ {
+ state.activePointerKind = null;
+ state.lastBoomX = -1;
+ state.lastBoomY = -1;
+ state.lastBoomTime = 0;
+ state.lastMoveX = -1;
+ state.lastMoveY = -1;
+ }
+
+ function pointerType()
+ {
+ return state.inputMode === 'touch' ? 'touch' : 'mouse';
+ }
+
+ function toCanvasPoint(percentX, percentY)
+ {
+ if (!state.fx)
+ {
+ return null;
+ }
+
+ const normalizedX = Number(percentX);
+ const normalizedY = Number(percentY);
+
+ if (!Number.isFinite(normalizedX) || !Number.isFinite(normalizedY))
+ {
+ return null;
+ }
+
+ // C# 传入窗口内归一化坐标;公开 API 要求 Canvas 局部 CSS 像素。
+ return (
+ {
+ x: clamp(normalizedX, 0, 1) * state.fx.width,
+ y: clamp(normalizedY, 0, 1) * state.fx.height,
+ pointerId: POINTER_ID,
+ pointerType: pointerType(),
+ });
+ }
+
+ function cancelActivePointer()
+ {
+ if (!state.fx || state.activePointerKind === null)
+ {
+ return false;
+ }
+
+ const accepted = state.fx.pointerCancel(POINTER_ID);
+
+ state.activePointerKind = null;
+ return accepted;
+ }
+
+ function applyInputContext()
+ {
+ if (!state.fx)
+ {
+ return;
+ }
+
+ state.fx.updateConfig(
+ {
+ trailAlways: state.effectiveAlwaysTrail,
+ });
+ }
+
+ function applyColor()
+ {
+ if (!state.fx)
+ {
+ return;
+ }
+
+ state.fx.setThemeColor(state.color);
+ }
+
+ function applyEffectSettings()
+ {
+ if (!state.fx)
+ {
+ return;
+ }
+
+ state.fx.updateConfig(
+ {
+ // 旧引擎以 1.5 为默认尺寸,新引擎以 1 为默认尺寸。
+ scale: Math.max(0.01, state.settings.scale / 1.5),
+ opacity: state.settings.opacity,
+ trailTimeScale: state.settings.trailSpeed,
+ clickTimeScale: state.settings.clickSpeed,
+ });
+ }
+
+ function parseRgbColor(rgbString)
+ {
+ const channels = String(rgbString).split(',').map((channel) =>
+ {
+ return Number(channel.trim());
+ });
+
+ if (
+ channels.length !== 3 ||
+ channels.some((channel) => !Number.isFinite(channel))
+ )
+ {
+ return null;
+ }
+
+ return `#${channels.map((channel) =>
+ {
+ return Math.round(clamp(channel, 0, 255))
+ .toString(16)
+ .padStart(2, '0');
+ }).join('')}`;
+ }
+
+ function normalizeSpeed(value, fallback)
+ {
+ const numeric = Number(value);
+
+ if (!Number.isFinite(numeric))
+ {
+ return fallback;
+ }
+
+ return clamp(numeric, 0.2, 3);
+ }
+
+ window.externalBoom = function (percentX, percentY)
+ {
+ if (state.paused || !state.fx)
+ {
+ return false;
+ }
+
+ const numericX = Number(percentX);
+ const numericY = Number(percentY);
+ const now = Date.now();
+
+ if (
+ numericX === state.lastBoomX &&
+ numericY === state.lastBoomY &&
+ now - state.lastBoomTime < 25
+ )
+ {
+ return false;
+ }
+
+ const point = toCanvasPoint(numericX, numericY);
+
+ if (!point)
+ {
+ return false;
+ }
+
+ state.lastBoomX = numericX;
+ state.lastBoomY = numericY;
+ state.lastBoomTime = now;
+
+ return invokeFx('externalBoom', function ()
+ {
+ // 丢失抬起事件时先恢复指针状态,避免后续点击被单指针上限永久拒绝。
+ cancelActivePointer();
+ const accepted = state.fx.pointerDown(point);
+
+ if (accepted)
+ {
+ state.activePointerKind = 'press';
+ }
+
+ return accepted;
+ });
+ };
+
+ window.externalMove = function (percentX, percentY)
+ {
+ if (state.paused || !state.fx)
+ {
+ return false;
+ }
+
+ const numericX = Number(percentX);
+ const numericY = Number(percentY);
+
+ if (
+ numericX === state.lastMoveX &&
+ numericY === state.lastMoveY
+ )
+ {
+ return false;
+ }
+
+ const point = toCanvasPoint(numericX, numericY);
+
+ if (!point)
+ {
+ return false;
+ }
+
+ state.lastMoveX = numericX;
+ state.lastMoveY = numericY;
+
+ return invokeFx('externalMove', function ()
+ {
+ const accepted = state.fx.pointerMove(point);
+
+ if (
+ accepted &&
+ state.activePointerKind === null &&
+ state.effectiveAlwaysTrail
+ )
+ {
+ state.activePointerKind = 'hover';
+ }
+
+ return accepted;
+ });
+ };
+
+ window.externalUp = function ()
+ {
+ if (state.paused || !state.fx)
+ {
+ return false;
+ }
+
+ return invokeFx('externalUp', function ()
+ {
+ const accepted = state.fx.pointerUp(POINTER_ID);
+
+ if (accepted)
+ {
+ state.activePointerKind = null;
+ }
+
+ return accepted;
+ });
+ };
+
+ window.externalCancel = function ()
+ {
+ if (!state.fx)
+ {
+ resetInputCache();
+ return false;
+ }
+
+ return invokeFx('externalCancel', function ()
+ {
+ const accepted = state.fx.pointerCancel(POINTER_ID);
+
+ resetInputCache();
+ return accepted;
+ });
+ };
+
+ window.setRenderingPaused = function (paused)
+ {
+ state.paused = Boolean(paused);
+
+ if (!state.fx)
+ {
+ return;
+ }
+
+ invokeFx('setRenderingPaused', function ()
+ {
+ if (state.paused)
+ {
+ resetInputCache();
+ state.fx.setPaused(
+ true,
+ {
+ clear: true,
+ });
+ return true;
+ }
+
+ state.fx.setPaused(false);
+ return true;
+ });
+ };
+
+ window.setInputContext = function (mode, alwaysTrailEnabled)
+ {
+ const nextMode = mode === 'touch' ? 'touch' : 'mouse';
+ const nextAlwaysTrailEnabled = Boolean(alwaysTrailEnabled);
+ const nextEffectiveAlwaysTrail =
+ nextMode === 'mouse' && nextAlwaysTrailEnabled;
+
+ if (
+ nextMode !== state.inputMode ||
+ (
+ !nextEffectiveAlwaysTrail &&
+ state.activePointerKind === 'hover'
+ )
+ )
+ {
+ invokeFx('setInputContext.cancel', function ()
+ {
+ return cancelActivePointer();
+ });
+ }
+
+ state.inputMode = nextMode;
+ state.alwaysTrailEnabled = nextAlwaysTrailEnabled;
+ state.effectiveAlwaysTrail = nextEffectiveAlwaysTrail;
+
+ invokeFx('setInputContext', function ()
+ {
+ applyInputContext();
+ return true;
+ });
+ };
+
+ window.updateColor = function (rgbString)
+ {
+ const color = parseRgbColor(rgbString);
+
+ if (!color)
+ {
+ // 配置损坏不代表渲染器失效,保留当前颜色即可继续运行。
+ console.warn('[BASpark FX] 忽略非法 RGB 颜色:', rgbString);
+ return false;
+ }
+
+ state.color = color;
+
+ return invokeFx('updateColor', function ()
+ {
+ applyColor();
+ return true;
+ });
+ };
+
+ window.updateEffectSettings = function (
+ scale,
+ opacity,
+ trailSpeed,
+ clickSpeed
+ )
+ {
+ const numericScale = Number(scale);
+ const numericOpacity = Number(opacity);
+ const safeTrailSpeed = normalizeSpeed(
+ trailSpeed,
+ DEFAULT_SETTINGS.trailSpeed,
+ );
+
+ state.settings =
+ {
+ scale: Number.isFinite(numericScale) && numericScale > 0
+ ? numericScale
+ : DEFAULT_SETTINGS.scale,
+ opacity: Number.isFinite(numericOpacity)
+ ? clamp(numericOpacity, 0.1, 1)
+ : DEFAULT_SETTINGS.opacity,
+ trailSpeed: safeTrailSpeed,
+ clickSpeed: normalizeSpeed(clickSpeed, safeTrailSpeed),
+ };
+
+ return invokeFx('updateEffectSettings', function ()
+ {
+ applyEffectSettings();
+ return true;
+ });
+ };
+
+ function handleBackendChange(event)
+ {
+ const detail = event.detail || Object.create(null);
+ const requestedBloomBackend =
+ detail.requestedBloomBackend || state.fx.getConfig().bloomBackend;
+ const resolvedBloomBackend =
+ detail.resolvedBloomBackend ||
+ state.fx.getConfig().resolvedBloomBackend;
+
+ postHostMessage(
+ 'backend',
+ {
+ backend: resolvedBloomBackend,
+ requestedBloomBackend,
+ resolvedBloomBackend,
+ });
+
+ if (resolvedBloomBackend === 'software')
+ {
+ // 全屏 Float32 软件 Bloom 对桌面覆盖层代价过高,GPU 不可用时改用原生辉光。
+ state.fx.updateConfig(
+ {
+ bloomBackend: 'native',
+ });
+ }
+ }
+
+ function initialize()
+ {
+ if (state.initialized)
+ {
+ return;
+ }
+
+ state.initialized = true;
+
+ try
+ {
+ if (
+ !window.BAClickFX ||
+ typeof window.BAClickFX.BAClickFX !== 'function'
+ )
+ {
+ throw new Error('ba-click-fx IIFE 未在适配器之前注入');
+ }
+
+ state.fx = new window.BAClickFX.BAClickFX(
+ {
+ inputSource: 'manual',
+ bloomBackend: 'webgl2',
+ isolatedCompositing: true,
+ maxDpr: 2,
+ });
+
+ const backendEventName =
+ window.BAClickFX.BLOOM_BACKEND_CHANGE_EVENT ||
+ 'baclickfxbackendchange';
+
+ state.fx.canvas.addEventListener(
+ backendEventName,
+ handleBackendChange,
+ );
+
+ applyInputContext();
+ applyColor();
+ applyEffectSettings();
+
+ if (state.paused)
+ {
+ state.fx.setPaused(
+ true,
+ {
+ clear: true,
+ });
+ }
+
+ const config = state.fx.getConfig();
+
+ postHostMessage(
+ 'ready',
+ {
+ backend: config.resolvedBloomBackend,
+ resolvedBloomBackend: config.resolvedBloomBackend,
+ });
+ }
+ catch (error)
+ {
+ reportError('initialize', error);
+ }
+ }
+
+ function installCompatibilityShims()
+ {
+ if (typeof Array.prototype.at !== 'function')
+ {
+ Object.defineProperty(
+ Array.prototype,
+ 'at',
+ {
+ configurable: true,
+ writable: true,
+ value: function (index)
+ {
+ const length = this.length >>> 0;
+ const integer = Math.trunc(Number(index) || 0);
+ const offset = integer < 0 ? length + integer : integer;
+
+ if (offset < 0 || offset >= length)
+ {
+ return undefined;
+ }
+
+ return this[offset];
+ },
+ });
+ }
+
+ if (typeof window.structuredClone !== 'function')
+ {
+ // 上游只克隆由数字、布尔值、数组和普通对象组成的配置快照。
+ window.structuredClone = function (value)
+ {
+ return JSON.parse(JSON.stringify(value));
+ };
+ }
+ }
+
+ window.addEventListener('error', function (event)
+ {
+ reportError('window.error', event.error || event.message);
+ });
+
+ window.addEventListener('unhandledrejection', function (event)
+ {
+ reportError('unhandledrejection', event.reason);
+ });
+
+ window.addEventListener('beforeunload', function ()
+ {
+ if (!state.fx)
+ {
+ return;
+ }
+
+ invokeFx('beforeunload', function ()
+ {
+ state.fx.destroy();
+ state.fx = null;
+ resetInputCache();
+ return true;
+ });
+ });
+
+ installCompatibilityShims();
+
+ if (document.readyState === 'loading')
+ {
+ document.addEventListener(
+ 'DOMContentLoaded',
+ initialize,
+ DOM_CONTENT_LOADED_OPTIONS,
+ );
+ }
+ else
+ {
+ initialize();
+ }
+})();
diff --git a/src/Web/index.html b/src/Web/index.html
index 80951b4..478b3e4 100644
--- a/src/Web/index.html
+++ b/src/Web/index.html
@@ -3,791 +3,24 @@
+
-
-
+
+
-