Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion Actions/BackgroundPlayAudioAction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
using System;
using System.IO;
using System.Threading.Tasks;
using ClassIsland.Shared;
using SystemTools.Settings;

namespace SystemTools.Actions;
Expand Down
25 changes: 25 additions & 0 deletions Actions/ShowFloatingWindowAction.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Concurrent;
using System.Threading.Tasks;
using ClassIsland.Core.Abstractions.Automation;
using ClassIsland.Core.Attributes;
Expand All @@ -16,6 +17,7 @@ public class ShowFloatingWindowAction(
{
private readonly ILogger<ShowFloatingWindowAction> _logger = logger;
private readonly FloatingWindowService _floatingWindowService = floatingWindowService;
private static readonly ConcurrentDictionary<Guid, bool> PreviousStates = new();

protected override async Task OnInvoke()
{
Expand All @@ -24,6 +26,12 @@ protected override async Task OnInvoke()
try
{
var shouldShow = Settings.ShowFloatingWindow;

if (IsRevertable)
{
PreviousStates[ActionSet.Guid] = GlobalConstants.MainConfig!.Data.ShowFloatingWindow;
}

GlobalConstants.MainConfig!.Data.ShowFloatingWindow = shouldShow;
GlobalConstants.MainConfig.Save();
_floatingWindowService.UpdateWindowState();
Expand All @@ -39,4 +47,21 @@ protected override async Task OnInvoke()
await base.OnInvoke();
_logger.LogDebug("ShowFloatingWindowAction OnInvoke 完成");
}

protected override async Task OnRevert()
{
await base.OnRevert();

if (!PreviousStates.TryRemove(ActionSet.Guid, out var previousState))
{
_logger.LogInformation("未找到恢复快照,跳过悬浮窗恢复。ActionSet={ActionSetGuid}", ActionSet.Guid);
return;
}

GlobalConstants.MainConfig!.Data.ShowFloatingWindow = previousState;
GlobalConstants.MainConfig.Save();
_floatingWindowService.UpdateWindowState();

_logger.LogInformation("已恢复悬浮窗状态为: {State}", previousState ? "开启" : "关闭");
}
}
50 changes: 42 additions & 8 deletions Actions/SwitchSystemAccentColorAction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@
using Microsoft.Extensions.Logging;
using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using SystemTools.Settings;

namespace SystemTools.Actions;

[ActionInfo("SystemTools.SwitchSystemAccentColor", "切换系统强调色", "\uE790", false)]
[ActionInfo("SystemTools.SwitchSystemAccentColor", "切换系统强调色", "\uE523", false)]
public class SwitchSystemAccentColorAction(ILogger<SwitchSystemAccentColorAction> logger) : ActionBase<AccentColorSettings>
{
private readonly ILogger<SwitchSystemAccentColorAction> _logger = logger;
Expand All @@ -29,21 +30,20 @@ protected override async Task OnInvoke()
try
{
var color = ParseColor(Settings.ColorHex);
// Windows 使用 ABGR 格式(低位字节是 R)
var dword = ((uint)color.A << 24) | ((uint)color.B << 16) | ((uint)color.G << 8) | color.R;
// ColorizationColor 通常使用 C4 (196) 作为 Alpha
var colorizationDword = (0xC4u << 24) | ((uint)color.B << 16) | ((uint)color.G << 8) | color.R;

using var dwmKey = Registry.CurrentUser.CreateSubKey(@"Software\Microsoft\Windows\DWM");
dwmKey?.SetValue("AccentColor", dword, RegistryValueKind.DWord);
dwmKey?.SetValue("ColorizationColor", colorizationDword, RegistryValueKind.DWord);
dwmKey?.SetValue("ColorizationAfterglow", colorizationDword, RegistryValueKind.DWord);
dwmKey?.SetValue("AccentColor", unchecked((int)dword), RegistryValueKind.DWord);
dwmKey?.SetValue("ColorizationColor", unchecked((int)colorizationDword), RegistryValueKind.DWord);
dwmKey?.SetValue("ColorizationAfterglow", unchecked((int)colorizationDword), RegistryValueKind.DWord);
dwmKey?.SetValue("ColorPrevalence", 1, RegistryValueKind.DWord);

using var explorerKey = Registry.CurrentUser.CreateSubKey(@"Software\Microsoft\Windows\CurrentVersion\Explorer\Accent");
explorerKey?.SetValue("AccentColorMenu", dword, RegistryValueKind.DWord);
explorerKey?.SetValue("AccentColorMenu", unchecked((int)dword), RegistryValueKind.DWord);
explorerKey?.SetValue("StartColorMenu", unchecked((int)dword), RegistryValueKind.DWord);
explorerKey?.SetValue("AccentPalette", BuildAccentPalette(color.R, color.G, color.B), RegistryValueKind.Binary);

// 通知 Windows 刷新主题色
SendMessageTimeout((IntPtr)HWND_BROADCAST, WM_SETTINGCHANGE, (UIntPtr)0, "ImmersiveColorSet", SMTO_ABORTIFHUNG, 5000, out _);

_logger.LogInformation("系统强调色已切换为 {Color}", Settings.ColorHex);
Expand All @@ -66,4 +66,38 @@ private static (byte A, byte R, byte G, byte B) ParseColor(string colorHex)
var value = uint.Parse(hex, NumberStyles.HexNumber, CultureInfo.InvariantCulture);
return ((byte)(value >> 24), (byte)(value >> 16), (byte)(value >> 8), (byte)value);
}

private static byte[] BuildAccentPalette(byte r, byte g, byte b)
{
var colors = new List<(byte R, byte G, byte B)>
{
Scale(r, g, b, 0.60),
Scale(r, g, b, 0.75),
Scale(r, g, b, 0.90),
(r, g, b),
Scale(r, g, b, 1.10),
Scale(r, g, b, 1.25),
Scale(r, g, b, 1.40),
Scale(r, g, b, 1.55)
};

var palette = new byte[32];
for (var i = 0; i < colors.Count; i++)
{
var c = colors[i];
var p = i * 4;
palette[p] = c.R;
palette[p + 1] = c.G;
palette[p + 2] = c.B;
palette[p + 3] = 0xFF;
}

return palette;
}

private static (byte R, byte G, byte B) Scale(byte r, byte g, byte b, double factor)
{
static byte ClampToByte(double v) => (byte)Math.Clamp((int)Math.Round(v), 0, 255);
return (ClampToByte(r * factor), ClampToByte(g * factor), ClampToByte(b * factor));
}
}
15 changes: 15 additions & 0 deletions ConfigHandlers/MainConfigData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,21 @@ public bool AutoOpenUsbDriveOnInsert
}



bool _autoCleanupClassIslandMemory;

[JsonPropertyName("autoCleanupClassIslandMemory")]
public bool AutoCleanupClassIslandMemory
{
get => _autoCleanupClassIslandMemory;
set
{
if (value == _autoCleanupClassIslandMemory) return;
_autoCleanupClassIslandMemory = value;
OnPropertyChanged();
}
}

bool _autoHideMainWindowWhenOccluded;

[JsonPropertyName("autoHideMainWindowWhenOccluded")]
Expand Down
9 changes: 4 additions & 5 deletions Controls/ShowFloatingWindowSettingsControl.cs
Original file line number Diff line number Diff line change
@@ -1,26 +1,25 @@
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Controls.Primitives;
using ClassIsland.Core.Abstractions.Controls;
using SystemTools.Settings;

namespace SystemTools.Controls;

public class ShowFloatingWindowSettingsControl : ActionSettingsControlBase<ShowFloatingWindowSettings>
{
private CheckBox _toggleCheckBox;
private readonly ToggleSwitch _toggleSwitch;

public ShowFloatingWindowSettingsControl()
{
var panel = new StackPanel { Spacing = 10, Margin = new(10) };

_toggleCheckBox = new CheckBox
_toggleSwitch = new ToggleSwitch
{
Content = "显示悬浮窗",
IsChecked = true
};

panel.Children.Add(_toggleCheckBox);
panel.Children.Add(_toggleSwitch);

Content = panel;
}
Expand All @@ -29,7 +28,7 @@ protected override void OnInitialized()
{
base.OnInitialized();

_toggleCheckBox[!ToggleButton.IsCheckedProperty] = new Binding(nameof(Settings.ShowFloatingWindow))
_toggleSwitch[!ToggleSwitch.IsCheckedProperty] = new Binding(nameof(Settings.ShowFloatingWindow))
{
Source = Settings,
Mode = BindingMode.TwoWay
Expand Down
7 changes: 5 additions & 2 deletions Plugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
services.AddSingleton<FloatingWindowService>();
services.AddSingleton<AdaptiveThemeSyncService>();
services.AddSingleton<UsbAutoPlayService>();
services.AddSingleton<ClassIslandMemoryAutoCleanupService>();

// ========== 注册可选人脸识别 ==========
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
Expand Down Expand Up @@ -120,6 +121,7 @@
}
IAppHost.GetService<AdaptiveThemeSyncService>().Start();
IAppHost.GetService<UsbAutoPlayService>().Start();
IAppHost.GetService<ClassIslandMemoryAutoCleanupService>().ApplyConfig();
_logger = IAppHost.GetService<ILogger<Plugin>>();

_logger?.LogInformation("[SystemTools]实验性功能状态: {Status}", experimentalEnabled);
Expand All @@ -129,7 +131,7 @@
_logger?.LogWarning("[SystemTools]FFmpeg 功能已自动关闭:缺少依赖文件 ffmpeg.exe。");
}

if (GlobalConstants.MainConfig.Data.EnableFaceRecognition)

Check warning on line 134 in Plugin.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.
{
if (_faceRecognitionRegistered)
{
Expand Down Expand Up @@ -776,8 +778,8 @@
items.Add(new ActionMenuTreeItem("SystemTools.ChangeWallpaper", "切换壁纸", "\uE9BC"));
if (config.IsActionEnabled("SystemTools.SwitchTheme"))
items.Add(new ActionMenuTreeItem("SystemTools.SwitchTheme", "切换主题色", "\uF42F"));
if (config.IsActionEnabled("SystemTools.SwitchSystemAccentColor"))
items.Add(new ActionMenuTreeItem("SystemTools.SwitchSystemAccentColor", "切换系统强调色", "\uE790"));
//if (config.IsActionEnabled("SystemTools.SwitchSystemAccentColor"))
// items.Add(new ActionMenuTreeItem("SystemTools.SwitchSystemAccentColor", "切换系统强调色", "\uE523"));

if (items.Count > 0)
{
Expand Down Expand Up @@ -906,6 +908,7 @@
{
IAppHost.GetService<AdaptiveThemeSyncService>().Stop();
IAppHost.GetService<UsbAutoPlayService>().Stop();
IAppHost.GetService<ClassIslandMemoryAutoCleanupService>().Stop();
AdvancedShutdownAction.CancelPlanOnAppStopping();
if (GlobalConstants.MainConfig?.Data.EnableFloatingWindowFeature == true)
{
Expand Down
127 changes: 127 additions & 0 deletions Services/ClassIslandMemoryAutoCleanupService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
using ClassIsland.Core;
using ClassIsland.Shared;
using Microsoft.Extensions.Logging;
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using SystemTools.Shared;

namespace SystemTools.Services;

public class ClassIslandMemoryAutoCleanupService(ILogger<ClassIslandMemoryAutoCleanupService> logger)
{
private readonly ILogger<ClassIslandMemoryAutoCleanupService> _logger = logger;
private readonly object _sync = new();
private CancellationTokenSource? _cts;
private Task? _workerTask;

private const long ThresholdBytes = 500L * 1024 * 1024;

[DllImport("psapi.dll", SetLastError = true)]
private static extern bool EmptyWorkingSet(IntPtr hProcess);

public void ApplyConfig()
{
var enabled = GlobalConstants.MainConfig?.Data.AutoCleanupClassIslandMemory == true;
if (enabled)
{
Start();
return;
}

Stop();
}

public void Start()
{
lock (_sync)
{
if (_workerTask is { IsCompleted: false })
{
return;
}

_cts = new CancellationTokenSource();
_workerTask = Task.Run(() => RunAsync(_cts.Token));
}
}

public void Stop()
{
CancellationTokenSource? cts;
Task? worker;
lock (_sync)
{
cts = _cts;
worker = _workerTask;
_cts = null;
_workerTask = null;
}

if (cts == null)
{
return;
}

try { cts.Cancel(); } catch { }
cts.Dispose();

if (worker != null)
{
try { worker.Wait(1000); } catch { }
}
}

private async Task RunAsync(CancellationToken cancellationToken)
{
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(30));

try
{
while (await timer.WaitForNextTickAsync(cancellationToken))
{
TryCleanupOnce();
}
}
catch (OperationCanceledException)
{
// Ignore cancellation.
}
}

private void TryCleanupOnce()
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return;
}

try
{
var process = Process.GetCurrentProcess();
process.Refresh();
var privateMemory = process.PrivateMemorySize64;

if (privateMemory <= ThresholdBytes)
{
return;
}

var before = GC.GetTotalMemory(true);
GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, true);
GC.WaitForPendingFinalizers();
GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, true);
var after = GC.GetTotalMemory(true);

_ = EmptyWorkingSet(process.Handle);

_logger.LogInformation("ClassIsland 内存自动清理已执行。PrivateMemory={PrivateMemoryBytes}B ManagedBefore={ManagedBefore}B ManagedAfter={ManagedAfter}B", privateMemory, before, after);
}
catch (Exception ex)
{
_logger.LogDebug(ex, "ClassIsland 内存自动清理执行失败,将在下次周期继续。");
}
}
}
14 changes: 12 additions & 2 deletions SettingsPage/MoreFeaturesOptionsSettingsPage.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@
<ScrollViewer>
<StackPanel Classes="settings-container animated-intro">

<controls:SettingsExpander IconSource="{ci:FluentIconSource &#xE520;}"
<!--<controls:SettingsExpander IconSource="{ci:FluentIconSource &#xE520;}"
Header="自动匹配主界面背景色到 ClassIsland 主题"
Description="实时监测主界面所在区域色彩,偏黑则切换黑暗,偏白则切换明亮">
<controls:SettingsExpander.Footer>
<ToggleSwitch IsChecked="{Binding Config.AutoMatchMainBackgroundTheme, Mode=TwoWay}"
Checked="AutoMatchThemeToggle_OnChanged"
Unchecked="AutoMatchThemeToggle_OnChanged" />
</controls:SettingsExpander.Footer>
</controls:SettingsExpander>
</controls:SettingsExpander>-->

<controls:SettingsExpander IconSource="{ci:FluentIconSource &#xEE81;}"
Header="自动播放"
Expand All @@ -26,6 +26,16 @@
</controls:SettingsExpander.Footer>
</controls:SettingsExpander>

<controls:SettingsExpander IconSource="{ci:FluentIconSource &#xE97B;}"
Header="自动清理 ClassIsland 内存"
Description="当软件内存占用超过 500MB 时自动执行垃圾回收与工作集修剪。">
<controls:SettingsExpander.Footer>
<ToggleSwitch IsChecked="{Binding Config.AutoCleanupClassIslandMemory, Mode=TwoWay}"
Checked="AutoCleanupMemoryToggle_OnChanged"
Unchecked="AutoCleanupMemoryToggle_OnChanged" />
</controls:SettingsExpander.Footer>
</controls:SettingsExpander>

</StackPanel>
</ScrollViewer>
</ci:SettingsPageBase>
Loading
Loading