From fc73e766a2759e41e2adf5e1c7ce51a82e689b1e Mon Sep 17 00:00:00 2001 From: Wang Haoyu Date: Fri, 19 Jun 2026 17:40:01 +0800 Subject: [PATCH 1/4] Fix automation editors and time period rule UI --- Actions/SimulateKeyboardAction.cs | 70 ++++-- .../InTimePeriodRuleSettingsControl.axaml | 54 ++-- Controls/InTimePeriodRuleSettingsControl.cs | 38 ++- Controls/SimulateKeyboardSettingsControl.cs | 232 ++++++++++-------- Controls/SimulateMouseSettingsControl.cs | 104 +++++++- Services/FloatingWindowService.cs | 4 +- Settings/KeyboardInputSettings.cs | 22 +- 7 files changed, 360 insertions(+), 164 deletions(-) diff --git a/Actions/SimulateKeyboardAction.cs b/Actions/SimulateKeyboardAction.cs index 3e263bf6..a38e6f9a 100644 --- a/Actions/SimulateKeyboardAction.cs +++ b/Actions/SimulateKeyboardAction.cs @@ -2,7 +2,7 @@ using ClassIsland.Core.Attributes; using Microsoft.Extensions.Logging; using System; -using System.Runtime.InteropServices; +using System.Collections.Generic; using System.Threading.Tasks; using SystemTools.Settings; using Windows.Win32; @@ -20,7 +20,14 @@ protected override async Task OnInvoke() { _logger.LogDebug("SimulateKeyboardAction OnInvoke 开始"); - if (Settings == null || Settings.Keys == null || Settings.Keys.Count == 0) + if (Settings == null) + { + _logger.LogWarning("没有录制的按键"); + return; + } + + var actions = Settings.Actions.Count > 0 ? Settings.Actions : ConvertLegacyKeys(Settings.Keys); + if (actions.Count == 0) { _logger.LogWarning("没有录制的按键"); return; @@ -28,21 +35,28 @@ protected override async Task OnInvoke() try { - _logger.LogInformation("正在模拟 {Count} 个按键", Settings.Keys.Count); + _logger.LogInformation("正在模拟 {Count} 个按键操作", actions.Count); - for (int i = 0; i < Settings.Keys.Count; i++) + for (int i = 0; i < actions.Count; i++) { - if (byte.TryParse(Settings.Keys[i].Split(':')[0], out byte keyCode)) + var action = actions[i]; + await Task.Delay((int)action.Interval); + + switch (action.Type) { - PInvoke.keybd_event(keyCode, 0, 0, UIntPtr.Zero); - await Task.Delay(KEY_PRESS_DELAY); - PInvoke.keybd_event(keyCode, 0, - Windows.Win32.UI.Input.KeyboardAndMouse.KEYBD_EVENT_FLAGS.KEYEVENTF_KEYUP, UIntPtr.Zero); - - if (i < Settings.Keys.Count - 1) - { - await Task.Delay(KEY_INTERVAL_DELAY); - } + case KeyboardAction.ActionType.KeyDown: + PInvoke.keybd_event(action.KeyCode, 0, 0, UIntPtr.Zero); + break; + case KeyboardAction.ActionType.KeyUp: + PInvoke.keybd_event(action.KeyCode, 0, + Windows.Win32.UI.Input.KeyboardAndMouse.KEYBD_EVENT_FLAGS.KEYEVENTF_KEYUP, UIntPtr.Zero); + break; + default: + PInvoke.keybd_event(action.KeyCode, 0, 0, UIntPtr.Zero); + await Task.Delay(KEY_PRESS_DELAY); + PInvoke.keybd_event(action.KeyCode, 0, + Windows.Win32.UI.Input.KeyboardAndMouse.KEYBD_EVENT_FLAGS.KEYEVENTF_KEYUP, UIntPtr.Zero); + break; } } @@ -58,6 +72,34 @@ protected override async Task OnInvoke() _logger.LogDebug("SimulateKeyboardAction OnInvoke 完成"); } + private static List ConvertLegacyKeys(List? keys) + { + var actions = new List(); + if (keys == null) + { + return actions; + } + + foreach (var key in keys) + { + var parts = key.Split(':', 2); + if (!byte.TryParse(parts[0], out var keyCode)) + { + continue; + } + + actions.Add(new KeyboardAction + { + Type = KeyboardAction.ActionType.Press, + KeyCode = keyCode, + KeyName = parts.Length > 1 ? parts[1] : keyCode.ToString(), + Interval = actions.Count == 0 ? 0 : KEY_INTERVAL_DELAY + }); + } + + return actions; + } + //[DllImport("user32.dll", SetLastError = true)] //private static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, UIntPtr dwExtraInfo); diff --git a/Controls/InTimePeriodRuleSettingsControl.axaml b/Controls/InTimePeriodRuleSettingsControl.axaml index dfe0e05c..a4ea71b9 100644 --- a/Controls/InTimePeriodRuleSettingsControl.axaml +++ b/Controls/InTimePeriodRuleSettingsControl.axaml @@ -1,32 +1,28 @@ - - - - - - - - - - - - - + + + + + + - + diff --git a/Controls/InTimePeriodRuleSettingsControl.cs b/Controls/InTimePeriodRuleSettingsControl.cs index 8f5723f6..69fc9a4a 100644 --- a/Controls/InTimePeriodRuleSettingsControl.cs +++ b/Controls/InTimePeriodRuleSettingsControl.cs @@ -1,6 +1,6 @@ using System; using Avalonia.Controls; -using Avalonia.Markup.Xaml; +using Avalonia.Interactivity; using ClassIsland.Core.Abstractions.Controls; using SystemTools.Rules; @@ -11,41 +11,37 @@ public partial class InTimePeriodRuleSettingsControl : RuleSettingsControlBase SyncSettings(); - EndTimePicker.SelectedTimeChanged += (_, _) => SyncSettings(); } - private void InitializeComponent() + private void StartTimePicker_OnLoaded(object? sender, RoutedEventArgs e) { - AvaloniaXamlLoader.Load(this); - } - - protected override void OnInitialized() - { - base.OnInitialized(); - - if (TimeSpan.TryParse(Settings.StartTime, out var start)) + if (sender is TimePicker picker && TimeSpan.TryParse(Settings.StartTime, out var start)) { - StartTimePicker.SelectedTime = start; + picker.SelectedTime = start; } + } - if (TimeSpan.TryParse(Settings.EndTime, out var end)) + private void EndTimePicker_OnLoaded(object? sender, RoutedEventArgs e) + { + if (sender is TimePicker picker && TimeSpan.TryParse(Settings.EndTime, out var end)) { - EndTimePicker.SelectedTime = end; + picker.SelectedTime = end; } } - private void SyncSettings() + private void StartTimePicker_OnSelectedTimeChanged(object? sender, TimePickerSelectedValueChangedEventArgs e) { - if (StartTimePicker.SelectedTime.HasValue) + if (sender is TimePicker { SelectedTime: { } selectedTime }) { - Settings.StartTime = StartTimePicker.SelectedTime.Value.ToString(@"hh\:mm\:ss"); + Settings.StartTime = selectedTime.ToString(@"hh\:mm\:ss"); } + } - if (EndTimePicker.SelectedTime.HasValue) + private void EndTimePicker_OnSelectedTimeChanged(object? sender, TimePickerSelectedValueChangedEventArgs e) + { + if (sender is TimePicker { SelectedTime: { } selectedTime }) { - Settings.EndTime = EndTimePicker.SelectedTime.Value.ToString(@"hh\:mm\:ss"); + Settings.EndTime = selectedTime.ToString(@"hh\:mm\:ss"); } } } diff --git a/Controls/SimulateKeyboardSettingsControl.cs b/Controls/SimulateKeyboardSettingsControl.cs index 2934b026..98836e3a 100644 --- a/Controls/SimulateKeyboardSettingsControl.cs +++ b/Controls/SimulateKeyboardSettingsControl.cs @@ -3,8 +3,8 @@ using ClassIsland.Core.Abstractions.Controls; using System; using System.Collections.Generic; +using System.Diagnostics; using System.Runtime.InteropServices; -using System.Text; using SystemTools.Settings; using Windows.Win32; using Windows.Win32.Foundation; @@ -14,153 +14,193 @@ namespace SystemTools.Controls; public class SimulateKeyboardSettingsControl : ActionSettingsControlBase { - private Avalonia.Controls.Button _startButton; - private Avalonia.Controls.Button _stopButton; - private Avalonia.Controls.TextBox _keysTextBox; + private readonly Button _startButton; + private readonly Button _stopButton; + private readonly ListBox _actionsListBox; + private readonly ComboBox _typeBox; + private readonly TextBox _keyCodeBox; + private readonly TextBox _keyNameBox; + private readonly TextBox _intervalBox; private bool _isRecording; private HHOOK _hookId = HHOOK.Null; - private readonly List _recordedKeys = []; + private readonly List _recordedActions = []; + private readonly Stopwatch _stopwatch = new(); + private long _lastActionTime; private HOOKPROC? _hookProc; - //private delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam); - public SimulateKeyboardSettingsControl() { - var panel = new Avalonia.Controls.StackPanel { Spacing = 10, Margin = new(10) }; - - var buttonPanel = new Avalonia.Controls.StackPanel - { - Orientation = Avalonia.Layout.Orientation.Horizontal, - Spacing = 10 - }; - - _startButton = new Avalonia.Controls.Button - { - Content = "开始录制键盘输入", - Width = 150 - }; - _startButton.Click += (s, e) => StartRecording(); - - _stopButton = new Avalonia.Controls.Button - { - Content = "结束录制", - Width = 100, - IsVisible = false - }; - _stopButton.Click += (s, e) => StopRecording(); + var panel = new StackPanel { Spacing = 10, Margin = new(10) }; + var buttonPanel = new StackPanel { Orientation = Avalonia.Layout.Orientation.Horizontal, Spacing = 10 }; + _startButton = new Button { Content = "开始录制键盘输入", Width = 150 }; + _startButton.Click += (_, _) => StartRecording(); + _stopButton = new Button { Content = "结束录制", Width = 100, IsVisible = false }; + _stopButton.Click += (_, _) => StopRecording(); buttonPanel.Children.Add(_startButton); buttonPanel.Children.Add(_stopButton); panel.Children.Add(buttonPanel); - _keysTextBox = new Avalonia.Controls.TextBox - { - Height = 100, - IsReadOnly = true, - TextWrapping = Avalonia.Media.TextWrapping.Wrap, - Watermark = "录制的按键将显示在这里" - }; - panel.Children.Add(_keysTextBox); - + _actionsListBox = new ListBox { Height = 160 }; + _actionsListBox.SelectionChanged += (_, _) => LoadSelectedAction(); + panel.Children.Add(_actionsListBox); + + var editor = new StackPanel { Spacing = 6 }; + _typeBox = new ComboBox { ItemsSource = Enum.GetValues(), SelectedIndex = 0, MinWidth = 120 }; + _keyCodeBox = new TextBox { Watermark = "虚拟键码", Width = 90 }; + _keyNameBox = new TextBox { Watermark = "按键名称", Width = 120 }; + _intervalBox = new TextBox { Watermark = "延迟(ms)", Width = 90 }; + var row = new StackPanel { Orientation = Avalonia.Layout.Orientation.Horizontal, Spacing = 8 }; + row.Children.Add(new TextBlock { Text = "操作", VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center }); + row.Children.Add(_typeBox); + row.Children.Add(new TextBlock { Text = "键码", VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center }); + row.Children.Add(_keyCodeBox); + row.Children.Add(new TextBlock { Text = "名称", VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center }); + row.Children.Add(_keyNameBox); + row.Children.Add(new TextBlock { Text = "延迟", VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center }); + row.Children.Add(_intervalBox); + editor.Children.Add(row); + + var editButtons = new StackPanel { Orientation = Avalonia.Layout.Orientation.Horizontal, Spacing = 8 }; + AddButton(editButtons, "应用修改", ApplySelectedAction); + AddButton(editButtons, "新增", AddActionFromEditor); + AddButton(editButtons, "删除", DeleteSelectedAction); + editor.Children.Add(editButtons); + panel.Children.Add(editor); Content = panel; } protected override void OnInitialized() { base.OnInitialized(); - if (Settings.Keys != null) + _recordedActions.Clear(); + if (Settings.Actions.Count > 0) { - _recordedKeys.Clear(); - _recordedKeys.AddRange(Settings.Keys); - UpdateTextBox(); + _recordedActions.AddRange(Settings.Actions); } + else + { + foreach (var key in Settings.Keys) + { + var parts = key.Split(':', 2); + if (byte.TryParse(parts[0], out var keyCode)) + { + _recordedActions.Add(new KeyboardAction { KeyCode = keyCode, KeyName = parts.Length > 1 ? parts[1] : keyCode.ToString(), Interval = _recordedActions.Count == 0 ? 0 : 100 }); + } + } + } + SaveActions(); + UpdateListBox(); + } + + private static void AddButton(StackPanel panel, string text, Action action) + { + var button = new Button { Content = text }; + button.Click += (_, _) => action(); + panel.Children.Add(button); } private void StartRecording() { _isRecording = true; - _recordedKeys.Clear(); - UpdateTextBox(); - + _recordedActions.Clear(); + _lastActionTime = 0; + _stopwatch.Restart(); + UpdateListBox(); _startButton.IsVisible = false; _stopButton.IsVisible = true; - _keysTextBox.Watermark = "正在录制..."; - _hookProc = HookCallback; - _hookId = (HHOOK)SetHook(_hookProc); + _hookId = PInvoke.SetWindowsHookEx(WINDOWS_HOOK_ID.WH_KEYBOARD_LL, _hookProc, + PInvoke.GetModuleHandle(Process.GetCurrentProcess().MainModule?.ModuleName), 0); } private void StopRecording() { _isRecording = false; - + _stopwatch.Stop(); _startButton.IsVisible = true; _stopButton.IsVisible = false; - _keysTextBox.Watermark = "录制的按键将显示在这里"; - - PInvoke.UnhookWindowsHookEx(_hookId); - _hookId = HHOOK.Null; + if (_hookId != IntPtr.Zero) + { + PInvoke.UnhookWindowsHookEx(_hookId); + _hookId = HHOOK.Null; + } _hookProc = null; - - Settings.Keys = [.. _recordedKeys]; + SaveActions(); } private LRESULT HookCallback(int nCode, WPARAM wParam, LPARAM lParam) { - if (nCode >= 0 && _isRecording) + if (nCode >= 0 && _isRecording && wParam == 0x100) { var hookStruct = Marshal.PtrToStructure(lParam); - var keyCode = hookStruct.VkCode; - - if (wParam == 0x100) - { - var keyName = ((System.Windows.Forms.Keys)keyCode).ToString(); - _recordedKeys.Add($"{keyCode}:{keyName}"); - - Dispatcher.UIThread.Post(() => { UpdateTextBox(); }); - } + var currentTime = _stopwatch.ElapsedMilliseconds; + var interval = _lastActionTime == 0 ? 0 : currentTime - _lastActionTime; + _lastActionTime = currentTime; + var keyName = ((System.Windows.Forms.Keys)hookStruct.VkCode).ToString(); + _recordedActions.Add(new KeyboardAction { Type = KeyboardAction.ActionType.Press, KeyCode = (byte)hookStruct.VkCode, KeyName = keyName, Interval = interval }); + Dispatcher.UIThread.Post(() => { SaveActions(); UpdateListBox(); }); } - return PInvoke.CallNextHookEx(_hookId, nCode, wParam, lParam); } - private void UpdateTextBox() + private void LoadSelectedAction() { - var sb = new StringBuilder(); - foreach (var key in _recordedKeys) - { - var parts = key.Split(':'); - if (parts.Length > 1) - { - sb.Append(parts[1]).Append(" "); - } - } + if (_actionsListBox.SelectedIndex < 0 || _actionsListBox.SelectedIndex >= _recordedActions.Count) return; + var action = _recordedActions[_actionsListBox.SelectedIndex]; + _typeBox.SelectedItem = action.Type; + _keyCodeBox.Text = action.KeyCode.ToString(); + _keyNameBox.Text = action.KeyName; + _intervalBox.Text = action.Interval.ToString(); + } - _keysTextBox.Text = sb.ToString().Trim(); + private KeyboardAction? ReadEditor() + { + if (!byte.TryParse(_keyCodeBox.Text, out var keyCode) || !long.TryParse(_intervalBox.Text, out var interval)) return null; + return new KeyboardAction { Type = (KeyboardAction.ActionType)(_typeBox.SelectedItem ?? KeyboardAction.ActionType.Press), KeyCode = keyCode, KeyName = string.IsNullOrWhiteSpace(_keyNameBox.Text) ? keyCode.ToString() : _keyNameBox.Text!, Interval = Math.Max(0, interval) }; } - private IntPtr SetHook(HOOKPROC proc) + private void ApplySelectedAction() { - using var curProcess = System.Diagnostics.Process.GetCurrentProcess(); - using var curModule = curProcess.MainModule; - return PInvoke - .SetWindowsHookEx(WINDOWS_HOOK_ID.WH_KEYBOARD_LL, proc, PInvoke.GetModuleHandle(curModule?.ModuleName), 0) - .DangerousGetHandle(); + var action = ReadEditor(); + if (action == null || _actionsListBox.SelectedIndex < 0 || _actionsListBox.SelectedIndex >= _recordedActions.Count) return; + _recordedActions[_actionsListBox.SelectedIndex] = action; + SaveActions(); + UpdateListBox(); } - //[DllImport("user32.dll", SetLastError = true)] - //private static extern IntPtr SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hMod, uint dwThreadId); - // - //[DllImport("user32.dll", SetLastError = true)] - //private static extern bool UnhookWindowsHookEx(IntPtr hHook); - // - //[DllImport("user32.dll")] - //private static extern IntPtr CallNextHookEx(IntPtr hHook, int nCode, IntPtr wParam, IntPtr lParam); - // - //[DllImport("kernel32.dll")] - //private static extern IntPtr GetModuleHandle(string lpModuleName); + private void AddActionFromEditor() + { + var action = ReadEditor() ?? new KeyboardAction { Type = KeyboardAction.ActionType.Press, KeyCode = 13, KeyName = "Enter", Interval = 0 }; + _recordedActions.Add(action); + SaveActions(); + UpdateListBox(); + } + + private void DeleteSelectedAction() + { + if (_actionsListBox.SelectedIndex < 0 || _actionsListBox.SelectedIndex >= _recordedActions.Count) return; + _recordedActions.RemoveAt(_actionsListBox.SelectedIndex); + SaveActions(); + UpdateListBox(); + } - //private const int WH_KEYBOARD_LL = 13; + private void SaveActions() + { + Settings.Actions = [.. _recordedActions]; + Settings.Keys = _recordedActions.ConvertAll(x => $"{x.KeyCode}:{x.KeyName}"); + } + + private void UpdateListBox() + { + var items = new List(); + for (var i = 0; i < _recordedActions.Count; i++) + { + var a = _recordedActions[i]; + items.Add($"{i + 1}. [{a.Interval}ms] {a.Type} {a.KeyName} ({a.KeyCode})"); + } + _actionsListBox.ItemsSource = items; + } [StructLayout(LayoutKind.Sequential)] private struct Kbdllhookstruct @@ -171,4 +211,4 @@ private struct Kbdllhookstruct public uint Time; public UIntPtr DwExtraInfo; } -} \ No newline at end of file +} diff --git a/Controls/SimulateMouseSettingsControl.cs b/Controls/SimulateMouseSettingsControl.cs index 1345cd3f..c654d0df 100644 --- a/Controls/SimulateMouseSettingsControl.cs +++ b/Controls/SimulateMouseSettingsControl.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.Runtime.InteropServices; -using System.Text; using SystemTools.Settings; using Windows.Win32; using Windows.Win32.Foundation; @@ -18,6 +17,12 @@ public class SimulateMouseSettingsControl : ActionSettingsControlBase LoadSelectedAction(); panel.Children.Add(_actionsListBox); + var editor = new Avalonia.Controls.StackPanel { Spacing = 6 }; + var row = new Avalonia.Controls.StackPanel { Orientation = Avalonia.Layout.Orientation.Horizontal, Spacing = 8 }; + _typeBox = new Avalonia.Controls.ComboBox { ItemsSource = Enum.GetValues(), SelectedIndex = 0, MinWidth = 120 }; + _xBox = new Avalonia.Controls.TextBox { Watermark = "X", Width = 70 }; + _yBox = new Avalonia.Controls.TextBox { Watermark = "Y", Width = 70 }; + _scrollBox = new Avalonia.Controls.TextBox { Watermark = "滚动", Width = 70 }; + _intervalBox = new Avalonia.Controls.TextBox { Watermark = "延迟(ms)", Width = 90 }; + _dragEndBox = new Avalonia.Controls.CheckBox { Content = "拖动结束" }; + row.Children.Add(new Avalonia.Controls.TextBlock { Text = "操作", VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center }); + row.Children.Add(_typeBox); + row.Children.Add(new Avalonia.Controls.TextBlock { Text = "坐标", VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center }); + row.Children.Add(_xBox); + row.Children.Add(_yBox); + row.Children.Add(new Avalonia.Controls.TextBlock { Text = "滚动", VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center }); + row.Children.Add(_scrollBox); + row.Children.Add(new Avalonia.Controls.TextBlock { Text = "延迟", VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center }); + row.Children.Add(_intervalBox); + row.Children.Add(_dragEndBox); + editor.Children.Add(row); + var editButtons = new Avalonia.Controls.StackPanel { Orientation = Avalonia.Layout.Orientation.Horizontal, Spacing = 8 }; + AddEditorButton(editButtons, "应用修改", ApplySelectedAction); + AddEditorButton(editButtons, "新增", AddActionFromEditor); + AddEditorButton(editButtons, "删除", DeleteSelectedAction); + editor.Children.Add(editButtons); + panel.Children.Add(editor); + _disableMouseCheckBox = new Avalonia.Controls.CheckBox { Content = "运行期间禁用鼠标", @@ -76,6 +108,13 @@ public SimulateMouseSettingsControl() Content = panel; } + private static void AddEditorButton(Avalonia.Controls.StackPanel panel, string text, Action action) + { + var button = new Avalonia.Controls.Button { Content = text }; + button.Click += (_, _) => action(); + panel.Children.Add(button); + } + protected override void OnInitialized() { base.OnInitialized(); @@ -144,7 +183,7 @@ private void StopRecording() _mouseHookProc = null; _keyboardHookProc = null; - Settings.Actions = [.. _recordedActions]; + SaveActions(); } private LRESULT MouseHookCallback(int nCode, WPARAM wParam, LPARAM lParam) @@ -253,6 +292,67 @@ private MouseAction AddAction(MouseAction.ActionType type, int x, int y, int scr return action; } + + private void LoadSelectedAction() + { + if (_actionsListBox.SelectedIndex < 0 || _actionsListBox.SelectedIndex >= _recordedActions.Count) return; + var action = _recordedActions[_actionsListBox.SelectedIndex]; + _typeBox.SelectedItem = action.Type; + _xBox.Text = action.X.ToString(); + _yBox.Text = action.Y.ToString(); + _scrollBox.Text = action.ScrollDelta.ToString(); + _intervalBox.Text = action.Interval.ToString(); + _dragEndBox.IsChecked = action.IsDragEnd; + } + + private MouseAction? ReadEditor() + { + if (!int.TryParse(_xBox.Text, out var x) || !int.TryParse(_yBox.Text, out var y) || + !int.TryParse(_scrollBox.Text, out var scroll) || !long.TryParse(_intervalBox.Text, out var interval)) + { + return null; + } + + return new MouseAction + { + Type = (MouseAction.ActionType)(_typeBox.SelectedItem ?? MouseAction.ActionType.LeftClick), + X = x, + Y = y, + ScrollDelta = scroll, + Interval = Math.Max(0, interval), + IsDragEnd = _dragEndBox.IsChecked ?? false + }; + } + + private void ApplySelectedAction() + { + var action = ReadEditor(); + if (action == null || _actionsListBox.SelectedIndex < 0 || _actionsListBox.SelectedIndex >= _recordedActions.Count) return; + _recordedActions[_actionsListBox.SelectedIndex] = action; + SaveActions(); + UpdateListBox(); + } + + private void AddActionFromEditor() + { + _recordedActions.Add(ReadEditor() ?? new MouseAction()); + SaveActions(); + UpdateListBox(); + } + + private void DeleteSelectedAction() + { + if (_actionsListBox.SelectedIndex < 0 || _actionsListBox.SelectedIndex >= _recordedActions.Count) return; + _recordedActions.RemoveAt(_actionsListBox.SelectedIndex); + SaveActions(); + UpdateListBox(); + } + + private void SaveActions() + { + Settings.Actions = [.. _recordedActions]; + } + private void UpdateListBox() { var items = new List(); diff --git a/Services/FloatingWindowService.cs b/Services/FloatingWindowService.cs index 0f22fa4f..40c72334 100644 --- a/Services/FloatingWindowService.cs +++ b/Services/FloatingWindowService.cs @@ -506,7 +506,9 @@ private void RefreshWindowButtons() private List> GetOrderedRows() { - var values = _entries.Values.ToDictionary(x => x.ButtonId, x => x); + var values = _entries.Values + .GroupBy(x => x.ButtonId) + .ToDictionary(x => x.Key, x => x.First()); var order = _configHandler.Data.FloatingWindowButtonOrder ?? []; var orderedIds = values.Keys diff --git a/Settings/KeyboardInputSettings.cs b/Settings/KeyboardInputSettings.cs index 1815ed54..c48d98cb 100644 --- a/Settings/KeyboardInputSettings.cs +++ b/Settings/KeyboardInputSettings.cs @@ -6,4 +6,24 @@ namespace SystemTools.Settings; public class KeyboardInputSettings { [JsonPropertyName("keys")] public List Keys { get; set; } = []; -} \ No newline at end of file + + [JsonPropertyName("actions")] public List Actions { get; set; } = []; +} + +public class KeyboardAction +{ + public enum ActionType + { + Press, + KeyDown, + KeyUp + } + + [JsonPropertyName("type")] public ActionType Type { get; set; } = ActionType.Press; + + [JsonPropertyName("keyCode")] public byte KeyCode { get; set; } + + [JsonPropertyName("keyName")] public string KeyName { get; set; } = string.Empty; + + [JsonPropertyName("interval")] public long Interval { get; set; } +} From 5c4e64ada39e0aa47eea5aad546a02d0e4b9b3e6 Mon Sep 17 00:00:00 2001 From: Wang Haoyu Date: Fri, 19 Jun 2026 18:02:10 +0800 Subject: [PATCH 2/4] Fix automation editors and complete wallpaper features --- Actions/ChangeWallpaperAction.cs | 190 ++++++++++++------ .../BetterCarouselContainerComponent.axaml.cs | 16 +- ...tterCarouselContainerSettingsControl.axaml | 11 +- Controls/SimulateKeyboardSettingsControl.cs | 4 +- Controls/WallpaperSettingsControl.cs | 67 +++++- .../BetterCarouselContainerSettings.cs | 5 +- Plugin.cs | 122 +---------- Rules/Handlers/InTimePeriodRuleHandler.cs | 27 +++ .../Handlers/MediaMusicPlayingRuleHandler.cs | 47 +++++ Rules/Handlers/ProcessRunningRuleHandler.cs | 31 +++ Rules/Handlers/UsingClassPlanRuleHandler.cs | 26 +++ Rules/Handlers/UsingTimeLayoutRuleHandler.cs | 26 +++ Settings/WallpaperSettings.cs | 10 + 13 files changed, 393 insertions(+), 189 deletions(-) create mode 100644 Rules/Handlers/InTimePeriodRuleHandler.cs create mode 100644 Rules/Handlers/MediaMusicPlayingRuleHandler.cs create mode 100644 Rules/Handlers/ProcessRunningRuleHandler.cs create mode 100644 Rules/Handlers/UsingClassPlanRuleHandler.cs create mode 100644 Rules/Handlers/UsingTimeLayoutRuleHandler.cs diff --git a/Actions/ChangeWallpaperAction.cs b/Actions/ChangeWallpaperAction.cs index c2e09e90..3509bdf0 100644 --- a/Actions/ChangeWallpaperAction.cs +++ b/Actions/ChangeWallpaperAction.cs @@ -4,7 +4,6 @@ using Microsoft.Win32; using System; using System.ComponentModel; -using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; using System.Threading.Tasks; @@ -22,13 +21,19 @@ protected override async Task OnInvoke() { _logger.LogDebug("ChangeWallpaperAction OnInvoke 开始"); - if (Settings == null || string.IsNullOrWhiteSpace(Settings.ImagePath)) + if (Settings == null) + { + _logger.LogWarning("壁纸设置为空"); + return; + } + + if (Settings.Mode == ChangeWallpaperMode.Image && string.IsNullOrWhiteSpace(Settings.ImagePath)) { _logger.LogWarning("图片路径为空"); return; } - if (!File.Exists(Settings.ImagePath)) + if (Settings.Mode == ChangeWallpaperMode.Image && !File.Exists(Settings.ImagePath)) { _logger.LogError("图片文件不存在: {Path}", Settings.ImagePath); throw new FileNotFoundException("指定的图片文件不存在", Settings.ImagePath); @@ -36,67 +41,33 @@ protected override async Task OnInvoke() try { - var imagePath = Settings.ImagePath; - var fit = Settings.FitStyle; - _logger.LogInformation("正在切换壁纸到: {Path}, FitStyle: {Fit}", imagePath, fit); - - // 根据 fitStyle 计算注册表值(TileWallpaper, WallpaperStyle) - var (tileValue, styleValue) = fit switch + if (Settings.Mode == ChangeWallpaperMode.SolidColor) { - 0 => ("1", "1"), // 平铺:TileWallpaper=1, WallpaperStyle=1 - 1 => ("0", "0"), // 居中:TileWallpaper=0, WallpaperStyle=0 - 2 => ("0", "2"), // 拉伸:TileWallpaper=0, WallpaperStyle=2 - 3 => ("0", "6"), // 填充:TileWallpaper=0, WallpaperStyle=6 - 4 => ("0", "10"), // 适应:TileWallpaper=0, WallpaperStyle=10 - 5 => ("0", "22"), // 跨区:TileWallpaper=0, WallpaperStyle=22 - _ => ("0", "2") // 默认:拉伸 - }; - - // 在后台线程执行可能阻塞的注册表与系统 API 操作,避免阻塞宿主 UI - await Task.Run(() => + var color = ParseColor(Settings.SolidColor); + _logger.LogInformation("正在切换为纯色壁纸: {Color}", Settings.SolidColor); + await Task.Run(() => SetSolidColorWallpaper(color)); + _logger.LogInformation("切换纯色壁纸成功: {Color}", Settings.SolidColor); + } + else { - // 1. 修改注册表以设置契合度 - using (var desktopRegKey = Registry.CurrentUser.OpenSubKey("Control Panel\\Desktop", true)) - { - if (desktopRegKey == null) - { - throw new Win32Exception("无法访问系统桌面注册表配置项,操作失败。"); - } - - desktopRegKey.SetValue("TileWallpaper", tileValue, RegistryValueKind.String); - desktopRegKey.SetValue("WallpaperStyle", styleValue, RegistryValueKind.String); - } - - // 2. 调用 SystemParametersInfo 设置壁纸并通知系统 - IntPtr uniPtr = IntPtr.Zero; - try - { - uniPtr = Marshal.StringToHGlobalUni(imagePath); - bool result; - unsafe - { - void* uniVoidPtr = (void*)uniPtr; - result = PInvoke.SystemParametersInfo( - Windows.Win32.UI.WindowsAndMessaging.SYSTEM_PARAMETERS_INFO_ACTION.SPI_SETDESKWALLPAPER, - 0, - uniVoidPtr, - Windows.Win32.UI.WindowsAndMessaging.SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS.SPIF_UPDATEINIFILE | - Windows.Win32.UI.WindowsAndMessaging.SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS.SPIF_SENDCHANGE); - } - - if (!result) - { - throw new Win32Exception(Marshal.GetLastWin32Error(), "SystemParametersInfo失败"); - } - } - finally + var imagePath = Settings.ImagePath; + var fit = Settings.FitStyle; + _logger.LogInformation("正在切换壁纸到: {Path}, FitStyle: {Fit}", imagePath, fit); + + var (tileValue, styleValue) = fit switch { - if (uniPtr != IntPtr.Zero) - Marshal.FreeHGlobal(uniPtr); - } - }); + 0 => ("1", "1"), // 平铺 + 1 => ("0", "0"), // 居中 + 2 => ("0", "2"), // 拉伸 + 3 => ("0", "10"), // 填充 + 4 => ("0", "6"), // 适应 + 5 => ("0", "22"), // 跨区 + _ => ("0", "10") // 默认:填充 + }; - _logger.LogInformation("切换壁纸成功: {Path}", imagePath); + await Task.Run(() => SetImageWallpaper(imagePath, tileValue, styleValue)); + _logger.LogInformation("切换壁纸成功: {Path}", imagePath); + } } catch (Exception ex) { @@ -108,4 +79,103 @@ await Task.Run(() => await base.OnInvoke(); _logger.LogDebug("ChangeWallpaperAction OnInvoke 完成"); } + + private static (byte R, byte G, byte B) ParseColor(string value) + { + var color = value.Trim(); + if (color.StartsWith("#")) + { + color = color[1..]; + } + + if (color.Length == 6 && int.TryParse(color, System.Globalization.NumberStyles.HexNumber, null, out var rgb)) + { + return ((byte)((rgb >> 16) & 0xFF), (byte)((rgb >> 8) & 0xFF), (byte)(rgb & 0xFF)); + } + + var parts = value.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + if (parts.Length == 3 && + byte.TryParse(parts[0], out var r) && + byte.TryParse(parts[1], out var g) && + byte.TryParse(parts[2], out var b)) + { + return (r, g, b); + } + + throw new FormatException("纯色壁纸颜色格式无效,请使用 #RRGGBB 或 R,G,B。"); + } + + private static void SetImageWallpaper(string imagePath, string tileValue, string styleValue) + { + using (var desktopRegKey = Registry.CurrentUser.OpenSubKey("Control Panel\\Desktop", true)) + { + if (desktopRegKey == null) + { + throw new Win32Exception("无法访问系统桌面注册表配置项,操作失败。"); + } + + desktopRegKey.SetValue("TileWallpaper", tileValue, RegistryValueKind.String); + desktopRegKey.SetValue("WallpaperStyle", styleValue, RegistryValueKind.String); + } + + ApplyWallpaper(imagePath); + } + + private static void SetSolidColorWallpaper((byte R, byte G, byte B) color) + { + using (var colorsRegKey = Registry.CurrentUser.OpenSubKey("Control Panel\\Colors", true)) + { + if (colorsRegKey == null) + { + throw new Win32Exception("无法访问系统颜色注册表配置项,操作失败。"); + } + + colorsRegKey.SetValue("Background", $"{color.R} {color.G} {color.B}", RegistryValueKind.String); + } + + using (var desktopRegKey = Registry.CurrentUser.OpenSubKey("Control Panel\\Desktop", true)) + { + if (desktopRegKey == null) + { + throw new Win32Exception("无法访问系统桌面注册表配置项,操作失败。"); + } + + desktopRegKey.SetValue("Wallpaper", string.Empty, RegistryValueKind.String); + desktopRegKey.SetValue("TileWallpaper", "0", RegistryValueKind.String); + desktopRegKey.SetValue("WallpaperStyle", "0", RegistryValueKind.String); + } + + ApplyWallpaper(string.Empty); + } + + private static void ApplyWallpaper(string wallpaperPath) + { + IntPtr uniPtr = IntPtr.Zero; + try + { + uniPtr = Marshal.StringToHGlobalUni(wallpaperPath); + bool result; + unsafe + { + result = PInvoke.SystemParametersInfo( + Windows.Win32.UI.WindowsAndMessaging.SYSTEM_PARAMETERS_INFO_ACTION.SPI_SETDESKWALLPAPER, + 0, + (void*)uniPtr, + Windows.Win32.UI.WindowsAndMessaging.SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS.SPIF_UPDATEINIFILE | + Windows.Win32.UI.WindowsAndMessaging.SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS.SPIF_SENDCHANGE); + } + + if (!result) + { + throw new Win32Exception(Marshal.GetLastWin32Error(), "SystemParametersInfo失败"); + } + } + finally + { + if (uniPtr != IntPtr.Zero) + { + Marshal.FreeHGlobal(uniPtr); + } + } + } } diff --git a/Controls/Components/BetterCarouselContainerComponent.axaml.cs b/Controls/Components/BetterCarouselContainerComponent.axaml.cs index f3705b47..9f0bd426 100644 --- a/Controls/Components/BetterCarouselContainerComponent.axaml.cs +++ b/Controls/Components/BetterCarouselContainerComponent.axaml.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Specialized; using System.ComponentModel; using System.Linq; @@ -36,6 +36,7 @@ public partial class BetterCarouselContainerComponent : ComponentBase + + + + + + diff --git a/Controls/SimulateKeyboardSettingsControl.cs b/Controls/SimulateKeyboardSettingsControl.cs index 98836e3a..5c2dff1b 100644 --- a/Controls/SimulateKeyboardSettingsControl.cs +++ b/Controls/SimulateKeyboardSettingsControl.cs @@ -110,8 +110,8 @@ private void StartRecording() _startButton.IsVisible = false; _stopButton.IsVisible = true; _hookProc = HookCallback; - _hookId = PInvoke.SetWindowsHookEx(WINDOWS_HOOK_ID.WH_KEYBOARD_LL, _hookProc, - PInvoke.GetModuleHandle(Process.GetCurrentProcess().MainModule?.ModuleName), 0); + _hookId = (HHOOK)PInvoke.SetWindowsHookEx(WINDOWS_HOOK_ID.WH_KEYBOARD_LL, _hookProc, + PInvoke.GetModuleHandle(Process.GetCurrentProcess().MainModule?.ModuleName), 0).DangerousGetHandle(); } private void StopRecording() diff --git a/Controls/WallpaperSettingsControl.cs b/Controls/WallpaperSettingsControl.cs index 7c54033b..ec6b2a1d 100644 --- a/Controls/WallpaperSettingsControl.cs +++ b/Controls/WallpaperSettingsControl.cs @@ -1,5 +1,4 @@ using Avalonia.Controls; -using Avalonia.Data; using Avalonia.Platform.Storage; using ClassIsland.Core.Abstractions.Controls; using ClassIsland.Shared; @@ -12,9 +11,14 @@ namespace SystemTools.Controls; public class ChangeWallpaperSettingsControl : ActionSettingsControlBase { + private Avalonia.Controls.ComboBox _modeComboBox; + private Avalonia.Controls.TextBlock _pathLabel; private Avalonia.Controls.TextBox _pathBox; private Avalonia.Controls.Button _browseButton; + private Avalonia.Controls.TextBlock _fitLabel; private Avalonia.Controls.ComboBox _fitComboBox; + private Avalonia.Controls.TextBlock _solidColorLabel; + private Avalonia.Controls.TextBox _solidColorBox; public ChangeWallpaperSettingsControl() { @@ -22,10 +26,32 @@ public ChangeWallpaperSettingsControl() panel.Children.Add(new Avalonia.Controls.TextBlock { - Text = "图片路径:", + Text = "壁纸类型:", FontWeight = Avalonia.Media.FontWeight.Bold }); + _modeComboBox = new Avalonia.Controls.ComboBox + { + ItemsSource = new[] { "图片壁纸", "纯色壁纸" }, + Width = 200 + }; + _modeComboBox.SelectionChanged += (s, e) => + { + if (_modeComboBox.SelectedIndex >= 0) + { + Settings.Mode = (ChangeWallpaperMode)_modeComboBox.SelectedIndex; + RefreshModeVisibility(); + } + }; + panel.Children.Add(_modeComboBox); + + _pathLabel = new Avalonia.Controls.TextBlock + { + Text = "图片路径:", + FontWeight = Avalonia.Media.FontWeight.Bold + }; + panel.Children.Add(_pathLabel); + _pathBox = new Avalonia.Controls.TextBox { Watermark = "请选择壁纸图片文件" @@ -44,12 +70,13 @@ public ChangeWallpaperSettingsControl() panel.Children.Add(_browseButton); // 新增:契合度下拉 - panel.Children.Add(new Avalonia.Controls.TextBlock + _fitLabel = new Avalonia.Controls.TextBlock { Text = "壁纸契合度:", FontWeight = Avalonia.Media.FontWeight.Bold, Margin = new Avalonia.Thickness(0, 8, 0, 0) - }); + }; + panel.Children.Add(_fitLabel); _fitComboBox = new Avalonia.Controls.ComboBox { @@ -63,14 +90,46 @@ public ChangeWallpaperSettingsControl() }; panel.Children.Add(_fitComboBox); + _solidColorLabel = new Avalonia.Controls.TextBlock + { + Text = "纯色颜色:", + FontWeight = Avalonia.Media.FontWeight.Bold, + Margin = new Avalonia.Thickness(0, 8, 0, 0) + }; + panel.Children.Add(_solidColorLabel); + + _solidColorBox = new Avalonia.Controls.TextBox + { + Watermark = "#000000 或 0,0,0", + Width = 200, + HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Left + }; + _solidColorBox.TextChanged += (s, e) => { Settings.SolidColor = _solidColorBox.Text ?? "#000000"; }; + panel.Children.Add(_solidColorBox); + Content = panel; } protected override void OnInitialized() { base.OnInitialized(); + _modeComboBox.SelectedIndex = (int)Settings.Mode; _pathBox.Text = Settings.ImagePath; _fitComboBox.SelectedIndex = Settings.FitStyle; + _solidColorBox.Text = Settings.SolidColor; + RefreshModeVisibility(); + } + + private void RefreshModeVisibility() + { + var isImageMode = Settings.Mode == ChangeWallpaperMode.Image; + _pathLabel.IsVisible = isImageMode; + _pathBox.IsVisible = isImageMode; + _browseButton.IsVisible = isImageMode; + _fitLabel.IsVisible = isImageMode; + _fitComboBox.IsVisible = isImageMode; + _solidColorLabel.IsVisible = !isImageMode; + _solidColorBox.IsVisible = !isImageMode; } private async Task BrowseButton_Click() diff --git a/Models/ComponentSettings/BetterCarouselContainerSettings.cs b/Models/ComponentSettings/BetterCarouselContainerSettings.cs index 0edd5df1..e7fd8e95 100644 --- a/Models/ComponentSettings/BetterCarouselContainerSettings.cs +++ b/Models/ComponentSettings/BetterCarouselContainerSettings.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.ObjectModel; using System.Collections.Specialized; using System.ComponentModel; @@ -62,6 +62,9 @@ public partial class BetterCarouselContainerSettings : ObservableObject, ICompon [ObservableProperty] private bool _showProgressBar = true; + [ObservableProperty] + private bool _reduceProgressBarPrecision = false; + [ObservableProperty] private bool _showSideSeparators = false; diff --git a/Plugin.cs b/Plugin.cs index 20092fc5..c80489df 100644 --- a/Plugin.cs +++ b/Plugin.cs @@ -1,4 +1,4 @@ -using Avalonia.Controls; +using Avalonia.Controls; using Avalonia.Threading; using AvaloniaEdit.Utils; using ClassIsland.Core; @@ -20,7 +20,6 @@ using System.IO; using System.Linq; using System.Runtime.InteropServices; -using System.ComponentModel; using SystemTools.Actions; using SystemTools.ConfigHandlers; using SystemTools.Controls; @@ -31,8 +30,6 @@ using SystemTools.Settings; using SystemTools.Shared; using SystemTools.Triggers; -using Windows.Media.Control; -using WinMedia = Windows.Media.Control; namespace SystemTools; @@ -44,7 +41,7 @@ _________ __ ___________ .__ /_______ // ____|/____ > |__| \___ >|__|_| /|____| \____/ \____/ |____//____ > \/ \/ \/ \/ \/ \/ */ -public class Plugin : PluginBase +public partial class Plugin : PluginBase { private ILogger? _logger; private NativeMenuItem? _toggleFloatingWindowMenuItem; @@ -556,121 +553,6 @@ private bool HasAnyActionEnabled(MainConfigData config, params string[] actionId return actionIds.Any(id => config.IsActionEnabled(id)); } - private static bool HandleProcessRunningRule(object? settings) - { - if (settings is not ProcessRunningRuleSettings ruleSettings || - string.IsNullOrWhiteSpace(ruleSettings.ProcessName)) - { - return false; - } - - var processName = ruleSettings.ProcessName.Trim(); - if (processName.EndsWith(".exe", StringComparison.OrdinalIgnoreCase)) - { - processName = processName[..^4]; - } - - try - { - return System.Diagnostics.Process.GetProcessesByName(processName).Length > 0; - } - catch - { - return false; - } - } - - private static bool HandleUsingClassPlanRule(object? settings) - { - if (settings is not UsingClassPlanRuleSettings ruleSettings || - !Guid.TryParse(ruleSettings.ClassPlanId, out var classPlanId)) - { - return false; - } - - var profile = IAppHost.TryGetService()?.Profile; - if (profile == null || !profile.ClassPlans.TryGetValue(classPlanId, out var classPlan)) - { - return false; - } - - return classPlan.IsActivated; - } - - private static bool HandleUsingTimeLayoutRule(object? settings) - { - if (settings is not UsingTimeLayoutRuleSettings ruleSettings || - !Guid.TryParse(ruleSettings.TimeLayoutId, out var timeLayoutId)) - { - return false; - } - - var profile = IAppHost.TryGetService()?.Profile; - if (profile == null || !profile.TimeLayouts.TryGetValue(timeLayoutId, out var timeLayout)) - { - return false; - } - - return timeLayout.IsActivated; - } - - private static bool HandleInTimePeriodRule(object? settings) - { - if (settings is not InTimePeriodRuleSettings ruleSettings || - !TimeSpan.TryParse(ruleSettings.StartTime, out var start) || - !TimeSpan.TryParse(ruleSettings.EndTime, out var end)) - { - return false; - } - - var current = IAppHost.TryGetService()?.GetCurrentLocalDateTime().TimeOfDay ?? DateTime.Now.TimeOfDay; - if (start <= end) - { - return current >= start && current <= end; - } - - 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(); - - var sessions = manager?.GetSessions(); - var isPlaying = sessions != null && sessions.Any(session => - { - var playbackInfo = session.GetPlaybackInfo(); - return playbackInfo?.PlaybackStatus == WinMedia.GlobalSystemMediaTransportControlsSessionPlaybackStatus.Playing; - }); - - _lastMediaRuleResult = isPlaying; - _lastMediaRuleCheckAt = now; - return isPlaying; - } - catch - { - _lastMediaRuleCheckAt = now; - return _lastMediaRuleResult; - } - } - private void BuildSimulationMenu(MainConfigData config) { var items = new List(); diff --git a/Rules/Handlers/InTimePeriodRuleHandler.cs b/Rules/Handlers/InTimePeriodRuleHandler.cs new file mode 100644 index 00000000..a02e964b --- /dev/null +++ b/Rules/Handlers/InTimePeriodRuleHandler.cs @@ -0,0 +1,27 @@ +using System; +using ClassIsland.Core; +using ClassIsland.Core.Abstractions.Services; +using SystemTools.Rules; + +namespace SystemTools; + +public partial class Plugin +{ + private static bool HandleInTimePeriodRule(object? settings) + { + if (settings is not InTimePeriodRuleSettings ruleSettings || + !TimeSpan.TryParse(ruleSettings.StartTime, out var start) || + !TimeSpan.TryParse(ruleSettings.EndTime, out var end)) + { + return false; + } + + var current = IAppHost.TryGetService()?.GetCurrentLocalDateTime().TimeOfDay ?? DateTime.Now.TimeOfDay; + if (start <= end) + { + return current >= start && current <= end; + } + + return current >= start || current <= end; + } +} diff --git a/Rules/Handlers/MediaMusicPlayingRuleHandler.cs b/Rules/Handlers/MediaMusicPlayingRuleHandler.cs new file mode 100644 index 00000000..3ef5e1ce --- /dev/null +++ b/Rules/Handlers/MediaMusicPlayingRuleHandler.cs @@ -0,0 +1,47 @@ +using System; +using System.Linq; +using WinMedia = Windows.Media.Control; + +namespace SystemTools; + +public partial class Plugin +{ + 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(); + + var sessions = manager?.GetSessions(); + var isPlaying = sessions != null && sessions.Any(session => + { + var playbackInfo = session.GetPlaybackInfo(); + return playbackInfo?.PlaybackStatus == WinMedia.GlobalSystemMediaTransportControlsSessionPlaybackStatus.Playing; + }); + + _lastMediaRuleResult = isPlaying; + _lastMediaRuleCheckAt = now; + return isPlaying; + } + catch + { + _lastMediaRuleCheckAt = now; + return _lastMediaRuleResult; + } + } +} diff --git a/Rules/Handlers/ProcessRunningRuleHandler.cs b/Rules/Handlers/ProcessRunningRuleHandler.cs new file mode 100644 index 00000000..8671f66a --- /dev/null +++ b/Rules/Handlers/ProcessRunningRuleHandler.cs @@ -0,0 +1,31 @@ +using System; +using SystemTools.Rules; + +namespace SystemTools; + +public partial class Plugin +{ + private static bool HandleProcessRunningRule(object? settings) + { + if (settings is not ProcessRunningRuleSettings ruleSettings || + string.IsNullOrWhiteSpace(ruleSettings.ProcessName)) + { + return false; + } + + var processName = ruleSettings.ProcessName.Trim(); + if (processName.EndsWith(".exe", StringComparison.OrdinalIgnoreCase)) + { + processName = processName[..^4]; + } + + try + { + return System.Diagnostics.Process.GetProcessesByName(processName).Length > 0; + } + catch + { + return false; + } + } +} diff --git a/Rules/Handlers/UsingClassPlanRuleHandler.cs b/Rules/Handlers/UsingClassPlanRuleHandler.cs new file mode 100644 index 00000000..514b37c1 --- /dev/null +++ b/Rules/Handlers/UsingClassPlanRuleHandler.cs @@ -0,0 +1,26 @@ +using System; +using ClassIsland.Core; +using ClassIsland.Core.Abstractions.Services; +using SystemTools.Rules; + +namespace SystemTools; + +public partial class Plugin +{ + private static bool HandleUsingClassPlanRule(object? settings) + { + if (settings is not UsingClassPlanRuleSettings ruleSettings || + !Guid.TryParse(ruleSettings.ClassPlanId, out var classPlanId)) + { + return false; + } + + var profile = IAppHost.TryGetService()?.Profile; + if (profile == null || !profile.ClassPlans.TryGetValue(classPlanId, out var classPlan)) + { + return false; + } + + return classPlan.IsActivated; + } +} diff --git a/Rules/Handlers/UsingTimeLayoutRuleHandler.cs b/Rules/Handlers/UsingTimeLayoutRuleHandler.cs new file mode 100644 index 00000000..3ae4be65 --- /dev/null +++ b/Rules/Handlers/UsingTimeLayoutRuleHandler.cs @@ -0,0 +1,26 @@ +using System; +using ClassIsland.Core; +using ClassIsland.Core.Abstractions.Services; +using SystemTools.Rules; + +namespace SystemTools; + +public partial class Plugin +{ + private static bool HandleUsingTimeLayoutRule(object? settings) + { + if (settings is not UsingTimeLayoutRuleSettings ruleSettings || + !Guid.TryParse(ruleSettings.TimeLayoutId, out var timeLayoutId)) + { + return false; + } + + var profile = IAppHost.TryGetService()?.Profile; + if (profile == null || !profile.TimeLayouts.TryGetValue(timeLayoutId, out var timeLayout)) + { + return false; + } + + return timeLayout.IsActivated; + } +} diff --git a/Settings/WallpaperSettings.cs b/Settings/WallpaperSettings.cs index 08fefe74..b325761b 100644 --- a/Settings/WallpaperSettings.cs +++ b/Settings/WallpaperSettings.cs @@ -2,10 +2,20 @@ namespace SystemTools.Settings; +public enum ChangeWallpaperMode +{ + Image = 0, + SolidColor = 1 +} + public class ChangeWallpaperSettings { [JsonPropertyName("imagePath")] public string ImagePath { get; set; } = string.Empty; + [JsonPropertyName("mode")] public ChangeWallpaperMode Mode { get; set; } = ChangeWallpaperMode.Image; + + [JsonPropertyName("solidColor")] public string SolidColor { get; set; } = "#000000"; + // 壁纸契合度:0=平铺,1=居中,2=拉伸,3=填充,4=适应,5=跨区 [JsonPropertyName("fitStyle")] public int FitStyle { get; set; } = 3; // 默认填充 From ef22dc367afe8b3931964ecbd34fe42976c7f566 Mon Sep 17 00:00:00 2001 From: Wang Haoyu Date: Fri, 19 Jun 2026 19:47:05 +0800 Subject: [PATCH 3/4] Fix floating-window trigger duplicate IDs --- Services/FloatingWindowService.cs | 43 +++++++++++++++++--- SettingsPage/SystemToolsSettingsViewModel.cs | 5 ++- Triggers/FloatingWindowTrigger.cs | 31 ++++++++++++++ 3 files changed, 73 insertions(+), 6 deletions(-) diff --git a/Services/FloatingWindowService.cs b/Services/FloatingWindowService.cs index 40c72334..516a46b6 100644 --- a/Services/FloatingWindowService.cs +++ b/Services/FloatingWindowService.cs @@ -130,8 +130,43 @@ public void Stop() public void RegisterTrigger(FloatingWindowTrigger trigger) { - _entries[trigger] = new FloatingWindowEntry( - trigger.GetButtonId(), + _entries[trigger] = CreateEntry(trigger); + + PruneButtonWidthCache(); + NotifyEntriesChanged(); + } + + public void EnsureUniqueButtonIds() + { + var usedButtonIds = new HashSet(); + var changed = false; + + foreach (var trigger in _entries.Keys.ToList()) + { + var oldButtonId = trigger.GetButtonId(); + var buttonId = trigger.GetUniqueButtonId(usedButtonIds.Contains); + usedButtonIds.Add(buttonId); + _entries[trigger] = CreateEntry(trigger); + + if (!string.Equals(oldButtonId, buttonId, StringComparison.Ordinal)) + { + changed = true; + } + } + + if (changed) + { + PruneButtonWidthCache(); + } + } + + private FloatingWindowEntry CreateEntry(FloatingWindowTrigger trigger) + { + var buttonId = trigger.GetUniqueButtonId(id => _entries.Any(x => + !ReferenceEquals(x.Key, trigger) && string.Equals(x.Value.ButtonId, id, StringComparison.Ordinal))); + + return new FloatingWindowEntry( + buttonId, trigger.GetIcon(), trigger.GetButtonName(), trigger.ShouldUseRevertStyle(), @@ -139,9 +174,6 @@ public void RegisterTrigger(FloatingWindowTrigger trigger) trigger.GetLayoutButtonName(), trigger.TriggerFromFloatingWindow, trigger.CancelIsOnState); - - PruneButtonWidthCache(); - NotifyEntriesChanged(); } public void UnregisterTrigger(FloatingWindowTrigger trigger) @@ -506,6 +538,7 @@ private void RefreshWindowButtons() private List> GetOrderedRows() { + EnsureUniqueButtonIds(); var values = _entries.Values .GroupBy(x => x.ButtonId) .ToDictionary(x => x.Key, x => x.First()); diff --git a/SettingsPage/SystemToolsSettingsViewModel.cs b/SettingsPage/SystemToolsSettingsViewModel.cs index 6c0d7c7f..a127cc33 100644 --- a/SettingsPage/SystemToolsSettingsViewModel.cs +++ b/SettingsPage/SystemToolsSettingsViewModel.cs @@ -263,7 +263,10 @@ public void SaveFeatureSettings() public void RefreshFloatingTriggers() { - var entries = _floatingWindowService.Entries.ToDictionary(x => x.ButtonId, x => x); + _floatingWindowService.EnsureUniqueButtonIds(); + var entries = _floatingWindowService.Entries + .GroupBy(x => x.ButtonId) + .ToDictionary(x => x.Key, x => x.First()); HasFloatingTriggerEntries = entries.Count > 0; if (!HasFloatingTriggerEntries && Settings.ShowFloatingWindow) diff --git a/Triggers/FloatingWindowTrigger.cs b/Triggers/FloatingWindowTrigger.cs index 586d256e..34776997 100644 --- a/Triggers/FloatingWindowTrigger.cs +++ b/Triggers/FloatingWindowTrigger.cs @@ -14,6 +14,7 @@ public class FloatingWindowTrigger : TriggerBase { private readonly FloatingWindowService _floatingWindowService; private readonly ILogger _logger; + private bool _isNormalizingButtonId; public FloatingWindowTrigger(FloatingWindowService floatingWindowService, ILogger logger) { @@ -66,6 +67,31 @@ public string GetButtonId() return Settings.ButtonId; } + public string GetUniqueButtonId(Func isButtonIdInUse) + { + EnsureButtonId(); + if (!isButtonIdInUse(Settings.ButtonId)) + { + return Settings.ButtonId; + } + + _isNormalizingButtonId = true; + try + { + do + { + Settings.ButtonId = Guid.NewGuid().ToString("N"); + } while (isButtonIdInUse(Settings.ButtonId)); + } + finally + { + _isNormalizingButtonId = false; + } + + _logger.LogInformation("检测到重复的悬浮窗按钮 ID,已为触发器生成新 ID: {ButtonId}", Settings.ButtonId); + return Settings.ButtonId; + } + public string GetIcon() { return Settings.Icon; @@ -132,6 +158,11 @@ private void EnsureButtonId() private void OnSettingsChanged(object? sender, PropertyChangedEventArgs e) { + if (_isNormalizingButtonId) + { + return; + } + if (e.PropertyName == nameof(FloatingWindowTriggerConfig.Icon) || e.PropertyName == nameof(FloatingWindowTriggerConfig.ButtonId) || e.PropertyName == nameof(FloatingWindowTriggerConfig.ButtonName)) From 793e21f990022268801ae6a4d4f800fde3fe23fe Mon Sep 17 00:00:00 2001 From: Programmer_MrWang Date: Fri, 19 Jun 2026 19:49:08 +0800 Subject: [PATCH 4/4] =?UTF-8?q?fix:=20=E8=A1=A5=E5=85=A8=E7=BC=BA=E5=A4=B1?= =?UTF-8?q?=E7=9A=84=E5=BC=95=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Plugin.cs | 1 + Rules/Handlers/InTimePeriodRuleHandler.cs | 1 + Rules/Handlers/UsingClassPlanRuleHandler.cs | 1 + Rules/Handlers/UsingTimeLayoutRuleHandler.cs | 1 + 4 files changed, 4 insertions(+) diff --git a/Plugin.cs b/Plugin.cs index c80489df..ab6d2fd4 100644 --- a/Plugin.cs +++ b/Plugin.cs @@ -17,6 +17,7 @@ using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; +using System.ComponentModel; using System.IO; using System.Linq; using System.Runtime.InteropServices; diff --git a/Rules/Handlers/InTimePeriodRuleHandler.cs b/Rules/Handlers/InTimePeriodRuleHandler.cs index a02e964b..bb8825f4 100644 --- a/Rules/Handlers/InTimePeriodRuleHandler.cs +++ b/Rules/Handlers/InTimePeriodRuleHandler.cs @@ -1,6 +1,7 @@ using System; using ClassIsland.Core; using ClassIsland.Core.Abstractions.Services; +using ClassIsland.Shared; using SystemTools.Rules; namespace SystemTools; diff --git a/Rules/Handlers/UsingClassPlanRuleHandler.cs b/Rules/Handlers/UsingClassPlanRuleHandler.cs index 514b37c1..1e7e9ed2 100644 --- a/Rules/Handlers/UsingClassPlanRuleHandler.cs +++ b/Rules/Handlers/UsingClassPlanRuleHandler.cs @@ -1,6 +1,7 @@ using System; using ClassIsland.Core; using ClassIsland.Core.Abstractions.Services; +using ClassIsland.Shared; using SystemTools.Rules; namespace SystemTools; diff --git a/Rules/Handlers/UsingTimeLayoutRuleHandler.cs b/Rules/Handlers/UsingTimeLayoutRuleHandler.cs index 3ae4be65..e355a0b3 100644 --- a/Rules/Handlers/UsingTimeLayoutRuleHandler.cs +++ b/Rules/Handlers/UsingTimeLayoutRuleHandler.cs @@ -1,6 +1,7 @@ using System; using ClassIsland.Core; using ClassIsland.Core.Abstractions.Services; +using ClassIsland.Shared; using SystemTools.Rules; namespace SystemTools;