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
69 changes: 69 additions & 0 deletions Actions/SwitchSystemAccentColorAction.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using ClassIsland.Core.Abstractions.Automation;
using ClassIsland.Core.Attributes;
using Microsoft.Extensions.Logging;
using Microsoft.Win32;
using System;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using SystemTools.Settings;

namespace SystemTools.Actions;

[ActionInfo("SystemTools.SwitchSystemAccentColor", "切换系统强调色", "\uE790", false)]
public class SwitchSystemAccentColorAction(ILogger<SwitchSystemAccentColorAction> logger) : ActionBase<AccentColorSettings>
{
private readonly ILogger<SwitchSystemAccentColorAction> _logger = logger;

[DllImport("user32.dll", SetLastError = true)]
static extern IntPtr SendMessageTimeout(IntPtr hWnd, uint Msg, UIntPtr wParam, string lParam, uint fuFlags, uint uTimeout, out UIntPtr lpdwResult);

const uint HWND_BROADCAST = 0xFFFF;
const uint WM_SETTINGCHANGE = 0x001A;
const uint SMTO_ABORTIFHUNG = 0x0002;

protected override async Task OnInvoke()
{
if (Settings == null || string.IsNullOrWhiteSpace(Settings.ColorHex)) return;

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("ColorPrevalence", 1, RegistryValueKind.DWord);

using var explorerKey = Registry.CurrentUser.CreateSubKey(@"Software\Microsoft\Windows\CurrentVersion\Explorer\Accent");
explorerKey?.SetValue("AccentColorMenu", dword, RegistryValueKind.DWord);

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

_logger.LogInformation("系统强调色已切换为 {Color}", Settings.ColorHex);
}
catch (Exception ex)
{
_logger.LogError(ex, "切换系统强调色失败");
throw;
}

await base.OnInvoke();
}

private static (byte A, byte R, byte G, byte B) ParseColor(string colorHex)
{
var hex = colorHex.Trim().TrimStart('#');
if (hex.Length == 6) hex = "FF" + hex;
if (hex.Length != 8) throw new FormatException("颜色格式无效");

var value = uint.Parse(hex, NumberStyles.HexNumber, CultureInfo.InvariantCulture);
return ((byte)(value >> 24), (byte)(value >> 16), (byte)(value >> 8), (byte)value);
}
}
34 changes: 34 additions & 0 deletions Controls/AccentColorSettingsControl.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using Avalonia.Controls;
using Avalonia.Layout;
using ClassIsland.Core.Abstractions.Controls;
using SystemTools.Settings;

namespace SystemTools.Controls;

public class AccentColorSettingsControl : ActionSettingsControlBase<AccentColorSettings>
{
private readonly ColorPicker _colorPicker;

public AccentColorSettingsControl()
{
var panel = new StackPanel { Spacing = 10, Margin = new(10) };
panel.Children.Add(new TextBlock { Text = "切换系统强调色", FontSize = 14, FontWeight = Avalonia.Media.FontWeight.Bold });

_colorPicker = new ColorPicker
{
IsAlphaEnabled = false,
HorizontalAlignment = HorizontalAlignment.Left
};
_colorPicker.ColorChanged += (_, _) => Settings.ColorHex = _colorPicker.Color.ToString();

panel.Children.Add(_colorPicker);
Content = panel;
}

protected override void OnInitialized()
{
base.OnInitialized();
if (Avalonia.Media.Color.TryParse(Settings.ColorHex, out var color))
_colorPicker.Color = color;
}
}
34 changes: 22 additions & 12 deletions Plugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ private void RegisterBaseActions(IServiceCollection services)
RegisterActionIfEnabled<ChangeWallpaperAction, ChangeWallpaperSettingsControl>(services, config,
"SystemTools.ChangeWallpaper");
RegisterActionIfEnabled<SwitchThemeAction, ThemeSettingsControl>(services, config, "SystemTools.SwitchTheme");
RegisterActionIfEnabled<SwitchSystemAccentColorAction, AccentColorSettingsControl>(services, config, "SystemTools.SwitchSystemAccentColor");

// 实用工具
RegisterActionIfEnabled<ScreenShotAction, ScreenShotSettingsControl>(services, config,
Expand Down Expand Up @@ -491,7 +492,7 @@ private void BuildBaseActionTree()
}

// 系统个性化
if (HasAnyActionEnabled(config, "SystemTools.ChangeWallpaper", "SystemTools.SwitchTheme"))
if (HasAnyActionEnabled(config, "SystemTools.ChangeWallpaper", "SystemTools.SwitchTheme", "SystemTools.SwitchSystemAccentColor"))
{
IActionService.ActionMenuTree["SystemTools 行动"].Add(new ActionMenuTreeGroup("系统个性化…", "\uF42F"));
BuildPersonalizationMenu(config);
Expand Down Expand Up @@ -625,35 +626,42 @@ private static bool HandleInTimePeriodRule(object? settings)
return current >= start || current <= end;
}

private static DateTime _lastMediaRuleCheckAt = DateTime.MinValue;
private static bool _lastMediaRuleResult;

private static bool HandleMediaMusicPlayingRule(object? settings)
{
if (!OperatingSystem.IsWindowsVersionAtLeast(10, 0, 17134))
{
return false;
}

var now = DateTime.UtcNow;
if (now - _lastMediaRuleCheckAt < TimeSpan.FromMilliseconds(800))
{
return _lastMediaRuleResult;
}

try
{
var manager = WinMedia.GlobalSystemMediaTransportControlsSessionManager.RequestAsync()
.AsTask().GetAwaiter().GetResult();

if (manager == null)
return false;

var sessions = manager.GetSessions();
if (sessions == null || sessions.Count == 0)
return false;

return sessions.Any(session =>
var sessions = manager?.GetSessions();
var isPlaying = sessions != null && sessions.Any(session =>
{
var playbackInfo = session.GetPlaybackInfo();
return playbackInfo != null &&
playbackInfo.PlaybackStatus == WinMedia.GlobalSystemMediaTransportControlsSessionPlaybackStatus.Playing;
return playbackInfo?.PlaybackStatus == WinMedia.GlobalSystemMediaTransportControlsSessionPlaybackStatus.Playing;
});

_lastMediaRuleResult = isPlaying;
_lastMediaRuleCheckAt = now;
return isPlaying;
}
catch
{
return false;
_lastMediaRuleCheckAt = now;
return _lastMediaRuleResult;
Comment on lines +663 to +664
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Clear cached playing state when SMTC query throws

If media was previously detected as playing, this catch path returns that stale true value on every SMTC failure, so the rule can remain permanently true after playback has already stopped whenever RequestAsync()/GetSessions() starts throwing (for example, intermittent WinRT/permission failures). Because this rule drives automations, returning the last positive state on exceptions can cause repeated false triggers; the failure path should invalidate or age out the cached result instead of reusing it indefinitely.

Useful? React with 👍 / 👎.

}
}

Expand Down Expand Up @@ -768,6 +776,8 @@ private void BuildPersonalizationMenu(MainConfigData config)
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"));
Comment thread
Programmer-MrWang marked this conversation as resolved.

if (items.Count > 0)
{
Expand Down
31 changes: 24 additions & 7 deletions Services/AdaptiveThemeSyncService.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Avalonia.Threading;
using ClassIsland.Core;
using ClassIsland.Core.Abstractions.Services;
using System.Reflection;
using Microsoft.Extensions.Logging;
using System;
using System.Drawing;
Expand Down Expand Up @@ -56,13 +57,7 @@ private void OnTick(object? sender, EventArgs e)
return;
}

var themeService = IAppHost.TryGetService<IThemeService>();
if (themeService == null)
{
return;
}

themeService.SetTheme(targetTheme.Value, null);
ApplyThemeLikeAppSettings(targetTheme.Value);
_lastAppliedTheme = targetTheme;
_logger.LogDebug("已自动匹配主题为:{Theme}", targetTheme == 1 ? "黑暗" : "明亮");
}
Expand Down Expand Up @@ -178,6 +173,28 @@ private static bool ResolveUseTopAreaFromClassIslandSettings()
return null;
}

private static void ApplyThemeLikeAppSettings(int targetTheme)
{
var settingsServiceObj = IAppHost.TryGetService<object>();
if (settingsServiceObj != null)
{
var type = settingsServiceObj.GetType();
if (type.Name == "SettingsService")
Comment on lines +178 to +182
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Resolve settings service instead of requesting object

This lookup asks DI for object, which typically does not resolve to arbitrary registered services in the default .NET container, so the reflection branch is effectively unreachable and the method always falls back to IThemeService.SetTheme(...). That means the new “apply like app settings” synchronization logic does not actually run, so the intended fix for theme-state consistency is not applied.

Useful? React with 👍 / 👎.

{
var settingsProp = type.GetProperty("Settings", BindingFlags.Instance | BindingFlags.Public);
var settings = settingsProp?.GetValue(settingsServiceObj);
var themeProp = settings?.GetType().GetProperty("Theme", BindingFlags.Instance | BindingFlags.Public);
if (themeProp?.CanWrite == true)
{
themeProp.SetValue(settings, targetTheme);
return;
}
}
}

IAppHost.TryGetService<IThemeService>()?.SetTheme(targetTheme, null);
}

private static string[] GetPossibleClassIslandSettingsPaths()
{
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
Expand Down
8 changes: 8 additions & 0 deletions Settings/AccentColorSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using System.Text.Json.Serialization;

namespace SystemTools.Settings;

public class AccentColorSettings
{
[JsonPropertyName("colorHex")] public string ColorHex { get; set; } = "#FF0078D4";
}
1 change: 1 addition & 0 deletions SettingsPage/SystemToolsSettingsViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ public void InitializeFeatureItems()
("SystemTools.Delete", "删除", "文件操作"),
("SystemTools.ChangeWallpaper", "切换壁纸", "系统个性化"),
("SystemTools.SwitchTheme", "切换主题色", "系统个性化"),
("SystemTools.SwitchSystemAccentColor", "切换系统强调色", "系统个性化"),
("SystemTools.FullscreenClock", "沉浸式时钟", "其他工具"),
("SystemTools.KillProcess", "退出进程", "实用工具"),
("SystemTools.ScreenShot", "屏幕截图", "实用工具"),
Expand Down
Loading