diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 0000000..a37c1c8
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,26 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ // 使用 IntelliSense 找出 C# 调试存在哪些属性
+ // 将悬停用于现有属性的说明
+ // 有关详细信息,请访问 https://github.com/dotnet/vscode-csharp/blob/main/debugger-launchjson.md
+ "name": ".NET Core Launch (console)",
+ "type": "coreclr",
+ "request": "launch",
+ "preLaunchTask": "build",
+ // 如果已更改目标框架,请确保更新程序路径。
+ "program": "${workspaceFolder}/src/bin/Debug/net8.0-windows10.0.19041.0/BASpark.dll",
+ "args": [],
+ "cwd": "${workspaceFolder}/src",
+ // 有关“控制台”字段的详细信息,请参阅 https://aka.ms/VSCode-CS-LaunchJson-Console
+ "console": "internalConsole",
+ "stopAtEntry": false
+ },
+ {
+ "name": ".NET Core Attach",
+ "type": "coreclr",
+ "request": "attach"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
new file mode 100644
index 0000000..3eb040e
--- /dev/null
+++ b/.vscode/tasks.json
@@ -0,0 +1,41 @@
+{
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "label": "build",
+ "command": "dotnet",
+ "type": "process",
+ "args": [
+ "build",
+ "${workspaceFolder}/src/BASpark.csproj",
+ "/property:GenerateFullPaths=true",
+ "/consoleloggerparameters:NoSummary;ForceNoAlign"
+ ],
+ "problemMatcher": "$msCompile"
+ },
+ {
+ "label": "publish",
+ "command": "dotnet",
+ "type": "process",
+ "args": [
+ "publish",
+ "${workspaceFolder}/src/BASpark.csproj",
+ "/property:GenerateFullPaths=true",
+ "/consoleloggerparameters:NoSummary;ForceNoAlign"
+ ],
+ "problemMatcher": "$msCompile"
+ },
+ {
+ "label": "watch",
+ "command": "dotnet",
+ "type": "process",
+ "args": [
+ "watch",
+ "run",
+ "--project",
+ "${workspaceFolder}/src/BASpark.csproj"
+ ],
+ "problemMatcher": "$msCompile"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/BASpark.csproj b/src/BASpark.csproj
index a0d39c7..45cb207 100644
--- a/src/BASpark.csproj
+++ b/src/BASpark.csproj
@@ -35,5 +35,11 @@
+
+
+ PreserveNewest
+
+
+
\ No newline at end of file
diff --git a/src/ConfigManager.cs b/src/ConfigManager.cs
index e32cc3b..e928597 100644
--- a/src/ConfigManager.cs
+++ b/src/ConfigManager.cs
@@ -1,7 +1,9 @@
using Microsoft.Win32;
using System;
using System.Collections.Generic;
+using System.IO;
using System.Linq;
+using System.Reflection;
namespace BASpark
{
@@ -51,8 +53,11 @@ public class ScreenSelectionState
public static class ConfigManager
{
private const string RegPath = @"Software\BASpark";
+ public const string CustomHtmlVirtualHost = "baspark.custom";
+ public const string CustomHtmlTemplateFileName = "custom-effect-template.html";
public static string ParticleColor { get; set; } = "45,175,255";
+ public static string CustomHtmlPath { get; set; } = "";
public static bool IsEffectEnabled { get; set; } = true;
public static bool AutoStart { get; set; } = false;
public static bool AgreedToPrivacy { get; set; } = false;
@@ -122,6 +127,7 @@ public static void Load()
ScreenSelections = key.GetValue("ScreenSelections", "")?.ToString() ?? "";
UiLanguage = key.GetValue("UiLanguage", "")?.ToString() ?? "";
ScrollbarVisibility = ParseScrollbarVisibility(key.GetValue("ScrollbarVisibility", "OnScroll")?.ToString());
+ CustomHtmlPath = key.GetValue("CustomHtmlPath", "")?.ToString() ?? "";
if (!string.IsNullOrWhiteSpace(UiLanguage))
{
Localization.ApplyCulture(UiLanguage);
@@ -199,6 +205,80 @@ public static void GetAnimationSpeedsForOverlay(out double trailSpeed, out doubl
}
}
+ public static bool IsUsingCustomHtml => TryResolveCustomHtmlUri(out _, out _);
+
+ public static bool TryResolveCustomHtmlUri(out string navigateUri, out string folderPath)
+ {
+ navigateUri = string.Empty;
+ folderPath = string.Empty;
+ if (string.IsNullOrWhiteSpace(CustomHtmlPath))
+ {
+ return false;
+ }
+
+ string fullPath;
+ try
+ {
+ fullPath = Path.GetFullPath(CustomHtmlPath.Trim());
+ }
+ catch
+ {
+ return false;
+ }
+
+ if (!File.Exists(fullPath))
+ {
+ return false;
+ }
+
+ string ext = Path.GetExtension(fullPath);
+ if (!ext.Equals(".html", StringComparison.OrdinalIgnoreCase) &&
+ !ext.Equals(".htm", StringComparison.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+
+ folderPath = Path.GetDirectoryName(fullPath) ?? string.Empty;
+ if (string.IsNullOrEmpty(folderPath))
+ {
+ return false;
+ }
+
+ string fileName = Path.GetFileName(fullPath);
+ navigateUri = $"https://{CustomHtmlVirtualHost}/{Uri.EscapeDataString(fileName)}";
+ return true;
+ }
+
+ public static string? GetWebSamplesDirectory()
+ {
+ static bool IsWebSamplesDir(string path) =>
+ Directory.Exists(path) &&
+ File.Exists(Path.Combine(path, CustomHtmlTemplateFileName));
+
+ string besideExe = Path.Combine(AppContext.BaseDirectory, "Web");
+ if (IsWebSamplesDir(besideExe))
+ {
+ return besideExe;
+ }
+
+ string? dir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
+ for (int i = 0; i < 8 && !string.IsNullOrEmpty(dir); i++)
+ {
+ foreach (string relative in new[] { "Web", Path.Combine("src", "Web") })
+ {
+ string candidate = Path.Combine(dir, relative);
+ if (IsWebSamplesDir(candidate))
+ {
+ return candidate;
+ }
+ }
+
+ dir = Path.GetDirectoryName(dir);
+ }
+
+ return null;
+ }
+
public static List GetProfiles() => _profiles;
public static FilterProfile? GetActiveProfile()
diff --git a/src/ControlPanelWindow.xaml b/src/ControlPanelWindow.xaml
index 50c9de4..560816f 100644
--- a/src/ControlPanelWindow.xaml
+++ b/src/ControlPanelWindow.xaml
@@ -376,7 +376,20 @@
-
+
+
+
+
+
+
+
+
+
+
@@ -493,6 +506,7 @@
+
diff --git a/src/ControlPanelWindow.xaml.cs b/src/ControlPanelWindow.xaml.cs
index 6132052..24df604 100644
--- a/src/ControlPanelWindow.xaml.cs
+++ b/src/ControlPanelWindow.xaml.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.IO;
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls;
@@ -80,6 +81,8 @@ private struct POINT
private bool _isCheckingUpdate = false;
private bool _suspendLinkedAnimationUiHandlers;
private string _languageAtLoad = Localization.CultureZhCn;
+ private string _customHtmlPathAtLoad = "";
+ private string _customHtmlPathPending = "";
public ObservableCollection Profiles { get; set; } = new ObservableCollection();
public ObservableCollection CurrentProfileProcesses { get; set; } = new ObservableCollection();
@@ -105,6 +108,7 @@ public ControlPanelWindow()
LoadSettings();
ApplyScrollbarSettings();
UiLocalizer.ApplyControlPanel(this);
+ UpdateCustomHtmlPathDisplay();
LoadScreenOptions();
CheckAdminStatus();
LoadRemoteNotice();
@@ -522,6 +526,70 @@ private void LoadSettingsCore()
{
RadioScrollbarOnScroll.IsChecked = true;
}
+
+ _customHtmlPathPending = ConfigManager.CustomHtmlPath ?? "";
+ _customHtmlPathAtLoad = _customHtmlPathPending;
+ UpdateCustomHtmlPathDisplay();
+ ApplyCustomHtmlUiState();
+ }
+
+ private void UpdateCustomHtmlPathDisplay()
+ {
+ TxtCustomHtmlPath.Text = string.IsNullOrWhiteSpace(_customHtmlPathPending)
+ ? Localization.Get("CustomHtml_Builtin")
+ : _customHtmlPathPending;
+ }
+
+ private void ApplyCustomHtmlUiState()
+ {
+ bool usingCustom = !string.IsNullOrWhiteSpace(_customHtmlPathPending) &&
+ File.Exists(_customHtmlPathPending);
+ PanelVisualBuiltIn.IsEnabled = !usingCustom;
+ BtnVisualReset.IsEnabled = !usingCustom;
+ }
+
+ private void BrowseCustomHtml_Click(object sender, RoutedEventArgs e)
+ {
+ var dialog = new Microsoft.Win32.OpenFileDialog
+ {
+ Filter = Localization.Get("CustomHtml_FileFilter"),
+ Title = Localization.Get("CustomHtml_SelectTitle")
+ };
+
+ string? webDir = ConfigManager.GetWebSamplesDirectory();
+ if (!string.IsNullOrEmpty(webDir))
+ {
+ dialog.InitialDirectory = webDir;
+ dialog.FileName = ConfigManager.CustomHtmlTemplateFileName;
+ }
+
+ if (dialog.ShowDialog() == true)
+ {
+ if (string.Equals(
+ Path.GetFileName(dialog.FileName),
+ "index.html",
+ StringComparison.OrdinalIgnoreCase))
+ {
+ System.Windows.MessageBox.Show(
+ this,
+ Localization.Get("CustomHtml_BuiltinIndexWarning"),
+ Localization.Get("CustomHtml_SelectTitle"),
+ MessageBoxButton.OK,
+ MessageBoxImage.Warning);
+ return;
+ }
+
+ _customHtmlPathPending = dialog.FileName;
+ UpdateCustomHtmlPathDisplay();
+ ApplyCustomHtmlUiState();
+ }
+ }
+
+ private void ClearCustomHtml_Click(object sender, RoutedEventArgs e)
+ {
+ _customHtmlPathPending = "";
+ UpdateCustomHtmlPathDisplay();
+ ApplyCustomHtmlUiState();
}
private void ApplyScrollbarSettings()
@@ -1167,6 +1235,12 @@ private void SaveSettings_Click(object sender, RoutedEventArgs e)
ConfigManager.Save("AutoStart", autoStartEnabled);
ConfigManager.Save("EnableTelemetry", CheckTelemetry.IsChecked ?? false);
ConfigManager.Save("ParticleColor", ConfigManager.ParticleColor);
+ bool customHtmlPathChanged = !string.Equals(
+ _customHtmlPathAtLoad,
+ _customHtmlPathPending,
+ StringComparison.OrdinalIgnoreCase);
+ ConfigManager.CustomHtmlPath = _customHtmlPathPending;
+ ConfigManager.Save("CustomHtmlPath", _customHtmlPathPending);
ConfigManager.Save("EffectScale", effectScale);
ConfigManager.Save("EffectOpacity", effectOpacity);
ConfigManager.Save("UseLinkedAnimationSpeed", useLinkedAnimationSpeed);
@@ -1218,6 +1292,13 @@ private void SaveSettings_Click(object sender, RoutedEventArgs e)
App.SetAutoStart(ConfigManager.AutoStart);
ApplyAutoStartSettings();
+ if (customHtmlPathChanged)
+ {
+ App.Overlay?.ReloadAllEffectContent();
+ _customHtmlPathAtLoad = _customHtmlPathPending;
+ }
+
+ ApplyCustomHtmlUiState();
App.Overlay?.UpdateColor(ConfigManager.ParticleColor);
GetUiAnimationSpeeds(out double overlayTrail, out double overlayClick);
App.Overlay?.UpdateEffectSettings(effectScale, effectOpacity, overlayTrail, overlayClick);
@@ -1250,6 +1331,7 @@ private void SaveSettings_Click(object sender, RoutedEventArgs e)
if (languageChanged)
{
UiLocalizer.ApplyControlPanel(this);
+ UpdateCustomHtmlPathDisplay();
LoadScreenOptions();
ConfigManager.Save("LastNoticeContent", string.Empty);
LoadRemoteNotice();
diff --git a/src/MainWindow.xaml.cs b/src/MainWindow.xaml.cs
index 3e828df..accafba 100644
--- a/src/MainWindow.xaml.cs
+++ b/src/MainWindow.xaml.cs
@@ -2,6 +2,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Drawing;
using System.Globalization;
+using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Windows;
@@ -193,11 +194,21 @@ private void SafeEnsureTopmost()
public void UpdateColor(string color)
{
+ if (ConfigManager.IsUsingCustomHtml)
+ {
+ return;
+ }
+
ExecuteScript($"if(window.updateColor) window.updateColor('{color}');");
}
public void UpdateEffectSettings(double scale, double opacity, double trailSpeed, double clickSpeed)
{
+ if (ConfigManager.IsUsingCustomHtml)
+ {
+ return;
+ }
+
string scaleStr = scale.ToString("F2", CultureInfo.InvariantCulture);
string opacityStr = opacity.ToString("F2", CultureInfo.InvariantCulture);
string trailStr = trailSpeed.ToString("F2", CultureInfo.InvariantCulture);
@@ -287,25 +298,9 @@ private async System.Threading.Tasks.Task InitWebView()
_processFailedHandler = OnWebViewProcessFailed;
coreWebView.ProcessFailed += _processFailedHandler;
- var streamInfo = System.Windows.Application.GetResourceStream(new Uri("pack://application:,,,/Web/index.html"));
- if (streamInfo != null)
- {
- using var reader = new System.IO.StreamReader(streamInfo.Stream);
- string htmlContent = reader.ReadToEnd();
- coreWebView.NavigateToString(htmlContent);
- _navigationCompletedHandler = (s, e) =>
- {
- 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);
- };
- coreWebView.NavigationCompleted += _navigationCompletedHandler;
- }
+ _navigationCompletedHandler = OnEffectNavigationCompleted;
+ coreWebView.NavigationCompleted += _navigationCompletedHandler;
+ LoadEffectContent(coreWebView);
}
catch (Exception ex) when (IsExpectedWebViewShutdownException(ex))
{
@@ -316,6 +311,68 @@ private async System.Threading.Tasks.Task InitWebView()
}
}
+ public void ReloadEffectContent()
+ {
+ if (!TryGetCoreWebView2(out CoreWebView2? coreWebView))
+ {
+ return;
+ }
+
+ LoadEffectContent(coreWebView);
+ }
+
+ private void LoadEffectContent(CoreWebView2 coreWebView)
+ {
+ if (ConfigManager.TryResolveCustomHtmlUri(out string navigateUri, out string folderPath))
+ {
+ try
+ {
+ coreWebView.SetVirtualHostNameToFolderMapping(
+ ConfigManager.CustomHtmlVirtualHost,
+ folderPath,
+ CoreWebView2HostResourceAccessKind.Allow);
+ coreWebView.Navigate(navigateUri);
+ return;
+ }
+ catch
+ {
+ }
+ }
+
+ NavigateBuiltinHtml(coreWebView);
+ }
+
+ private static void NavigateBuiltinHtml(CoreWebView2 coreWebView)
+ {
+ var streamInfo = System.Windows.Application.GetResourceStream(new Uri("pack://application:,,,/Web/index.html"));
+ if (streamInfo == null)
+ {
+ return;
+ }
+
+ using var reader = new StreamReader(streamInfo.Stream);
+ coreWebView.NavigateToString(reader.ReadToEnd());
+ }
+
+ private void OnEffectNavigationCompleted(object? sender, CoreWebView2NavigationCompletedEventArgs e)
+ {
+ if (_isClosing || !e.IsSuccess)
+ {
+ return;
+ }
+
+ _lastReportedInputMode = null;
+ _lastReportedAlwaysTrail = null;
+ if (!ConfigManager.IsUsingCustomHtml)
+ {
+ UpdateColor(ConfigManager.ParticleColor);
+ ConfigManager.GetAnimationSpeedsForOverlay(out double trailSp, out double clickSp);
+ UpdateEffectSettings(ConfigManager.EffectScale, ConfigManager.EffectOpacity, trailSp, clickSp);
+ }
+
+ SyncInputContext(InputModeMouse);
+ }
+
private void OnWebViewProcessFailed(object? sender, CoreWebView2ProcessFailedEventArgs e)
{
if (_isClosing) return;
diff --git a/src/OverlayManager.cs b/src/OverlayManager.cs
index 0e92050..1073961 100644
--- a/src/OverlayManager.cs
+++ b/src/OverlayManager.cs
@@ -130,9 +130,27 @@ public void Start()
SystemEvents.SessionSwitch += HandleSessionSwitch;
}
- public void UpdateColor(string color) => ForEachOverlay(w => w.UpdateColor(color));
- public void UpdateEffectSettings(double scale, double opacity, double trailSpeed, double clickSpeed) =>
+ public void UpdateColor(string color)
+ {
+ if (ConfigManager.IsUsingCustomHtml)
+ {
+ return;
+ }
+
+ ForEachOverlay(w => w.UpdateColor(color));
+ }
+
+ public void UpdateEffectSettings(double scale, double opacity, double trailSpeed, double clickSpeed)
+ {
+ if (ConfigManager.IsUsingCustomHtml)
+ {
+ return;
+ }
+
ForEachOverlay(w => w.UpdateEffectSettings(scale, opacity, trailSpeed, clickSpeed));
+ }
+
+ public void ReloadAllEffectContent() => ForEachOverlay(w => w.ReloadEffectContent());
public void UpdateTrailRefreshRate(int hz)
{
hz = Math.Clamp(hz, 10, 240);
diff --git a/src/Resources/Strings.en.resx b/src/Resources/Strings.en.resx
index 2821245..77fcacd 100644
--- a/src/Resources/Strings.en.resx
+++ b/src/Resources/Strings.en.resx
@@ -535,6 +535,30 @@ Download now?
Trail refresh rate
+
+ Custom HTML effect
+
+
+ Select HTML file
+
+
+ Use built-in effect
+
+
+ Built-in visual controls below are disabled while a custom HTML file is in use.
+
+
+ (Built-in effect)
+
+
+ HTML files (*.html;*.htm)|*.html;*.htm
+
+
+ Select effect HTML file
+
+
+ index.html is the built-in default effect and cannot be used as a custom file. Copy custom-effect-template.html and edit it, or choose your own HTML.
+
WebView2 initialization failed: {0}
diff --git a/src/Resources/Strings.ja.resx b/src/Resources/Strings.ja.resx
index c1e935e..3110319 100644
--- a/src/Resources/Strings.ja.resx
+++ b/src/Resources/Strings.ja.resx
@@ -535,6 +535,30 @@
トレイル更新率
+
+ カスタム HTML エフェクト
+
+
+ HTML ファイルを選択
+
+
+ 組み込みエフェクトに戻す
+
+
+ カスタム HTML 使用時は、下の組み込みビジュアル設定は無効になります。
+
+
+ (組み込みエフェクト)
+
+
+ HTML ファイル (*.html;*.htm)|*.html;*.htm
+
+
+ エフェクト用 HTML を選択
+
+
+ index.html は組み込みの既定エフェクトのため、カスタムファイルとして選べません。custom-effect-template.html をコピーして編集するか、独自の HTML を選んでください。
+
WebView2 の初期化に失敗しました: {0}
diff --git a/src/Resources/Strings.resx b/src/Resources/Strings.resx
index 67d504e..5465852 100644
--- a/src/Resources/Strings.resx
+++ b/src/Resources/Strings.resx
@@ -535,6 +535,30 @@
拖尾刷新率
+
+ 自定义 HTML 特效
+
+
+ 选择 HTML 文件
+
+
+ 恢复内置特效
+
+
+ 使用自定义 HTML 时,下方内置视觉调节将不可用。
+
+
+ (使用内置特效)
+
+
+ HTML 文件 (*.html;*.htm)|*.html;*.htm
+
+
+ 选择特效 HTML 文件
+
+
+ index.html 为软件内置默认特效,不能作为自定义文件选用。请复制 custom-effect-template.html 后修改,或选择您自己的 HTML。
+
WebView2 初始化失败: {0}
diff --git a/src/UiLocalizer.cs b/src/UiLocalizer.cs
index c0aa609..7514f91 100644
--- a/src/UiLocalizer.cs
+++ b/src/UiLocalizer.cs
@@ -91,6 +91,10 @@ public static void ApplyControlPanel(ControlPanelWindow w)
w.TxtTrailRefresh.Text = Localization.Get("Visual_TrailRefresh");
w.TxtEffectColor.Text = Localization.Get("Visual_Color");
w.BtnPickColor.Content = Localization.Get("Visual_ChangeColor");
+ w.TxtCustomHtmlTitle.Text = Localization.Get("CustomHtml_Title");
+ w.BtnBrowseCustomHtml.Content = Localization.Get("CustomHtml_Browse");
+ w.BtnClearCustomHtml.Content = Localization.Get("CustomHtml_ClearBuiltin");
+ w.TxtCustomHtmlHint.Text = Localization.Get("CustomHtml_Hint");
w.TxtFilterTitle.Text = Localization.Get("Filter_Title");
w.CheckEnvironmentFilter.Content = Localization.Get("Filter_Enable");
diff --git a/src/Web/custom-effect-template.html b/src/Web/custom-effect-template.html
new file mode 100644
index 0000000..81a1322
--- /dev/null
+++ b/src/Web/custom-effect-template.html
@@ -0,0 +1,107 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Web/custom-effect-test.html b/src/Web/custom-effect-test.html
new file mode 100644
index 0000000..fc00a63
--- /dev/null
+++ b/src/Web/custom-effect-test.html
@@ -0,0 +1,125 @@
+
+
+
+
+
+
+
+
+
+
+