Skip to content
Open
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
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 ? "开启" : "关闭");
}
}
45 changes: 41 additions & 4 deletions Actions/SwitchSystemAccentColorAction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Microsoft.Extensions.Logging;
using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
Expand Down Expand Up @@ -35,13 +36,15 @@ protected override async Task OnInvoke()
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 _);
Expand All @@ -66,4 +69,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
3 changes: 3 additions & 0 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 @@ -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 内存自动清理执行失败,将在下次周期继续。");
}
}
}
10 changes: 10 additions & 0 deletions SettingsPage/MoreFeaturesOptionsSettingsPage.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@
</controls:SettingsExpander.Footer>
</controls:SettingsExpander>

<controls:SettingsExpander IconSource="{ci:FluentIconSource &#xE9D9;}"
Header="自动清理 ClassIsland 内存"
Description="开启后每30秒检测一次;当内存占用超过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>
12 changes: 12 additions & 0 deletions SettingsPage/MoreFeaturesOptionsSettingsPage.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,16 @@ private void AutoOpenUsbToggle_OnChanged(object? sender, RoutedEventArgs e)
service.ApplyConfig();
GlobalConstants.MainConfig?.Save();
}

private void AutoCleanupMemoryToggle_OnChanged(object? sender, RoutedEventArgs e)
{
if (sender is Avalonia.Controls.ToggleSwitch toggleSwitch)
{
Config.AutoCleanupClassIslandMemory = toggleSwitch.IsChecked == true;
}

var service = ClassIsland.Shared.IAppHost.GetService<ClassIslandMemoryAutoCleanupService>();
service.ApplyConfig();
GlobalConstants.MainConfig?.Save();
}
}
Loading