diff --git a/.idea/.idea.SystemTools/.idea/indexLayout.xml b/.idea/.idea.SystemTools/.idea/indexLayout.xml new file mode 100644 index 00000000..7b08163c --- /dev/null +++ b/.idea/.idea.SystemTools/.idea/indexLayout.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/.idea.SystemTools/.idea/vcs.xml b/.idea/.idea.SystemTools/.idea/vcs.xml new file mode 100644 index 00000000..35eb1ddf --- /dev/null +++ b/.idea/.idea.SystemTools/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/.idea.SystemTools/.idea/workspace.xml b/.idea/.idea.SystemTools/.idea/workspace.xml index 40f53431..efe56f5e 100644 --- a/.idea/.idea.SystemTools/.idea/workspace.xml +++ b/.idea/.idea.SystemTools/.idea/workspace.xml @@ -35,6 +35,9 @@ } }]]> + + { "associatedIndex": 4 } @@ -62,7 +65,7 @@ "node.js.selected.package.eslint": "(autodetect)", "node.js.selected.package.tslint": "(autodetect)", "nodejs_package_manager_path": "npm", - "settings.editor.selected.configurable": "preferences.pluginManager", + "settings.editor.selected.configurable": "preferences.lookFeel", "vue.rearranger.settings.migration": "true" } } diff --git a/Actions/ActionFlowExecutionConfirmationAction.cs b/Actions/ActionFlowExecutionConfirmationAction.cs new file mode 100644 index 00000000..d90fdc7f --- /dev/null +++ b/Actions/ActionFlowExecutionConfirmationAction.cs @@ -0,0 +1,388 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Threading; +using ClassIsland.Core; +using ClassIsland.Core.Abstractions.Automation; +using ClassIsland.Core.Abstractions.Services; +using ClassIsland.Core.Attributes; +using FluentAvalonia.UI.Controls; +using Microsoft.Extensions.Logging; +using System; +using System.Linq; +using System.Threading.Tasks; +using SystemTools.Settings; +using SystemTools.Shared; + +namespace SystemTools.Actions; + +[ActionInfo("SystemTools.ActionFlowExecutionConfirmation", "行动流执行确认", "\uE01D", false)] +public class ActionFlowExecutionConfirmationAction( + IActionService actionService, + ILogger logger) + : ActionBase +{ + private const string ContinueResult = "continue"; + private const string DelayResult = "delay"; + private const string DelayConfirmedResult = "delayConfirmed"; + private const string ConfirmDelayResult = "confirmDelay"; + private const string CancelDelayResult = "cancelDelay"; + private const string StopActionFlowResult = "stopActionFlow"; + private const string InterruptedResult = "interrupted"; + + private FATaskDialog? _activeDialog; + private FATaskDialog? _activeDelayDialog; + private bool _isDelayDialogOpen; + + protected override async Task OnInvoke() + { + await base.OnInvoke(); + + var outcome = await ShowConfirmationAsync(); + if (outcome.DelaySeconds is int delaySeconds) + { + logger.LogInformation("行动流“{ActionSetName}”将在 {DelaySeconds} 秒后继续执行。", + ActionSet.Name, delaySeconds); + + try + { + await Task.Delay(TimeSpan.FromSeconds(delaySeconds), InterruptCancellationToken); + } + catch (OperationCanceledException) when (InterruptCancellationToken.IsCancellationRequested) + { + // 行动流已被外部中断,无需继续等待。 + } + + return; + } + + if (!Equals(outcome.Result, StopActionFlowResult)) + { + return; + } + + logger.LogInformation("用户停止了行动流“{ActionSetName}”。", ActionSet.Name); + + // InterruptActionSetAsync 会等待当前行动结束,因此这里只发起中断而不等待。 + _ = actionService.InterruptActionSetAsync(ActionSet); + } + + protected override async Task OnInterrupted() + { + await Dispatcher.UIThread.InvokeAsync(() => + { + _activeDelayDialog?.Hide(InterruptedResult); + _activeDialog?.Hide(InterruptedResult); + }); + await base.OnInterrupted(); + } + + private Task ShowConfirmationAsync() + { + var resultSource = new TaskCompletionSource( + TaskCreationOptions.RunContinuationsAsynchronously); + + Dispatcher.UIThread.Post(async () => + { + try + { + if (InterruptCancellationToken.IsCancellationRequested) + { + resultSource.TrySetResult(new ConfirmationOutcome(InterruptedResult)); + return; + } + + var mainWindow = AppBase.Current.MainWindow + ?? throw new InvalidOperationException("ClassIsland 主窗口尚未初始化"); + var promptName = string.IsNullOrWhiteSpace(Settings.PromptName) + ? "未命名自动化" + : Settings.PromptName.Trim(); + + var dialog = new FATaskDialog + { + XamlRoot = mainWindow, + Title = "SystemTools - 行动流执行确认", + Header = "行动流执行确认", + SubHeader = $"即将执行自动化“{promptName}”,是否执行?", + IconSource = new FAFontIconSource { Glyph = "\uE01D" } + }; + dialog.Buttons.Add(new FATaskDialogButton("停止行动流", StopActionFlowResult)); + dialog.Buttons.Add(new FATaskDialogButton("延迟执行", DelayResult)); + dialog.Buttons.Add(new FATaskDialogButton("立即执行", ContinueResult) + { + IsDefault = true + }); + int? delaySeconds = null; + dialog.Opened += (_, _) => RestoreDialogPosition(dialog, mainWindow); + dialog.Closing += (sender, e) => + { + if (Equals(e.Result, DelayResult)) + { + e.Cancel = true; + if (!_isDelayDialogOpen) + { + _ = HandleDelayRequestAsync(dialog, seconds => delaySeconds = seconds); + } + + return; + } + + var isAllowedToClose = Equals(e.Result, ContinueResult) || + Equals(e.Result, DelayConfirmedResult) || + Equals(e.Result, StopActionFlowResult) || + Equals(e.Result, InterruptedResult); + if (!isAllowedToClose) + { + e.Cancel = true; + return; + } + + RememberDialogPosition(dialog); + }; + + _activeDialog = dialog; + var result = await dialog.ShowAsync(showHosted: false); + resultSource.TrySetResult(new ConfirmationOutcome(result, delaySeconds)); + } + catch (Exception ex) + { + resultSource.TrySetException(ex); + } + finally + { + _activeDialog = null; + } + }); + + return resultSource.Task; + } + + private async Task HandleDelayRequestAsync(FATaskDialog parentDialog, Action setDelaySeconds) + { + _isDelayDialogOpen = true; + try + { + var delaySeconds = await ShowDelayDialogAsync(parentDialog); + if (delaySeconds is not int seconds || InterruptCancellationToken.IsCancellationRequested) + { + return; + } + + setDelaySeconds(seconds); + parentDialog.Hide(DelayConfirmedResult); + } + catch (Exception ex) + { + logger.LogError(ex, "显示延迟执行设置窗口时发生错误。"); + } + finally + { + _isDelayDialogOpen = false; + } + } + + private async Task ShowDelayDialogAsync(FATaskDialog parentDialog) + { + if (TopLevel.GetTopLevel(parentDialog) is not Window parentWindow) + { + throw new InvalidOperationException("无法获取行动流执行确认窗口"); + } + + var secondsInput = new NumericUpDown + { + Minimum = 1, + Maximum = 86400, + Increment = 1, + Value = 10, + FormatString = "F0", + Width = 160, + HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Left + }; + var content = new StackPanel { Spacing = 8 }; + content.Children.Add(new TextBlock { Text = "延迟秒数" }); + content.Children.Add(secondsInput); + + var dialog = new FATaskDialog + { + XamlRoot = parentWindow, + Title = "SystemTools - 延迟执行", + Header = "延迟执行", + SubHeader = "设置多少秒后继续执行该行动流。", + Content = content, + IconSource = new FAFontIconSource { Glyph = "\uE01D" } + }; + dialog.Buttons.Add(new FATaskDialogButton("取消", CancelDelayResult)); + dialog.Buttons.Add(new FATaskDialogButton("确认", ConfirmDelayResult) + { + IsDefault = true + }); + dialog.Opened += (_, _) => + { + RestoreDelayDialogPosition(dialog, parentWindow); + if (TopLevel.GetTopLevel(dialog) is Window window) + { + window.Topmost = true; + window.Activate(); + } + }; + dialog.Closing += (_, _) => RememberDelayDialogPosition(dialog); + + _activeDelayDialog = dialog; + var previousParentTopmost = parentWindow.Topmost; + parentWindow.Topmost = false; + try + { + var result = await dialog.ShowAsync(showHosted: false); + if (!Equals(result, ConfirmDelayResult)) + { + return null; + } + + return Math.Max(1, (int)(secondsInput.Value ?? 10)); + } + finally + { + _activeDelayDialog = null; + parentWindow.Topmost = previousParentTopmost; + if (parentWindow.IsVisible) + { + parentWindow.Activate(); + } + } + } + + private static void RestoreDialogPosition(FATaskDialog dialog, Window owner) + { + if (TopLevel.GetTopLevel(dialog) is not Window window) + { + return; + } + + var scaling = Math.Max(0.5, window.RenderScaling); + var widthPx = (int)Math.Round(window.Bounds.Width * scaling); + var heightPx = (int)Math.Round(window.Bounds.Height * scaling); + if (widthPx <= 0 || heightPx <= 0) + { + return; + } + + var config = GlobalConstants.MainConfig?.Data; + if (config?.ActionFlowExecutionConfirmationPositionX is int savedX && + config.ActionFlowExecutionConfirmationPositionY is int savedY) + { + var savedPosition = new PixelPoint(savedX, savedY); + var savedRect = new PixelRect(savedPosition, new PixelSize(widthPx, heightPx)); + if (owner.Screens.All.Any(screen => screen.WorkingArea.Intersects(savedRect))) + { + window.Position = savedPosition; + return; + } + } + + CenterDialogWindow(window, owner, widthPx, heightPx); + } + + private static void CenterDialogWindow(Window window, Window owner, int widthPx, int heightPx) + { + var screen = owner.Screens.ScreenFromWindow(owner) ?? owner.Screens.Primary; + if (screen is null) + { + return; + } + + var area = screen.WorkingArea; + window.Position = new PixelPoint( + area.X + (area.Width - widthPx) / 2, + area.Y + (area.Height - heightPx) / 2); + } + + private static void CenterDialogOverOwner(FATaskDialog dialog, Window owner) + { + if (TopLevel.GetTopLevel(dialog) is not Window window) + { + return; + } + + var scaling = Math.Max(0.5, window.RenderScaling); + var ownerScaling = Math.Max(0.5, owner.RenderScaling); + var widthPx = (int)Math.Round(window.Bounds.Width * scaling); + var heightPx = (int)Math.Round(window.Bounds.Height * scaling); + var ownerWidthPx = (int)Math.Round(owner.Bounds.Width * ownerScaling); + var ownerHeightPx = (int)Math.Round(owner.Bounds.Height * ownerScaling); + if (widthPx <= 0 || heightPx <= 0 || ownerWidthPx <= 0 || ownerHeightPx <= 0) + { + return; + } + + var screen = owner.Screens.ScreenFromWindow(owner) ?? owner.Screens.Primary; + if (screen is null) + { + return; + } + + var area = screen.WorkingArea; + var desiredX = owner.Position.X + (ownerWidthPx - widthPx) / 2; + var desiredY = owner.Position.Y + (ownerHeightPx - heightPx) / 2; + window.Position = new PixelPoint( + Math.Clamp(desiredX, area.X, Math.Max(area.X, area.Right - widthPx)), + Math.Clamp(desiredY, area.Y, Math.Max(area.Y, area.Bottom - heightPx))); + } + + private static void RestoreDelayDialogPosition(FATaskDialog dialog, Window owner) + { + if (TopLevel.GetTopLevel(dialog) is not Window window) + { + return; + } + + var scaling = Math.Max(0.5, window.RenderScaling); + var widthPx = (int)Math.Round(window.Bounds.Width * scaling); + var heightPx = (int)Math.Round(window.Bounds.Height * scaling); + if (widthPx <= 0 || heightPx <= 0) + { + return; + } + + var config = GlobalConstants.MainConfig?.Data; + if (config?.ActionFlowExecutionDelayPositionX is int savedX && + config.ActionFlowExecutionDelayPositionY is int savedY) + { + var savedPosition = new PixelPoint(savedX, savedY); + var savedRect = new PixelRect(savedPosition, new PixelSize(widthPx, heightPx)); + if (owner.Screens.All.Any(screen => screen.WorkingArea.Intersects(savedRect))) + { + window.Position = savedPosition; + return; + } + } + + CenterDialogOverOwner(dialog, owner); + } + + private static void RememberDialogPosition(FATaskDialog dialog) + { + if (TopLevel.GetTopLevel(dialog) is not Window window || + GlobalConstants.MainConfig is not { } config) + { + return; + } + + config.Data.ActionFlowExecutionConfirmationPositionX = window.Position.X; + config.Data.ActionFlowExecutionConfirmationPositionY = window.Position.Y; + config.Save(); + } + + private static void RememberDelayDialogPosition(FATaskDialog dialog) + { + if (TopLevel.GetTopLevel(dialog) is not Window window || + GlobalConstants.MainConfig is not { } config) + { + return; + } + + config.Data.ActionFlowExecutionDelayPositionX = window.Position.X; + config.Data.ActionFlowExecutionDelayPositionY = window.Position.Y; + config.Save(); + } + + private sealed record ConfirmationOutcome(object Result, int? DelaySeconds = null); +} diff --git a/Actions/AdjustScreenBrightnessAction.cs b/Actions/AdjustScreenBrightnessAction.cs index bbe5fe5c..2d5d5aa0 100644 --- a/Actions/AdjustScreenBrightnessAction.cs +++ b/Actions/AdjustScreenBrightnessAction.cs @@ -5,6 +5,9 @@ using System.Management; using System.Threading.Tasks; using SystemTools.Settings; +using ClassIsland.Core.Models.Notification; +using SystemTools.Services; +using ClassIsland.Shared; using SystemTools.Shared; namespace SystemTools.Actions; @@ -51,6 +54,12 @@ await Task.Run(() => throw; } }); + if (Settings.NotifyOnExecute) + IAppHost.GetService()?.ShowNotification(new NotificationRequest + { + MaskContent = NotificationContent.CreateTwoIconsMask("已执行调整屏幕亮度操作", "\uE9FB", "") + }); + await base.OnInvoke(); } diff --git a/Actions/AdvancedShutdownAction.cs b/Actions/AdvancedShutdownAction.cs index bf7699fc..1d6c3a6f 100644 --- a/Actions/AdvancedShutdownAction.cs +++ b/Actions/AdvancedShutdownAction.cs @@ -454,7 +454,7 @@ private void ShowOrUpdateFloatingWindow() CanResize = false, Topmost = true, ShowInTaskbar = false, - SystemDecorations = SystemDecorations.None, + WindowDecorations = WindowDecorations.None, Background = Brushes.Transparent, TransparencyLevelHint = [WindowTransparencyLevel.Transparent], Content = new Border diff --git a/Actions/AutoHideMainWindowWhenOccludedAction.cs b/Actions/AutoHideMainWindowWhenOccludedAction.cs new file mode 100644 index 00000000..96e46276 --- /dev/null +++ b/Actions/AutoHideMainWindowWhenOccludedAction.cs @@ -0,0 +1,53 @@ +using System; +using System.Threading.Tasks; +using ClassIsland.Core.Abstractions.Automation; +using ClassIsland.Core.Attributes; +using ClassIsland.Core.Models.Notification; +using ClassIsland.Shared; +using Microsoft.Extensions.Logging; +using SystemTools.Services; +using SystemTools.Settings; +using SystemTools.Shared; + +namespace SystemTools.Actions; + +[ActionInfo("SystemTools.AutoHideMainWindowWhenOccluded", "遮挡文字时隐藏主界面", "\uEEE3", false)] +public class AutoHideMainWindowWhenOccludedAction(ILogger logger) : ActionBase +{ + private readonly ILogger _logger = logger; + + protected override async Task OnInvoke() + { + _logger.LogDebug("AutoHideMainWindowWhenOccludedAction OnInvoke 开始"); + + if (Settings == null) return; + + var config = GlobalConstants.MainConfig?.Data; + if (config == null) return; + + try + { + config.AutoHideMainWindowWhenOccluded = Settings.Enable; + IAppHost.GetService().ApplyConfig(); + GlobalConstants.MainConfig?.Save(); + _logger.LogInformation("已{State}遮挡文字时隐藏主界面功能", Settings.Enable ? "开启" : "关闭"); + } + catch (Exception ex) + { + _logger.LogError(ex, "设置遮挡文字时隐藏主界面功能失败"); + throw; + } + + if (Settings.NotifyOnExecute) + { + IAppHost.GetService()?.ShowNotification(new NotificationRequest + { + MaskContent = NotificationContent.CreateTwoIconsMask( + (Settings.Enable ? "已开启功能 " : "已关闭功能 ") + "遮挡文字时隐藏主界面", "\uEEE3", "") + }); + } + + await base.OnInvoke(); + _logger.LogDebug("AutoHideMainWindowWhenOccludedAction OnInvoke 完成"); + } +} \ No newline at end of file diff --git a/Actions/AutoOpenUsbDriveOnInsertAction.cs b/Actions/AutoOpenUsbDriveOnInsertAction.cs new file mode 100644 index 00000000..e0d276be --- /dev/null +++ b/Actions/AutoOpenUsbDriveOnInsertAction.cs @@ -0,0 +1,53 @@ +using System; +using System.Threading.Tasks; +using ClassIsland.Core.Abstractions.Automation; +using ClassIsland.Core.Attributes; +using ClassIsland.Core.Models.Notification; +using ClassIsland.Shared; +using Microsoft.Extensions.Logging; +using SystemTools.Services; +using SystemTools.Settings; +using SystemTools.Shared; + +namespace SystemTools.Actions; + +[ActionInfo("SystemTools.AutoOpenUsbDriveOnInsert", "自动播放", "\uEE81", false)] +public class AutoOpenUsbDriveOnInsertAction(ILogger logger) : ActionBase +{ + private readonly ILogger _logger = logger; + + protected override async Task OnInvoke() + { + _logger.LogDebug("AutoOpenUsbDriveOnInsertAction OnInvoke 开始"); + + if (Settings == null) return; + + var config = GlobalConstants.MainConfig?.Data; + if (config == null) return; + + try + { + config.AutoOpenUsbDriveOnInsert = Settings.Enable; + IAppHost.GetService().ApplyConfig(); + GlobalConstants.MainConfig?.Save(); + _logger.LogInformation("已{State}自动播放功能", Settings.Enable ? "开启" : "关闭"); + } + catch (Exception ex) + { + _logger.LogError(ex, "设置自动播放功能失败"); + throw; + } + + if (Settings.NotifyOnExecute) + { + IAppHost.GetService()?.ShowNotification(new NotificationRequest + { + MaskContent = NotificationContent.CreateTwoIconsMask( + (Settings.Enable ? "已开启功能 " : "已关闭功能 ") + "自动播放", "\uEE81", "") + }); + } + + await base.OnInvoke(); + _logger.LogDebug("AutoOpenUsbDriveOnInsertAction OnInvoke 完成"); + } +} \ No newline at end of file diff --git a/Actions/AutoSwitchClassIslandThemeAction.cs b/Actions/AutoSwitchClassIslandThemeAction.cs new file mode 100644 index 00000000..058e3942 --- /dev/null +++ b/Actions/AutoSwitchClassIslandThemeAction.cs @@ -0,0 +1,53 @@ +using System; +using System.Threading.Tasks; +using ClassIsland.Core.Abstractions.Automation; +using ClassIsland.Core.Attributes; +using ClassIsland.Core.Models.Notification; +using ClassIsland.Shared; +using Microsoft.Extensions.Logging; +using SystemTools.Services; +using SystemTools.Settings; +using SystemTools.Shared; + +namespace SystemTools.Actions; + +[ActionInfo("SystemTools.AutoSwitchClassIslandTheme", "自动切换 ClassIsland 主题", "\uE5CB", false)] +public class AutoSwitchClassIslandThemeAction(ILogger logger) : ActionBase +{ + private readonly ILogger _logger = logger; + + protected override async Task OnInvoke() + { + _logger.LogDebug("AutoSwitchClassIslandThemeAction OnInvoke 开始"); + + if (Settings == null) return; + + var config = GlobalConstants.MainConfig?.Data; + if (config == null) return; + + try + { + config.AutoSwitchClassIslandTheme = Settings.Enable; + IAppHost.GetService().ApplyConfig(); + GlobalConstants.MainConfig?.Save(); + _logger.LogInformation("已{State}自动切换 ClassIsland 主题功能", Settings.Enable ? "开启" : "关闭"); + } + catch (Exception ex) + { + _logger.LogError(ex, "设置自动切换 ClassIsland 主题功能失败"); + throw; + } + + if (Settings.NotifyOnExecute) + { + IAppHost.GetService()?.ShowNotification(new NotificationRequest + { + MaskContent = NotificationContent.CreateTwoIconsMask( + (Settings.Enable ? "已开启功能 " : "已关闭功能 ") + "自动切换 ClassIsland 主题", "\uE5CB", "") + }); + } + + await base.OnInvoke(); + _logger.LogDebug("AutoSwitchClassIslandThemeAction OnInvoke 完成"); + } +} \ No newline at end of file diff --git a/Actions/BackgroundPlayAudioAction.cs b/Actions/BackgroundPlayAudioAction.cs index af538829..859725e4 100644 --- a/Actions/BackgroundPlayAudioAction.cs +++ b/Actions/BackgroundPlayAudioAction.cs @@ -7,6 +7,8 @@ using System.IO; using System.Threading.Tasks; using SystemTools.Settings; +using ClassIsland.Core.Models.Notification; +using SystemTools.Services; namespace SystemTools.Actions; diff --git a/Actions/BlackScreenHtmlAction.cs b/Actions/BlackScreenHtmlAction.cs index 965c326a..4ace34f5 100644 --- a/Actions/BlackScreenHtmlAction.cs +++ b/Actions/BlackScreenHtmlAction.cs @@ -1,6 +1,10 @@ -using ClassIsland.Core.Abstractions.Automation; +using ClassIsland.Core.Abstractions.Automation; using ClassIsland.Core.Attributes; using Microsoft.Extensions.Logging; +using ClassIsland.Core.Models.Notification; +using SystemTools.Services; +using SystemTools.Settings; +using ClassIsland.Shared; using System; using System.Diagnostics; using System.IO; @@ -11,7 +15,7 @@ namespace SystemTools.Actions; [ActionInfo("SystemTools.BlackScreenHtml", "黑屏html", "\uE643", false)] -public class BlackScreenHtmlAction(ILogger logger) : ActionBase +public class BlackScreenHtmlAction(ILogger logger) : ActionBase { private readonly ILogger _logger = logger; @@ -69,6 +73,12 @@ protected override async Task OnInvoke() _logger.LogError(ex, "执行黑屏html失败"); throw; } + if (Settings.NotifyOnExecute) + IAppHost.GetService()?.ShowNotification(new NotificationRequest + { + MaskContent = NotificationContent.CreateTwoIconsMask("已执行黑屏操作", "\uE9FB", "") + }); + await base.OnInvoke(); _logger.LogDebug("BlackScreenHtmlAction OnInvoke 完成"); diff --git a/Actions/CancelShutdownAction.cs b/Actions/CancelShutdownAction.cs index f325fe38..180126ec 100644 --- a/Actions/CancelShutdownAction.cs +++ b/Actions/CancelShutdownAction.cs @@ -1,6 +1,10 @@ -using ClassIsland.Core.Abstractions.Automation; +using ClassIsland.Core.Abstractions.Automation; using ClassIsland.Core.Attributes; using Microsoft.Extensions.Logging; +using ClassIsland.Core.Models.Notification; +using SystemTools.Services; +using SystemTools.Settings; +using ClassIsland.Shared; using System; using System.Diagnostics; using System.Threading.Tasks; @@ -8,7 +12,7 @@ namespace SystemTools.Actions; [ActionInfo("SystemTools.CancelShutdown", "取消关机计划", "\uE4CC", false)] -public class CancelShutdownAction(ILogger logger) : ActionBase +public class CancelShutdownAction(ILogger logger) : ActionBase { private readonly ILogger _logger = logger; @@ -38,6 +42,12 @@ protected override async Task OnInvoke() _logger.LogError(ex, "取消关机失败"); throw; } + if (Settings.NotifyOnExecute) + IAppHost.GetService()?.ShowNotification(new NotificationRequest + { + MaskContent = NotificationContent.CreateTwoIconsMask("已取消关机计划", "\uE9FB", "") + }); + await base.OnInvoke(); _logger.LogDebug("CancelShutdownAction OnInvoke 完成"); diff --git a/Actions/ChangeWallpaperAction.cs b/Actions/ChangeWallpaperAction.cs index 3509bdf0..f2994684 100644 --- a/Actions/ChangeWallpaperAction.cs +++ b/Actions/ChangeWallpaperAction.cs @@ -1,4 +1,4 @@ -using ClassIsland.Core.Abstractions.Automation; +using ClassIsland.Core.Abstractions.Automation; using ClassIsland.Core.Attributes; using Microsoft.Extensions.Logging; using Microsoft.Win32; @@ -8,6 +8,9 @@ using System.Runtime.InteropServices; using System.Threading.Tasks; using SystemTools.Settings; +using ClassIsland.Core.Models.Notification; +using SystemTools.Services; +using ClassIsland.Shared; using Windows.Win32; namespace SystemTools.Actions; @@ -75,6 +78,12 @@ protected override async Task OnInvoke() // 保持向上抛出异常,让上层 UI/宿主决定如何反馈给用户 throw; } + if (Settings.NotifyOnExecute) + IAppHost.GetService()?.ShowNotification(new NotificationRequest + { + MaskContent = NotificationContent.CreateTwoIconsMask("已自动切换桌面壁纸", "\uE9FB", "") + }); + await base.OnInvoke(); _logger.LogDebug("ChangeWallpaperAction OnInvoke 完成"); diff --git a/Actions/ClearAllNotificationsAction.cs b/Actions/ClearAllNotificationsAction.cs index 742f18b3..677bfe40 100644 --- a/Actions/ClearAllNotificationsAction.cs +++ b/Actions/ClearAllNotificationsAction.cs @@ -2,14 +2,17 @@ using ClassIsland.Core.Abstractions.Automation; using ClassIsland.Core.Attributes; using Microsoft.Extensions.Logging; +using ClassIsland.Core.Models.Notification; +using SystemTools.Services; +using SystemTools.Settings; +using ClassIsland.Shared; using System.Reflection; using System.Threading.Tasks; -using ClassIsland.Shared; namespace SystemTools.Actions; [ActionInfo("SystemTools.ClearAllNotifications", "清除全部提醒", "\uE029", false)] -public class ClearAllNotificationsAction(ILogger logger) : ActionBase +public class ClearAllNotificationsAction(ILogger logger) : ActionBase { private readonly ILogger _logger = logger; @@ -29,6 +32,12 @@ protected override async Task OnInvoke() method.Invoke(notificationHostService, null); _logger.LogInformation("已调用 ClassIsland 提醒系统的清除全部提醒"); + if (Settings.NotifyOnExecute) + IAppHost.GetService()?.ShowNotification(new NotificationRequest + { + MaskContent = NotificationContent.CreateTwoIconsMask("已自动清除全部提醒", "\uE9FB", "") + }); + await base.OnInvoke(); _logger.LogDebug("ClearAllNotificationsAction OnInvoke 完成"); diff --git a/Actions/CloneDisplayAction.cs b/Actions/CloneDisplayAction.cs index bd95df79..30e5ce84 100644 --- a/Actions/CloneDisplayAction.cs +++ b/Actions/CloneDisplayAction.cs @@ -1,9 +1,13 @@ -using System; +using System; using System.Diagnostics; using System.Threading.Tasks; using ClassIsland.Core.Abstractions.Automation; using ClassIsland.Core.Attributes; using Microsoft.Extensions.Logging; +using ClassIsland.Core.Models.Notification; +using SystemTools.Services; +using SystemTools.Settings; +using ClassIsland.Shared; namespace SystemTools.Actions; @@ -11,7 +15,7 @@ namespace SystemTools.Actions; /// 复制屏幕 /// [ActionInfo("SystemTools.CloneDisplay", "复制屏幕", "\uE635", false)] -public class CloneDisplayAction(ILogger logger) : ActionBase +public class CloneDisplayAction(ILogger logger) : ActionBase { private readonly ILogger _logger = logger; @@ -54,6 +58,12 @@ protected override async Task OnInvoke() _logger.LogError(ex, "复制屏幕失败"); throw; } + if (Settings.NotifyOnExecute) + IAppHost.GetService()?.ShowNotification(new NotificationRequest + { + MaskContent = NotificationContent.CreateTwoIconsMask("已执行复制屏幕操作", "\uE9FB", "") + }); + await base.OnInvoke(); } diff --git a/Actions/DisableDeviceAction.cs b/Actions/DisableDeviceAction.cs index 07c1d1b7..116ba962 100644 --- a/Actions/DisableDeviceAction.cs +++ b/Actions/DisableDeviceAction.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Diagnostics; using System.IO; using System.Threading.Tasks; @@ -6,6 +6,9 @@ using ClassIsland.Core.Attributes; using Microsoft.Extensions.Logging; using SystemTools.Settings; +using ClassIsland.Core.Models.Notification; +using SystemTools.Services; +using ClassIsland.Shared; namespace SystemTools.Actions; @@ -73,6 +76,12 @@ net session >nul 2>&1 _logger.LogError(ex, "禁用设备失败"); throw; } + if (Settings.NotifyOnExecute) + IAppHost.GetService()?.ShowNotification(new NotificationRequest + { + MaskContent = NotificationContent.CreateTwoIconsMask("已自动禁用硬件设备", "\uE9FB", "") + }); + await base.OnInvoke(); _logger.LogDebug("DisableDeviceAction OnInvoke 完成"); diff --git a/Actions/EnableDeviceAction.cs b/Actions/EnableDeviceAction.cs index 68ea078b..04092b59 100644 --- a/Actions/EnableDeviceAction.cs +++ b/Actions/EnableDeviceAction.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Diagnostics; using System.IO; using System.Threading.Tasks; @@ -6,6 +6,9 @@ using ClassIsland.Core.Attributes; using Microsoft.Extensions.Logging; using SystemTools.Settings; +using ClassIsland.Core.Models.Notification; +using SystemTools.Services; +using ClassIsland.Shared; namespace SystemTools.Actions; @@ -73,6 +76,12 @@ net session >nul 2>&1 _logger.LogError(ex, "启用设备失败"); throw; } + if (Settings.NotifyOnExecute) + IAppHost.GetService()?.ShowNotification(new NotificationRequest + { + MaskContent = NotificationContent.CreateTwoIconsMask("已自动启用硬件设备", "\uE9FB", "") + }); + await base.OnInvoke(); _logger.LogDebug("EnableDeviceAction OnInvoke 完成"); diff --git a/Actions/EnableVoiceWakeAiAction.cs b/Actions/EnableVoiceWakeAiAction.cs new file mode 100644 index 00000000..bfc23fac --- /dev/null +++ b/Actions/EnableVoiceWakeAiAction.cs @@ -0,0 +1,70 @@ +using System; +using System.Threading.Tasks; +using ClassIsland.Core.Abstractions.Automation; +using ClassIsland.Core.Attributes; +using ClassIsland.Core.Models.Notification; +using ClassIsland.Shared; +using Microsoft.Extensions.Logging; +using SystemTools.Services; +using SystemTools.Settings; +using SystemTools.Shared; + +namespace SystemTools.Actions; + +[ActionInfo("SystemTools.EnableVoiceWakeAi", "启用语音唤醒 AI", "\uED53", false)] +public class EnableVoiceWakeAiAction(ILogger logger) : ActionBase +{ + private readonly ILogger _logger = logger; + + protected override async Task OnInvoke() + { + _logger.LogDebug("EnableVoiceWakeAiAction OnInvoke 开始"); + + if (Settings == null) return; + + var config = GlobalConstants.MainConfig?.Data; + if (config == null) return; + + try + { + config.EnableVoiceWakeAi = Settings.Enable; + var service = IAppHost.TryGetService(); + if (service == null) + { + config.EnableVoiceWakeAi = false; + GlobalConstants.MainConfig?.Save(); + _logger.LogWarning("AI 服务尚未加载,无法{State}语音唤醒 AI", Settings.Enable ? "开启" : "关闭"); + return; + } + + service.ApplyConfig(); + if (Settings.Enable && !service.IsWakeWordEnabled) + { + config.EnableVoiceWakeAi = false; + GlobalConstants.MainConfig?.Save(); + _logger.LogWarning("语音唤醒 AI 未能启动:{Error}", service.LastError ?? "未知错误"); + return; + } + + GlobalConstants.MainConfig?.Save(); + _logger.LogInformation("已{State}语音唤醒 AI 功能", Settings.Enable ? "开启" : "关闭"); + } + catch (Exception ex) + { + _logger.LogError(ex, "设置语音唤醒 AI 功能失败"); + throw; + } + + if (Settings.NotifyOnExecute) + { + IAppHost.GetService()?.ShowNotification(new NotificationRequest + { + MaskContent = NotificationContent.CreateTwoIconsMask( + (Settings.Enable ? "已开启功能 " : "已关闭功能 ") + "启用语音唤醒 AI", "\uED53", "") + }); + } + + await base.OnInvoke(); + _logger.LogDebug("EnableVoiceWakeAiAction OnInvoke 完成"); + } +} \ No newline at end of file diff --git a/Actions/ExtendDisplayAction.cs b/Actions/ExtendDisplayAction.cs index a181836e..880e1687 100644 --- a/Actions/ExtendDisplayAction.cs +++ b/Actions/ExtendDisplayAction.cs @@ -1,9 +1,13 @@ -using System; +using System; using System.Diagnostics; using System.Threading.Tasks; using ClassIsland.Core.Abstractions.Automation; using ClassIsland.Core.Attributes; using Microsoft.Extensions.Logging; +using ClassIsland.Core.Models.Notification; +using SystemTools.Services; +using SystemTools.Settings; +using ClassIsland.Shared; namespace SystemTools.Actions; @@ -11,7 +15,7 @@ namespace SystemTools.Actions; /// 扩展屏幕 /// [ActionInfo("SystemTools.ExtendDisplay", "扩展屏幕", "\uE647", false)] -public class ExtendDisplayAction(ILogger logger) : ActionBase +public class ExtendDisplayAction(ILogger logger) : ActionBase { private readonly ILogger _logger = logger; @@ -54,6 +58,12 @@ protected override async Task OnInvoke() _logger.LogError(ex, "扩展屏幕失败"); throw; } + if (Settings.NotifyOnExecute) + IAppHost.GetService()?.ShowNotification(new NotificationRequest + { + MaskContent = NotificationContent.CreateTwoIconsMask("已执行扩展屏幕操作", "\uE9FB", "") + }); + await base.OnInvoke(); } diff --git a/Actions/ExternalDisplayAction.cs b/Actions/ExternalDisplayAction.cs index ed6b9864..105d2e54 100644 --- a/Actions/ExternalDisplayAction.cs +++ b/Actions/ExternalDisplayAction.cs @@ -1,9 +1,13 @@ -using System; +using System; using System.Diagnostics; using System.Threading.Tasks; using ClassIsland.Core.Abstractions.Automation; using ClassIsland.Core.Attributes; using Microsoft.Extensions.Logging; +using ClassIsland.Core.Models.Notification; +using SystemTools.Services; +using SystemTools.Settings; +using ClassIsland.Shared; namespace SystemTools.Actions; @@ -11,7 +15,7 @@ namespace SystemTools.Actions; /// 仅第二屏幕 /// [ActionInfo("SystemTools.ExternalDisplay", "仅第二屏幕", "\uE641", false)] -public class ExternalDisplayAction(ILogger logger) : ActionBase +public class ExternalDisplayAction(ILogger logger) : ActionBase { private readonly ILogger _logger = logger; @@ -54,6 +58,12 @@ protected override async Task OnInvoke() _logger.LogError(ex, "仅第二屏幕失败"); throw; } + if (Settings.NotifyOnExecute) + IAppHost.GetService()?.ShowNotification(new NotificationRequest + { + MaskContent = NotificationContent.CreateTwoIconsMask("已执行仅第二屏幕操作", "\uE9FB", "") + }); + await base.OnInvoke(); } diff --git a/Actions/FullscreenClockAction.cs b/Actions/FullscreenClockAction.cs index 8fd9fbb7..52178814 100644 --- a/Actions/FullscreenClockAction.cs +++ b/Actions/FullscreenClockAction.cs @@ -1,11 +1,9 @@ using System; using System.Diagnostics; -using System.Runtime.InteropServices; using System.Threading.Tasks; using ClassIsland.Core.Abstractions.Automation; using ClassIsland.Core.Attributes; using Microsoft.Extensions.Logging; -using Windows.Win32; namespace SystemTools.Actions; @@ -25,26 +23,11 @@ protected override async Task OnInvoke() var psi = new ProcessStartInfo { - FileName = "cmd", - Arguments = $"/c start \"\" \"{ClockUrl}\"", - UseShellExecute = false, - CreateNoWindow = true, - WindowStyle = ProcessWindowStyle.Hidden + FileName = ClockUrl, + UseShellExecute = true }; Process.Start(psi); - - _logger.LogInformation("等待3秒后发送F11全屏键"); - - await Task.Delay(3000); - - _logger.LogDebug("发送F11键"); - PInvoke.keybd_event(VK_F11, 0, 0, UIntPtr.Zero); - await Task.Delay(20); - PInvoke.keybd_event(VK_F11, 0, Windows.Win32.UI.Input.KeyboardAndMouse.KEYBD_EVENT_FLAGS.KEYEVENTF_KEYUP, - UIntPtr.Zero); - - _logger.LogInformation("F11全屏键已发送"); } catch (Exception ex) { @@ -55,10 +38,4 @@ protected override async Task OnInvoke() await base.OnInvoke(); _logger.LogDebug("FullscreenClockAction OnInvoke 完成"); } - - //[DllImport("user32.dll", SetLastError = true)] - //private static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, UIntPtr dwExtraInfo); - - private const byte VK_F11 = 0x7A; - //private const uint KEYEVENTF_KEYUP = 0x0002; -} \ No newline at end of file +} diff --git a/Actions/InternalDisplayAction.cs b/Actions/InternalDisplayAction.cs index 2d8440f3..c91e345f 100644 --- a/Actions/InternalDisplayAction.cs +++ b/Actions/InternalDisplayAction.cs @@ -1,9 +1,13 @@ -using System; +using System; using System.Diagnostics; using System.Threading.Tasks; using ClassIsland.Core.Abstractions.Automation; using ClassIsland.Core.Attributes; using Microsoft.Extensions.Logging; +using ClassIsland.Core.Models.Notification; +using SystemTools.Services; +using SystemTools.Settings; +using ClassIsland.Shared; namespace SystemTools.Actions; @@ -11,7 +15,7 @@ namespace SystemTools.Actions; /// 仅电脑屏幕 /// [ActionInfo("SystemTools.InternalDisplay", "仅电脑屏幕", "\uE62F", false)] -public class InternalDisplayAction(ILogger logger) : ActionBase +public class InternalDisplayAction(ILogger logger) : ActionBase { private readonly ILogger _logger = logger; @@ -54,6 +58,12 @@ protected override async Task OnInvoke() _logger.LogError(ex, "仅电脑屏幕失败"); throw; } + if (Settings.NotifyOnExecute) + IAppHost.GetService()?.ShowNotification(new NotificationRequest + { + MaskContent = NotificationContent.CreateTwoIconsMask("已执行仅电脑屏幕操作", "\uE9FB", "") + }); + await base.OnInvoke(); } diff --git a/Actions/KillProcessAction.cs b/Actions/KillProcessAction.cs index cb9308f1..13d85c7c 100644 --- a/Actions/KillProcessAction.cs +++ b/Actions/KillProcessAction.cs @@ -1,4 +1,4 @@ -using ClassIsland.Core.Abstractions.Automation; +using ClassIsland.Core.Abstractions.Automation; using ClassIsland.Core.Attributes; using Microsoft.Extensions.Logging; using System; @@ -6,6 +6,9 @@ using System.Net.Http; using System.Threading.Tasks; using SystemTools.Settings; +using ClassIsland.Core.Models.Notification; +using SystemTools.Services; +using ClassIsland.Shared; namespace SystemTools.Actions; @@ -80,6 +83,12 @@ protected override async Task OnInvoke() // _logger.LogWarning("终止进程 {ProcessName} 可能失败,退出码: {ExitCode}, 错误: {Error}", // processName, process.ExitCode, error); //} + if (Settings.NotifyOnExecute) + IAppHost.GetService()?.ShowNotification(new NotificationRequest + { + MaskContent = NotificationContent.CreateTwoIconsMask("已执行退出进程操作", "\uE9FB", "") + }); + await base.OnInvoke(); _logger.LogDebug("KillProcessAction OnInvoke 完成"); diff --git a/Actions/LoadTemporaryClassPlanAction.cs b/Actions/LoadTemporaryClassPlanAction.cs index d5fc783e..a8386477 100644 --- a/Actions/LoadTemporaryClassPlanAction.cs +++ b/Actions/LoadTemporaryClassPlanAction.cs @@ -6,6 +6,9 @@ using System.Collections.Concurrent; using System.Threading.Tasks; using SystemTools.Settings; +using ClassIsland.Core.Models.Notification; +using SystemTools.Services; +using ClassIsland.Shared; namespace SystemTools.Actions; @@ -46,6 +49,12 @@ protected override async Task OnInvoke() _profileService.Profile.TempClassPlanSetupTime = _exactTimeService.GetCurrentLocalDateTime(); _profileService.SaveProfile(); _logger.LogInformation("已加载临时课表:{ClassPlanName} ({ClassPlanId})", classPlan.Name, classPlanId); + if (Settings.NotifyOnExecute) + IAppHost.GetService()?.ShowNotification(new NotificationRequest + { + MaskContent = NotificationContent.CreateTwoIconsMask("已自动加载临时课表", "\uE9FB", "") + }); + await base.OnInvoke(); } diff --git a/Actions/LockScreenAction.cs b/Actions/LockScreenAction.cs index 0a89b8a1..6f488c37 100644 --- a/Actions/LockScreenAction.cs +++ b/Actions/LockScreenAction.cs @@ -1,6 +1,10 @@ -using ClassIsland.Core.Abstractions.Automation; +using ClassIsland.Core.Abstractions.Automation; using ClassIsland.Core.Attributes; using Microsoft.Extensions.Logging; +using ClassIsland.Core.Models.Notification; +using SystemTools.Services; +using SystemTools.Settings; +using ClassIsland.Shared; using System; using System.Diagnostics; using System.Threading.Tasks; @@ -8,7 +12,7 @@ namespace SystemTools.Actions; [ActionInfo("SystemTools.LockScreen", "锁定屏幕", "\uEAF0", false)] -public class LockScreenAction(ILogger logger) : ActionBase +public class LockScreenAction(ILogger logger) : ActionBase { private readonly ILogger _logger = logger; @@ -38,6 +42,12 @@ protected override async Task OnInvoke() _logger.LogError(ex, "锁定屏幕失败"); throw; } + if (Settings.NotifyOnExecute) + IAppHost.GetService()?.ShowNotification(new NotificationRequest + { + MaskContent = NotificationContent.CreateTwoIconsMask("已锁定屏幕", "\uE9FB", "") + }); + await base.OnInvoke(); _logger.LogDebug("LockScreenAction OnInvoke 完成"); diff --git a/Actions/OpenAppSettingsAction.cs b/Actions/OpenAppSettingsAction.cs index 845f1e20..18c5a50e 100644 --- a/Actions/OpenAppSettingsAction.cs +++ b/Actions/OpenAppSettingsAction.cs @@ -2,6 +2,10 @@ using ClassIsland.Core.Abstractions.Services; using ClassIsland.Core.Attributes; using Microsoft.Extensions.Logging; +using ClassIsland.Core.Models.Notification; +using SystemTools.Services; +using SystemTools.Settings; +using ClassIsland.Shared; using System; using System.Threading.Tasks; @@ -10,7 +14,7 @@ namespace SystemTools.Actions; [ActionInfo("SystemTools.OpenAppSettings", "打开应用设置", "\uEF27", false)] public class OpenAppSettingsAction( ILogger logger, - IUriNavigationService uriNavigationService) : ActionBase + IUriNavigationService uriNavigationService) : ActionBase { private readonly ILogger _logger = logger; private readonly IUriNavigationService _uriNavigationService = uriNavigationService; @@ -19,6 +23,12 @@ protected override Task OnInvoke() { _logger.LogInformation("正在打开 ClassIsland 应用设置窗口"); _uriNavigationService.NavigateWrapped(new Uri("classisland://app/settings")); + if (Settings.NotifyOnExecute) + IAppHost.GetService()?.ShowNotification(new NotificationRequest + { + MaskContent = NotificationContent.CreateTwoIconsMask("已自动打开应用设置", "\uE9FB", "") + }); + return base.OnInvoke(); } } diff --git a/Actions/OpenClassSwapWindowAction.cs b/Actions/OpenClassSwapWindowAction.cs index c26535af..aa880b37 100644 --- a/Actions/OpenClassSwapWindowAction.cs +++ b/Actions/OpenClassSwapWindowAction.cs @@ -2,6 +2,10 @@ using ClassIsland.Core.Abstractions.Services; using ClassIsland.Core.Attributes; using Microsoft.Extensions.Logging; +using ClassIsland.Core.Models.Notification; +using SystemTools.Services; +using SystemTools.Settings; +using ClassIsland.Shared; using System; using System.Threading.Tasks; @@ -10,7 +14,7 @@ namespace SystemTools.Actions; [ActionInfo("SystemTools.OpenClassSwapWindow", "打开换课窗口", "\uE13B", false)] public class OpenClassSwapWindowAction( ILogger logger, - IUriNavigationService uriNavigationService) : ActionBase + IUriNavigationService uriNavigationService) : ActionBase { private readonly ILogger _logger = logger; private readonly IUriNavigationService _uriNavigationService = uriNavigationService; @@ -19,6 +23,12 @@ protected override Task OnInvoke() { _logger.LogInformation("正在打开 ClassIsland 换课窗口"); _uriNavigationService.NavigateWrapped(new Uri("classisland://app/class-swap")); + if (Settings.NotifyOnExecute) + IAppHost.GetService()?.ShowNotification(new NotificationRequest + { + MaskContent = NotificationContent.CreateTwoIconsMask("已自动打开换课窗口", "\uE9FB", "") + }); + return base.OnInvoke(); } } diff --git a/Actions/OpenProfileEditorAction.cs b/Actions/OpenProfileEditorAction.cs index 9a444405..fed3997f 100644 --- a/Actions/OpenProfileEditorAction.cs +++ b/Actions/OpenProfileEditorAction.cs @@ -2,6 +2,10 @@ using ClassIsland.Core.Abstractions.Services; using ClassIsland.Core.Attributes; using Microsoft.Extensions.Logging; +using ClassIsland.Core.Models.Notification; +using SystemTools.Services; +using SystemTools.Settings; +using ClassIsland.Shared; using System; using System.Threading.Tasks; @@ -10,7 +14,7 @@ namespace SystemTools.Actions; [ActionInfo("SystemTools.OpenProfileEditor", "打开档案编辑", "\uE699", false)] public class OpenProfileEditorAction( ILogger logger, - IUriNavigationService uriNavigationService) : ActionBase + IUriNavigationService uriNavigationService) : ActionBase { private readonly ILogger _logger = logger; private readonly IUriNavigationService _uriNavigationService = uriNavigationService; @@ -19,6 +23,12 @@ protected override Task OnInvoke() { _logger.LogInformation("正在打开 ClassIsland 档案编辑窗口"); _uriNavigationService.NavigateWrapped(new Uri("classisland://app/profile")); + if (Settings.NotifyOnExecute) + IAppHost.GetService()?.ShowNotification(new NotificationRequest + { + MaskContent = NotificationContent.CreateTwoIconsMask("已自动打开档案编辑", "\uE9FB", "") + }); + return base.OnInvoke(); } } diff --git a/Actions/ScreenShotAction.cs b/Actions/ScreenShotAction.cs index 032d0b2e..35f2b09a 100644 --- a/Actions/ScreenShotAction.cs +++ b/Actions/ScreenShotAction.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Drawing; using System.Drawing.Imaging; using System.IO; @@ -8,6 +8,9 @@ using ClassIsland.Core.Attributes; using Microsoft.Extensions.Logging; using SystemTools.Settings; +using ClassIsland.Core.Models.Notification; +using SystemTools.Services; +using ClassIsland.Shared; namespace SystemTools.Actions; @@ -58,6 +61,12 @@ protected override async Task OnInvoke() _logger.LogError(ex, "屏幕截图失败"); throw; } + if (Settings.NotifyOnExecute) + IAppHost.GetService()?.ShowNotification(new NotificationRequest + { + MaskContent = NotificationContent.CreateTwoIconsMask("已自动执行屏幕截图", "\uE9FB", "") + }); + await base.OnInvoke(); _logger.LogDebug("ScreenShotAction OnInvoke 完成"); diff --git a/Actions/SetVolume.cs b/Actions/SetVolume.cs index f783166e..bddc4e8c 100644 --- a/Actions/SetVolume.cs +++ b/Actions/SetVolume.cs @@ -1,6 +1,10 @@ -using ClassIsland.Core.Abstractions.Automation; +using ClassIsland.Core.Abstractions.Automation; using ClassIsland.Core.Attributes; using Microsoft.Extensions.Logging; +using ClassIsland.Core.Models.Notification; +using SystemTools.Services; +using SystemTools.Settings; +using ClassIsland.Shared; using System; using System.Runtime.InteropServices; using System.Threading.Tasks; @@ -31,6 +35,12 @@ protected override async Task OnInvoke() _logger.LogError(ex, "设置音量失败"); throw; } + + if (Settings.NotifyOnExecute) + IAppHost.GetService()?.ShowNotification(new NotificationRequest + { + MaskContent = NotificationContent.CreateTwoIconsMask("已自动调整系统音量", "\uE9FB", "") + }); } } diff --git a/Actions/ShowAiChatDialogAction.cs b/Actions/ShowAiChatDialogAction.cs new file mode 100644 index 00000000..3530758e --- /dev/null +++ b/Actions/ShowAiChatDialogAction.cs @@ -0,0 +1,20 @@ +using System.Threading.Tasks; +using ClassIsland.Core.Abstractions.Automation; +using ClassIsland.Core.Attributes; +using Microsoft.Extensions.Logging; +using SystemTools.Services; + +namespace SystemTools.Actions; + +[ActionInfo("SystemTools.ShowAiChatDialog", "显示AI对话框", "\uE8C3", false)] +public class ShowAiChatDialogAction( + ILogger logger, + AiChatWindowService aiChatWindowService) : ActionBase +{ + protected override async Task OnInvoke() + { + logger.LogInformation("正在显示 AI 对话框"); + await aiChatWindowService.ShowAsync(); + await base.OnInvoke(); + } +} diff --git a/Actions/ShowDesktopAction.cs b/Actions/ShowDesktopAction.cs index 0cec00df..aa79f704 100644 --- a/Actions/ShowDesktopAction.cs +++ b/Actions/ShowDesktopAction.cs @@ -1,6 +1,10 @@ using ClassIsland.Core.Abstractions.Automation; using ClassIsland.Core.Attributes; using Microsoft.Extensions.Logging; +using ClassIsland.Core.Models.Notification; +using SystemTools.Services; +using SystemTools.Settings; +using ClassIsland.Shared; using System; using System.Threading.Tasks; using Windows.Win32; @@ -9,7 +13,7 @@ namespace SystemTools.Actions; [ActionInfo("SystemTools.ShowDesktop", "显示桌面", "\uE62F", false)] -public class ShowDesktopAction(ILogger logger) : ActionBase +public class ShowDesktopAction(ILogger logger) : ActionBase { private readonly ILogger _logger = logger; @@ -39,6 +43,12 @@ protected override async Task OnInvoke() _logger.LogError(ex, "发送 Win+D 失败"); throw; } + if (Settings.NotifyOnExecute) + IAppHost.GetService()?.ShowNotification(new NotificationRequest + { + MaskContent = NotificationContent.CreateTwoIconsMask("已执行显示桌面操作", "\uE9FB", "") + }); + await base.OnInvoke(); } diff --git a/Actions/ShowFloatingWindowAction.cs b/Actions/ShowFloatingWindowAction.cs index 94aceae3..0006c51a 100644 --- a/Actions/ShowFloatingWindowAction.cs +++ b/Actions/ShowFloatingWindowAction.cs @@ -6,6 +6,8 @@ using Microsoft.Extensions.Logging; using SystemTools.Services; using SystemTools.Settings; +using ClassIsland.Core.Models.Notification; +using ClassIsland.Shared; using SystemTools.Shared; namespace SystemTools.Actions; @@ -55,6 +57,12 @@ protected override async Task OnInvoke() _logger.LogError(ex, "更新悬浮窗状态失败"); throw; } + if (Settings.NotifyOnExecute) + IAppHost.GetService()?.ShowNotification(new NotificationRequest + { + MaskContent = NotificationContent.CreateTwoIconsMask("已自动显示悬浮窗", "\uE9FB", "") + }); + await base.OnInvoke(); _logger.LogDebug("ShowFloatingWindowAction OnInvoke 完成"); diff --git a/Actions/ShutdownAction.cs b/Actions/ShutdownAction.cs index 4f921a92..fca7244e 100644 --- a/Actions/ShutdownAction.cs +++ b/Actions/ShutdownAction.cs @@ -1,4 +1,4 @@ -using ClassIsland.Core.Abstractions.Automation; +using ClassIsland.Core.Abstractions.Automation; using ClassIsland.Core.Attributes; using Microsoft.Extensions.Logging; using System; @@ -6,6 +6,9 @@ using System.Threading.Tasks; using System.Windows.Forms; using SystemTools.Settings; +using ClassIsland.Core.Models.Notification; +using SystemTools.Services; +using ClassIsland.Shared; namespace SystemTools.Actions; @@ -45,6 +48,12 @@ protected override async Task OnInvoke() _logger.LogError(ex, "执行关机失败"); throw; } + if (Settings.NotifyOnExecute) + IAppHost.GetService()?.ShowNotification(new NotificationRequest + { + MaskContent = NotificationContent.CreateTwoIconsMask("已执行计时关机", "\uE9FB", "") + }); + await base.OnInvoke(); } diff --git a/Actions/SimulateKeyCombinationAction.cs b/Actions/SimulateKeyCombinationAction.cs new file mode 100644 index 00000000..bbede1c5 --- /dev/null +++ b/Actions/SimulateKeyCombinationAction.cs @@ -0,0 +1,80 @@ +using ClassIsland.Core.Abstractions.Automation; +using ClassIsland.Core.Attributes; +using ClassIsland.Core.Models.Notification; +using ClassIsland.Shared; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using SystemTools.Services; +using SystemTools.Settings; +using Windows.Win32; +using Windows.Win32.UI.Input.KeyboardAndMouse; + +namespace SystemTools.Actions; + +[ActionInfo("SystemTools.SimulateKeyCombination", "模拟组合键", "\uEA15", false)] +public class SimulateKeyCombinationAction(ILogger logger) : ActionBase +{ + private readonly ILogger _logger = logger; + private const int KeyEventDelay = 20; + private const int KeyHoldDelay = 40; + private const int MaxKeyCount = 5; + + protected override async Task OnInvoke() + { + _logger.LogDebug("SimulateKeyCombinationAction OnInvoke 开始"); + + var keys = Settings?.Keys + .Where(x => x.KeyCode is > 0) + .Take(MaxKeyCount) + .Select(x => x.KeyCode!.Value) + .ToList() ?? []; + + if (keys.Count == 0) + { + _logger.LogWarning("没有录入的组合键按键"); + return; + } + + var pressedKeys = new List(); + try + { + _logger.LogInformation("正在模拟同时按下 {Count} 个按键", keys.Count); + + foreach (var keyCode in keys) + { + PInvoke.keybd_event(keyCode, 0, 0, UIntPtr.Zero); + pressedKeys.Add(keyCode); + await Task.Delay(KeyEventDelay); + } + + await Task.Delay(KeyHoldDelay); + } + catch (Exception ex) + { + _logger.LogError(ex, "模拟组合键失败"); + throw; + } + finally + { + for (var i = pressedKeys.Count - 1; i >= 0; i--) + { + PInvoke.keybd_event(pressedKeys[i], 0, KEYBD_EVENT_FLAGS.KEYEVENTF_KEYUP, UIntPtr.Zero); + await Task.Delay(KeyEventDelay); + } + } + + if (Settings?.NotifyOnExecute == true) + { + IAppHost.GetService()?.ShowNotification(new NotificationRequest + { + MaskContent = NotificationContent.CreateTwoIconsMask("已完成模拟组合键", "\uE9FB", "") + }); + } + + await base.OnInvoke(); + _logger.LogDebug("SimulateKeyCombinationAction OnInvoke 完成"); + } +} \ No newline at end of file diff --git a/Actions/SimulateKeyboardAction.cs b/Actions/SimulateKeyboardAction.cs index a38e6f9a..5be86e1c 100644 --- a/Actions/SimulateKeyboardAction.cs +++ b/Actions/SimulateKeyboardAction.cs @@ -1,10 +1,13 @@ -using ClassIsland.Core.Abstractions.Automation; +using ClassIsland.Core.Abstractions.Automation; using ClassIsland.Core.Attributes; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Threading.Tasks; using SystemTools.Settings; +using ClassIsland.Core.Models.Notification; +using SystemTools.Services; +using ClassIsland.Shared; using Windows.Win32; namespace SystemTools.Actions; @@ -67,6 +70,12 @@ protected override async Task OnInvoke() _logger.LogError(ex, "模拟键盘失败"); throw; } + if (Settings.NotifyOnExecute) + IAppHost.GetService()?.ShowNotification(new NotificationRequest + { + MaskContent = NotificationContent.CreateTwoIconsMask("已完成模拟键盘输入", "\uE9FB", "") + }); + await base.OnInvoke(); _logger.LogDebug("SimulateKeyboardAction OnInvoke 完成"); diff --git a/Actions/SimulateMouseAction.cs b/Actions/SimulateMouseAction.cs index 70bbeffa..a0a7e26c 100644 --- a/Actions/SimulateMouseAction.cs +++ b/Actions/SimulateMouseAction.cs @@ -1,4 +1,4 @@ -using ClassIsland.Core.Abstractions.Automation; +using ClassIsland.Core.Abstractions.Automation; using ClassIsland.Core.Attributes; using Microsoft.Extensions.Logging; using System; @@ -7,6 +7,9 @@ using System.Runtime.InteropServices; using System.Threading.Tasks; using SystemTools.Settings; +using ClassIsland.Core.Models.Notification; +using SystemTools.Services; +using ClassIsland.Shared; using Windows.Win32; namespace SystemTools.Actions; @@ -176,6 +179,12 @@ protected override async Task OnInvoke() await ExecuteBatchFile("huifu.bat", "启用鼠标"); } } + if (Settings.NotifyOnExecute) + IAppHost.GetService()?.ShowNotification(new NotificationRequest + { + MaskContent = NotificationContent.CreateTwoIconsMask("已结束自动操作自动化", "\uE9FB", "") + }); + await base.OnInvoke(); _logger.LogDebug("SimulateMouseAction OnInvoke 完成"); diff --git a/Actions/SwitchFloatingWindowThemeAction.cs b/Actions/SwitchFloatingWindowThemeAction.cs index f33b6aed..502d01c1 100644 --- a/Actions/SwitchFloatingWindowThemeAction.cs +++ b/Actions/SwitchFloatingWindowThemeAction.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.Logging; using SystemTools.Services; using SystemTools.Settings; +using ClassIsland.Core.Models.Notification; using SystemTools.Shared; namespace SystemTools.Actions; @@ -55,6 +56,12 @@ protected override async Task OnInvoke() _logger.LogError(ex, "切换悬浮窗主题失败"); throw; } + if (Settings.NotifyOnExecute) + IAppHost.GetService()?.ShowNotification(new NotificationRequest + { + MaskContent = NotificationContent.CreateTwoIconsMask("已自动切换悬浮窗主题", "\uE9FB", "") + }); + await base.OnInvoke(); _logger.LogDebug("SwitchFloatingWindowThemeAction OnInvoke 完成"); @@ -90,6 +97,7 @@ private static string GetThemeName(int theme) 0 => "跟随系统", 1 => "浅色", 2 => "深色", + 3 => "自适应背景", _ => "未知" }; } diff --git a/Actions/SwitchThemeAction.cs b/Actions/SwitchThemeAction.cs index 119f94be..c63aa9ee 100644 --- a/Actions/SwitchThemeAction.cs +++ b/Actions/SwitchThemeAction.cs @@ -1,4 +1,4 @@ -using ClassIsland.Core.Abstractions.Automation; +using ClassIsland.Core.Abstractions.Automation; using ClassIsland.Core.Attributes; using Microsoft.Extensions.Logging; using Microsoft.Win32; @@ -6,6 +6,9 @@ using System.Diagnostics; using System.Threading.Tasks; using SystemTools.Settings; +using ClassIsland.Core.Models.Notification; +using SystemTools.Services; +using ClassIsland.Shared; namespace SystemTools.Actions; @@ -34,6 +37,12 @@ protected override async Task OnInvoke() _logger.LogError(ex, "切换主题失败"); throw; } + if (Settings.NotifyOnExecute) + IAppHost.GetService()?.ShowNotification(new NotificationRequest + { + MaskContent = NotificationContent.CreateTwoIconsMask("已自动切换系统主题色", "\uE9FB", "") + }); + await base.OnInvoke(); } diff --git a/Actions/ToggleFloatingWindowLayerAction.cs b/Actions/ToggleFloatingWindowLayerAction.cs index d4dad2ca..56f5535a 100644 --- a/Actions/ToggleFloatingWindowLayerAction.cs +++ b/Actions/ToggleFloatingWindowLayerAction.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.Logging; using SystemTools.Services; using SystemTools.Settings; +using ClassIsland.Core.Models.Notification; using SystemTools.Shared; namespace SystemTools.Actions; @@ -61,6 +62,12 @@ protected override async Task OnInvoke() _logger.LogError(ex, "切换悬浮窗层级失败"); throw; } + if (Settings.NotifyOnExecute) + IAppHost.GetService()?.ShowNotification(new NotificationRequest + { + MaskContent = NotificationContent.CreateTwoIconsMask("已自动切换悬浮窗层级", "\uE9FB", "") + }); + await base.OnInvoke(); _logger.LogDebug("ToggleFloatingWindowLayerAction OnInvoke 完成"); diff --git a/Actions/ToggleFloatingWindowProfileAction.cs b/Actions/ToggleFloatingWindowProfileAction.cs index fae43bba..82eb502f 100644 --- a/Actions/ToggleFloatingWindowProfileAction.cs +++ b/Actions/ToggleFloatingWindowProfileAction.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.Logging; using SystemTools.Services; using SystemTools.Settings; +using ClassIsland.Core.Models.Notification; namespace SystemTools.Actions; @@ -56,6 +57,12 @@ protected override async Task OnInvoke() _logger.LogError(ex, "切换悬浮窗配置方案失败"); throw; } + if (Settings.NotifyOnExecute) + IAppHost.GetService()?.ShowNotification(new NotificationRequest + { + MaskContent = NotificationContent.CreateTwoIconsMask("已自动切换悬浮窗配置方案", "\uE9FB", "") + }); + await base.OnInvoke(); _logger.LogDebug("ToggleFloatingWindowProfileAction OnInvoke 完成"); diff --git a/Actions/ToggleWorkflowAction.cs b/Actions/ToggleWorkflowAction.cs index 86fef99d..a7515235 100644 --- a/Actions/ToggleWorkflowAction.cs +++ b/Actions/ToggleWorkflowAction.cs @@ -12,7 +12,7 @@ namespace SystemTools.Actions; -[ActionInfo("SystemTools.ToggleWorkflow", "开关自动化", "\uE8B8", false)] +[ActionInfo("SystemTools.ToggleWorkflow", "开关自动化", "\uE051", false)] public class ToggleWorkflowAction(ILogger logger) : ActionBase { private readonly ILogger _logger = logger; diff --git a/Actions/TypeContentAction.cs b/Actions/TypeContentAction.cs index 3f059ac5..905b34ee 100644 --- a/Actions/TypeContentAction.cs +++ b/Actions/TypeContentAction.cs @@ -1,10 +1,13 @@ -using ClassIsland.Core.Abstractions.Automation; +using ClassIsland.Core.Abstractions.Automation; using ClassIsland.Core.Attributes; using Microsoft.Extensions.Logging; using System; using System.Runtime.InteropServices; using System.Threading.Tasks; using SystemTools.Settings; +using ClassIsland.Core.Models.Notification; +using SystemTools.Services; +using ClassIsland.Shared; using Windows.Win32; using Windows.Win32.Foundation; @@ -49,6 +52,12 @@ protected override async Task OnInvoke() _logger.LogError(ex, "键入内容失败"); throw; } + if (Settings.NotifyOnExecute) + IAppHost.GetService()?.ShowNotification(new NotificationRequest + { + MaskContent = NotificationContent.CreateTwoIconsMask("已自动键入内容", "\uE9FB", "") + }); + await base.OnInvoke(); _logger.LogDebug("OnInvoke 完成"); diff --git a/Actions/WakeUpVoiceConversationAiAction.cs b/Actions/WakeUpVoiceConversationAiAction.cs new file mode 100644 index 00000000..42e336f3 --- /dev/null +++ b/Actions/WakeUpVoiceConversationAiAction.cs @@ -0,0 +1,25 @@ +using System.Threading.Tasks; +using ClassIsland.Core.Abstractions.Automation; +using ClassIsland.Core.Attributes; +using Microsoft.Extensions.Logging; +using SystemTools.Services; + +namespace SystemTools.Actions; + +[ActionInfo("SystemTools.WakeUpVoiceConversationAi", "唤醒语音对话 AI", "\uEFF9", false)] +public class WakeUpVoiceConversationAiAction( + ILogger logger, + AiVoiceConversationService voiceConversationService) : ActionBase +{ + protected override async Task OnInvoke() + { + logger.LogInformation("正在通过行动唤醒语音对话 AI"); + + if (!voiceConversationService.TryStartVoiceConversation()) + { + logger.LogWarning("唤醒语音对话 AI 失败:{Error}", voiceConversationService.LastError ?? "未知错误"); + } + + await base.OnInvoke(); + } +} \ No newline at end of file diff --git a/Actions/WindowOperationAction.cs b/Actions/WindowOperationAction.cs index 2baa059d..66ccd024 100644 --- a/Actions/WindowOperationAction.cs +++ b/Actions/WindowOperationAction.cs @@ -1,10 +1,13 @@ -using System; +using System; using System.Runtime.InteropServices; using System.Threading.Tasks; using ClassIsland.Core.Abstractions.Automation; using ClassIsland.Core.Attributes; using Microsoft.Extensions.Logging; using SystemTools.Settings; +using ClassIsland.Core.Models.Notification; +using SystemTools.Services; +using ClassIsland.Shared; using Windows.Win32; namespace SystemTools.Actions; @@ -52,6 +55,12 @@ protected override async Task OnInvoke() _logger.LogError(ex, "窗口操作失败"); throw; } + if (Settings.NotifyOnExecute) + IAppHost.GetService()?.ShowNotification(new NotificationRequest + { + MaskContent = NotificationContent.CreateTwoIconsMask("已自动切换窗口样式", "\uE9FB", "") + }); + await base.OnInvoke(); _logger.LogDebug("WindowOperationAction OnInvoke 完成"); diff --git a/Config/MainWindowClickTriggerConfig.cs b/Config/MainWindowClickTriggerConfig.cs new file mode 100644 index 00000000..dc68a73e --- /dev/null +++ b/Config/MainWindowClickTriggerConfig.cs @@ -0,0 +1,5 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace SystemTools.Triggers; + +public sealed class MainWindowClickTriggerConfig : ObservableRecipient; diff --git a/ConfigHandlers/LiquidGlassButtonSettings.cs b/ConfigHandlers/LiquidGlassButtonSettings.cs new file mode 100644 index 00000000..537dbdad --- /dev/null +++ b/ConfigHandlers/LiquidGlassButtonSettings.cs @@ -0,0 +1,85 @@ +using System; +using System.ComponentModel; +using System.Runtime.CompilerServices; +using System.Text.Json.Serialization; + +namespace SystemTools.ConfigHandlers; + +/// +/// Persisted interaction and shadow settings shared by the approval buttons. +/// +public sealed class LiquidGlassButtonSettings : INotifyPropertyChanged +{ + private double _scaleDip = 3.5; + private bool _interactiveHighlightEnabled = true; + private bool _shadowEnabled = true; + private double _shadowRadius = 14; + private double _shadowOffsetX; + private double _shadowOffsetY = 2; + private double _shadowOpacity = 0.55; + + public event PropertyChangedEventHandler? PropertyChanged; + + [JsonPropertyName("scaleDip")] + public double ScaleDip + { + get => _scaleDip; + set => Set(ref _scaleDip, Clamp(value, 0, 12)); + } + + [JsonPropertyName("interactiveHighlightEnabled")] + public bool InteractiveHighlightEnabled + { + get => _interactiveHighlightEnabled; + set => Set(ref _interactiveHighlightEnabled, value); + } + + [JsonPropertyName("shadowEnabled")] + public bool ShadowEnabled + { + get => _shadowEnabled; + set => Set(ref _shadowEnabled, value); + } + + [JsonPropertyName("shadowRadius")] + public double ShadowRadius + { + get => _shadowRadius; + set => Set(ref _shadowRadius, Clamp(value, 0, 64)); + } + + [JsonPropertyName("shadowOffsetX")] + public double ShadowOffsetX + { + get => _shadowOffsetX; + set => Set(ref _shadowOffsetX, Clamp(value, -32, 32)); + } + + [JsonPropertyName("shadowOffsetY")] + public double ShadowOffsetY + { + get => _shadowOffsetY; + set => Set(ref _shadowOffsetY, Clamp(value, -32, 32)); + } + + [JsonPropertyName("shadowOpacity")] + public double ShadowOpacity + { + get => _shadowOpacity; + set => Set(ref _shadowOpacity, Clamp(value, 0, 1)); + } + + private void Set(ref T field, T value, [CallerMemberName] string? propertyName = null) + { + if (Equals(field, value)) + { + return; + } + + field = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + private static double Clamp(double value, double minimum, double maximum) => + double.IsFinite(value) ? Math.Clamp(value, minimum, maximum) : minimum; +} diff --git a/ConfigHandlers/LiquidGlassSettings.cs b/ConfigHandlers/LiquidGlassSettings.cs new file mode 100644 index 00000000..b3d2d339 --- /dev/null +++ b/ConfigHandlers/LiquidGlassSettings.cs @@ -0,0 +1,331 @@ +using System; +using System.ComponentModel; +using System.Runtime.CompilerServices; +using System.Text.Json.Serialization; + +namespace SystemTools.ConfigHandlers; + +public sealed class LiquidGlassSettings : INotifyPropertyChanged +{ + private bool _suppressNotifications; + private double _cornerRadius = 18; + private double _backdropRefreshIntervalMs = 50; + private double _backdropZoom = 1; + private double _backdropOffsetX; + private double _backdropOffsetY; + private double _refractionHeight = 12; + private double _refractionAmount = 24; + private bool _depthEffect; + private bool _chromaticAberration; + private double _blurRadius = 2; + private double _vibrancy = 1.5; + private double _brightness; + private double _contrast = 1; + private double _exposureEv; + private double _gammaPower = 1; + private double _backdropOpacity = 1; + private string _tintColor = "#00000000"; + private string _surfaceColor = "#00000000"; + private bool _progressiveBlurEnabled; + private double _progressiveBlurStart = 0.5; + private double _progressiveBlurEnd = 1; + private string _progressiveTintColor = "#00000000"; + private double _progressiveTintIntensity = 0.8; + private bool _adaptiveLuminanceEnabled; + private double _adaptiveLuminanceUpdateIntervalMs = 250; + private double _adaptiveLuminanceSmoothing = 0.2; + private bool _highlightEnabled = true; + private double _highlightWidth = 0.5; + private double _highlightBlurRadius = 0.25; + private double _highlightOpacity = 0.5; + private double _highlightAngle = 45; + private double _highlightFalloff = 1; + private bool _shadowEnabled = true; + private double _shadowRadius = 24; + private double _shadowOffsetX; + private double _shadowOffsetY = 4; + private string _shadowColor = "#1A000000"; + private double _shadowOpacity = 1; + private bool _innerShadowEnabled; + private double _innerShadowRadius = 24; + private double _innerShadowOffsetX; + private double _innerShadowOffsetY = 24; + private string _innerShadowColor = "#26000000"; + private double _innerShadowOpacity = 1; + + public event PropertyChangedEventHandler? PropertyChanged; + + [JsonPropertyName("cornerRadius")] + public double CornerRadius { get => _cornerRadius; set => Set(ref _cornerRadius, Clamp(value, 0, 96)); } + + [JsonPropertyName("backdropRefreshIntervalMs")] + public double BackdropRefreshIntervalMs { get => _backdropRefreshIntervalMs; set => Set(ref _backdropRefreshIntervalMs, Clamp(value, 5, 200)); } + + [JsonPropertyName("backdropZoom")] + public double BackdropZoom { get => _backdropZoom; set => Set(ref _backdropZoom, Clamp(value, 0.1, 10)); } + + [JsonPropertyName("backdropOffsetX")] + public double BackdropOffsetX { get => _backdropOffsetX; set => Set(ref _backdropOffsetX, Clamp(value, -500, 500)); } + + [JsonPropertyName("backdropOffsetY")] + public double BackdropOffsetY { get => _backdropOffsetY; set => Set(ref _backdropOffsetY, Clamp(value, -500, 500)); } + + [JsonPropertyName("refractionHeight")] + public double RefractionHeight { get => _refractionHeight; set => Set(ref _refractionHeight, Clamp(value, 0, 100)); } + + [JsonPropertyName("refractionAmount")] + public double RefractionAmount { get => _refractionAmount; set => Set(ref _refractionAmount, Clamp(value, 0, 200)); } + + [JsonPropertyName("depthEffect")] + public bool DepthEffect { get => _depthEffect; set => Set(ref _depthEffect, value); } + + [JsonPropertyName("chromaticAberration")] + public bool ChromaticAberration { get => _chromaticAberration; set => Set(ref _chromaticAberration, value); } + + [JsonPropertyName("blurRadius")] + public double BlurRadius { get => _blurRadius; set => Set(ref _blurRadius, Clamp(value, 0, 64)); } + + [JsonPropertyName("vibrancy")] + public double Vibrancy { get => _vibrancy; set => Set(ref _vibrancy, Clamp(value, 0, 4)); } + + [JsonPropertyName("brightness")] + public double Brightness { get => _brightness; set => Set(ref _brightness, Clamp(value, -1, 1)); } + + [JsonPropertyName("contrast")] + public double Contrast { get => _contrast; set => Set(ref _contrast, Clamp(value, 0, 4)); } + + [JsonPropertyName("exposureEv")] + public double ExposureEv { get => _exposureEv; set => Set(ref _exposureEv, Clamp(value, -4, 4)); } + + [JsonPropertyName("gammaPower")] + public double GammaPower { get => _gammaPower; set => Set(ref _gammaPower, Clamp(value, 0.1, 4)); } + + [JsonPropertyName("backdropOpacity")] + public double BackdropOpacity { get => _backdropOpacity; set => Set(ref _backdropOpacity, Clamp(value, 0, 1)); } + + [JsonPropertyName("tintColor")] + public string TintColor { get => _tintColor; set => Set(ref _tintColor, NormalizeColor(value)); } + + [JsonPropertyName("surfaceColor")] + public string SurfaceColor { get => _surfaceColor; set => Set(ref _surfaceColor, NormalizeColor(value)); } + + [JsonPropertyName("progressiveBlurEnabled")] + public bool ProgressiveBlurEnabled { get => _progressiveBlurEnabled; set => Set(ref _progressiveBlurEnabled, value); } + + [JsonPropertyName("progressiveBlurStart")] + public double ProgressiveBlurStart { get => _progressiveBlurStart; set => Set(ref _progressiveBlurStart, Clamp(value, 0, 1)); } + + [JsonPropertyName("progressiveBlurEnd")] + public double ProgressiveBlurEnd { get => _progressiveBlurEnd; set => Set(ref _progressiveBlurEnd, Clamp(value, 0, 1)); } + + [JsonPropertyName("progressiveTintColor")] + public string ProgressiveTintColor { get => _progressiveTintColor; set => Set(ref _progressiveTintColor, NormalizeColor(value)); } + + [JsonPropertyName("progressiveTintIntensity")] + public double ProgressiveTintIntensity { get => _progressiveTintIntensity; set => Set(ref _progressiveTintIntensity, Clamp(value, 0, 1)); } + + [JsonPropertyName("adaptiveLuminanceEnabled")] + public bool AdaptiveLuminanceEnabled { get => _adaptiveLuminanceEnabled; set => Set(ref _adaptiveLuminanceEnabled, value); } + + [JsonPropertyName("adaptiveLuminanceUpdateIntervalMs")] + public double AdaptiveLuminanceUpdateIntervalMs { get => _adaptiveLuminanceUpdateIntervalMs; set => Set(ref _adaptiveLuminanceUpdateIntervalMs, Clamp(value, 16, 5000)); } + + [JsonPropertyName("adaptiveLuminanceSmoothing")] + public double AdaptiveLuminanceSmoothing { get => _adaptiveLuminanceSmoothing; set => Set(ref _adaptiveLuminanceSmoothing, Clamp(value, 0, 1)); } + + [JsonPropertyName("highlightEnabled")] + public bool HighlightEnabled { get => _highlightEnabled; set => Set(ref _highlightEnabled, value); } + + [JsonPropertyName("highlightWidth")] + public double HighlightWidth { get => _highlightWidth; set => Set(ref _highlightWidth, Clamp(value, 0, 12)); } + + [JsonPropertyName("highlightBlurRadius")] + public double HighlightBlurRadius { get => _highlightBlurRadius; set => Set(ref _highlightBlurRadius, Clamp(value, 0, 12)); } + + [JsonPropertyName("highlightOpacity")] + public double HighlightOpacity { get => _highlightOpacity; set => Set(ref _highlightOpacity, Clamp(value, 0, 1)); } + + [JsonPropertyName("highlightAngle")] + public double HighlightAngle { get => _highlightAngle; set => Set(ref _highlightAngle, Clamp(value, 0, 360)); } + + [JsonPropertyName("highlightFalloff")] + public double HighlightFalloff { get => _highlightFalloff; set => Set(ref _highlightFalloff, Clamp(value, 0, 8)); } + + [JsonPropertyName("shadowEnabled")] + public bool ShadowEnabled { get => _shadowEnabled; set => Set(ref _shadowEnabled, value); } + + [JsonPropertyName("shadowRadius")] + public double ShadowRadius { get => _shadowRadius; set => Set(ref _shadowRadius, Clamp(value, 0, 128)); } + + [JsonPropertyName("shadowOffsetX")] + public double ShadowOffsetX { get => _shadowOffsetX; set => Set(ref _shadowOffsetX, Clamp(value, -200, 200)); } + + [JsonPropertyName("shadowOffsetY")] + public double ShadowOffsetY { get => _shadowOffsetY; set => Set(ref _shadowOffsetY, Clamp(value, -200, 200)); } + + [JsonPropertyName("shadowColor")] + public string ShadowColor { get => _shadowColor; set => Set(ref _shadowColor, NormalizeColor(value)); } + + [JsonPropertyName("shadowOpacity")] + public double ShadowOpacity { get => _shadowOpacity; set => Set(ref _shadowOpacity, Clamp(value, 0, 1)); } + + [JsonPropertyName("innerShadowEnabled")] + public bool InnerShadowEnabled { get => _innerShadowEnabled; set => Set(ref _innerShadowEnabled, value); } + + [JsonPropertyName("innerShadowRadius")] + public double InnerShadowRadius { get => _innerShadowRadius; set => Set(ref _innerShadowRadius, Clamp(value, 0, 128)); } + + [JsonPropertyName("innerShadowOffsetX")] + public double InnerShadowOffsetX { get => _innerShadowOffsetX; set => Set(ref _innerShadowOffsetX, Clamp(value, -200, 200)); } + + [JsonPropertyName("innerShadowOffsetY")] + public double InnerShadowOffsetY { get => _innerShadowOffsetY; set => Set(ref _innerShadowOffsetY, Clamp(value, -200, 200)); } + + [JsonPropertyName("innerShadowColor")] + public string InnerShadowColor { get => _innerShadowColor; set => Set(ref _innerShadowColor, NormalizeColor(value)); } + + [JsonPropertyName("innerShadowOpacity")] + public double InnerShadowOpacity { get => _innerShadowOpacity; set => Set(ref _innerShadowOpacity, Clamp(value, 0, 1)); } + + public void Reset() + { + _suppressNotifications = true; + try + { + CornerRadius = 18; + BackdropRefreshIntervalMs = 50; + BackdropZoom = 1; + BackdropOffsetX = 0; + BackdropOffsetY = 0; + RefractionHeight = 12; + RefractionAmount = 24; + DepthEffect = false; + ChromaticAberration = false; + BlurRadius = 2; + Vibrancy = 1.5; + Brightness = 0; + Contrast = 1; + ExposureEv = 0; + GammaPower = 1; + BackdropOpacity = 1; + TintColor = "#00000000"; + SurfaceColor = "#00000000"; + ProgressiveBlurEnabled = false; + ProgressiveBlurStart = 0.5; + ProgressiveBlurEnd = 1; + ProgressiveTintColor = "#00000000"; + ProgressiveTintIntensity = 0.8; + AdaptiveLuminanceEnabled = false; + AdaptiveLuminanceUpdateIntervalMs = 250; + AdaptiveLuminanceSmoothing = 0.2; + HighlightEnabled = true; + HighlightWidth = 0.5; + HighlightBlurRadius = 0.25; + HighlightOpacity = 0.5; + HighlightAngle = 45; + HighlightFalloff = 1; + ShadowEnabled = true; + ShadowRadius = 24; + ShadowOffsetX = 0; + ShadowOffsetY = 4; + ShadowColor = "#1A000000"; + ShadowOpacity = 1; + InnerShadowEnabled = false; + InnerShadowRadius = 24; + InnerShadowOffsetX = 0; + InnerShadowOffsetY = 24; + InnerShadowColor = "#26000000"; + InnerShadowOpacity = 1; + } + finally + { + _suppressNotifications = false; + } + + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(string.Empty)); + } + + /// + /// Replaces all persisted liquid-glass values in one notification batch. + /// + public void CopyFrom(LiquidGlassSettings source) + { + ArgumentNullException.ThrowIfNull(source); + + _suppressNotifications = true; + try + { + CornerRadius = source.CornerRadius; + BackdropRefreshIntervalMs = source.BackdropRefreshIntervalMs; + BackdropZoom = source.BackdropZoom; + BackdropOffsetX = source.BackdropOffsetX; + BackdropOffsetY = source.BackdropOffsetY; + RefractionHeight = source.RefractionHeight; + RefractionAmount = source.RefractionAmount; + DepthEffect = source.DepthEffect; + ChromaticAberration = source.ChromaticAberration; + BlurRadius = source.BlurRadius; + Vibrancy = source.Vibrancy; + Brightness = source.Brightness; + Contrast = source.Contrast; + ExposureEv = source.ExposureEv; + GammaPower = source.GammaPower; + BackdropOpacity = source.BackdropOpacity; + TintColor = source.TintColor; + SurfaceColor = source.SurfaceColor; + ProgressiveBlurEnabled = source.ProgressiveBlurEnabled; + ProgressiveBlurStart = source.ProgressiveBlurStart; + ProgressiveBlurEnd = source.ProgressiveBlurEnd; + ProgressiveTintColor = source.ProgressiveTintColor; + ProgressiveTintIntensity = source.ProgressiveTintIntensity; + AdaptiveLuminanceEnabled = source.AdaptiveLuminanceEnabled; + AdaptiveLuminanceUpdateIntervalMs = source.AdaptiveLuminanceUpdateIntervalMs; + AdaptiveLuminanceSmoothing = source.AdaptiveLuminanceSmoothing; + HighlightEnabled = source.HighlightEnabled; + HighlightWidth = source.HighlightWidth; + HighlightBlurRadius = source.HighlightBlurRadius; + HighlightOpacity = source.HighlightOpacity; + HighlightAngle = source.HighlightAngle; + HighlightFalloff = source.HighlightFalloff; + ShadowEnabled = source.ShadowEnabled; + ShadowRadius = source.ShadowRadius; + ShadowOffsetX = source.ShadowOffsetX; + ShadowOffsetY = source.ShadowOffsetY; + ShadowColor = source.ShadowColor; + ShadowOpacity = source.ShadowOpacity; + InnerShadowEnabled = source.InnerShadowEnabled; + InnerShadowRadius = source.InnerShadowRadius; + InnerShadowOffsetX = source.InnerShadowOffsetX; + InnerShadowOffsetY = source.InnerShadowOffsetY; + InnerShadowColor = source.InnerShadowColor; + InnerShadowOpacity = source.InnerShadowOpacity; + } + finally + { + _suppressNotifications = false; + } + + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(string.Empty)); + } + + private bool Set(ref T field, T value, [CallerMemberName] string? propertyName = null) + { + if (Equals(field, value)) + { + return false; + } + + field = value; + if (!_suppressNotifications) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + return true; + } + + private static double Clamp(double value, double minimum, double maximum) => + double.IsFinite(value) ? Math.Clamp(value, minimum, maximum) : minimum; + + private static string NormalizeColor(string? value) => value?.Trim() ?? string.Empty; +} diff --git a/ConfigHandlers/MainConfigData.cs b/ConfigHandlers/MainConfigData.cs index d764aae5..516b2e94 100644 --- a/ConfigHandlers/MainConfigData.cs +++ b/ConfigHandlers/MainConfigData.cs @@ -10,6 +10,13 @@ namespace SystemTools.ConfigHandlers; public class MainConfigData : INotifyPropertyChanged { + public MainConfigData() + { + _aiConversationLiquidGlass.PropertyChanged += OnLiquidGlassSettingsPropertyChanged; + _aiConversationApprovalButtonGlass.PropertyChanged += OnApprovalButtonGlassSettingsPropertyChanged; + _floatingWindowLiquidGlass.PropertyChanged += OnFloatingWindowLiquidGlassSettingsPropertyChanged; + } + public event EventHandler? RestartPropertyChanged; public event PropertyChangedEventHandler? PropertyChanged; @@ -88,16 +95,31 @@ public bool EnableFaceRecognition } } - bool _autoMatchMainBackgroundTheme; + bool _enableWindowsHello; - [JsonPropertyName("autoMatchMainBackgroundTheme")] - public bool AutoMatchMainBackgroundTheme + [JsonPropertyName("enableWindowsHello")] + public bool EnableWindowsHello { - get => _autoMatchMainBackgroundTheme; + get => _enableWindowsHello; set { - if (value == _autoMatchMainBackgroundTheme) return; - _autoMatchMainBackgroundTheme = value; + if (value == _enableWindowsHello) return; + _enableWindowsHello = value; + OnPropertyChanged(); + RestartPropertyChanged?.Invoke(this, EventArgs.Empty); + } + } + + bool _autoSwitchClassIslandTheme; + + [JsonPropertyName("autoSwitchClassIslandTheme")] + public bool AutoSwitchClassIslandTheme + { + get => _autoSwitchClassIslandTheme; + set + { + if (value == _autoSwitchClassIslandTheme) return; + _autoSwitchClassIslandTheme = value; OnPropertyChanged(); } } @@ -132,6 +154,35 @@ public bool AutoCleanupClassIslandMemory } } + bool _autoCleanupSystemMemory; + + [JsonPropertyName("autoCleanupSystemMemory")] + public bool AutoCleanupSystemMemory + { + get => _autoCleanupSystemMemory; + set + { + if (value == _autoCleanupSystemMemory) return; + _autoCleanupSystemMemory = value; + OnPropertyChanged(); + } + } + + int _systemMemoryCleanupThresholdPercent = 90; + + [JsonPropertyName("systemMemoryCleanupThresholdPercent")] + public int SystemMemoryCleanupThresholdPercent + { + get => _systemMemoryCleanupThresholdPercent; + set + { + var clamped = Math.Clamp(value, 50, 99); + if (clamped == _systemMemoryCleanupThresholdPercent) return; + _systemMemoryCleanupThresholdPercent = clamped; + OnPropertyChanged(); + } + } + bool _autoHideMainWindowWhenOccluded; [JsonPropertyName("autoHideMainWindowWhenOccluded")] @@ -146,6 +197,173 @@ public bool AutoHideMainWindowWhenOccluded } } + bool _enableAiService; + + [JsonPropertyName("enableAiService")] + public bool EnableAiService + { + get => _enableAiService; + set + { + if (value == _enableAiService) return; + _enableAiService = value; + OnPropertyChanged(); + RestartPropertyChanged?.Invoke(this, EventArgs.Empty); + } + } + + int _aiConversationFloatingWindowStyle; + + [JsonPropertyName("aiConversationFloatingWindowStyle")] + public int AiConversationFloatingWindowStyle + { + get => _aiConversationFloatingWindowStyle; + set + { + var normalized = value == 1 ? 1 : 0; + if (normalized == _aiConversationFloatingWindowStyle) return; + _aiConversationFloatingWindowStyle = normalized; + OnPropertyChanged(); + } + } + + LiquidGlassSettings _aiConversationLiquidGlass = new(); + + [JsonPropertyName("aiConversationLiquidGlass")] + public LiquidGlassSettings AiConversationLiquidGlass + { + get => _aiConversationLiquidGlass; + set + { + value ??= new LiquidGlassSettings(); + if (ReferenceEquals(value, _aiConversationLiquidGlass)) return; + _aiConversationLiquidGlass.PropertyChanged -= OnLiquidGlassSettingsPropertyChanged; + _aiConversationLiquidGlass = value; + _aiConversationLiquidGlass.PropertyChanged += OnLiquidGlassSettingsPropertyChanged; + OnPropertyChanged(); + } + } + + private LiquidGlassButtonSettings _aiConversationApprovalButtonGlass = new(); + + [JsonPropertyName("aiConversationApprovalButtonGlass")] + public LiquidGlassButtonSettings AiConversationApprovalButtonGlass + { + get => _aiConversationApprovalButtonGlass; + set + { + value ??= new LiquidGlassButtonSettings(); + if (ReferenceEquals(value, _aiConversationApprovalButtonGlass)) return; + _aiConversationApprovalButtonGlass.PropertyChanged -= OnApprovalButtonGlassSettingsPropertyChanged; + _aiConversationApprovalButtonGlass = value; + _aiConversationApprovalButtonGlass.PropertyChanged += OnApprovalButtonGlassSettingsPropertyChanged; + OnPropertyChanged(); + } + } + + string _aiProviderName = "OpenAI"; + + [JsonPropertyName("aiProviderName")] + public string AiProviderName + { + get => _aiProviderName; + set + { + value ??= string.Empty; + if (string.Equals(value, _aiProviderName, StringComparison.Ordinal)) return; + _aiProviderName = value; + OnPropertyChanged(); + } + } + + string _aiApiKey = string.Empty; + + [JsonPropertyName("aiApiKey")] + public string AiApiKey + { + get => _aiApiKey; + set + { + value ??= string.Empty; + if (string.Equals(value, _aiApiKey, StringComparison.Ordinal)) return; + _aiApiKey = value; + OnPropertyChanged(); + } + } + + string _aiApiUrl = "https://api.openai.com/v1"; + + [JsonPropertyName("aiApiUrl")] + public string AiApiUrl + { + get => _aiApiUrl; + set + { + value ??= string.Empty; + if (string.Equals(value, _aiApiUrl, StringComparison.Ordinal)) return; + _aiApiUrl = value; + OnPropertyChanged(); + } + } + + string _aiModel = string.Empty; + + [JsonPropertyName("aiModel")] + public string AiModel + { + get => _aiModel; + set + { + value ??= string.Empty; + if (string.Equals(value, _aiModel, StringComparison.Ordinal)) return; + _aiModel = value; + OnPropertyChanged(); + } + } + + bool _shareAiRepliesWithClassIslandNotifications; + + [JsonPropertyName("shareAiRepliesWithClassIslandNotifications")] + public bool ShareAiRepliesWithClassIslandNotifications + { + get => _shareAiRepliesWithClassIslandNotifications; + set + { + if (value == _shareAiRepliesWithClassIslandNotifications) return; + _shareAiRepliesWithClassIslandNotifications = value; + OnPropertyChanged(); + } + } + + bool _enableVoiceWakeAi; + + [JsonPropertyName("enableVoiceWakeAi")] + public bool EnableVoiceWakeAi + { + get => _enableVoiceWakeAi; + set + { + if (value == _enableVoiceWakeAi) return; + _enableVoiceWakeAi = value; + OnPropertyChanged(); + } + } + + string _aiWakeWord = "你好ci"; + + [JsonPropertyName("aiWakeWord")] + public string AiWakeWord + { + get => _aiWakeWord; + set + { + value = string.IsNullOrWhiteSpace(value) ? "你好ci" : value.Trim(); + if (string.Equals(value, _aiWakeWord, StringComparison.Ordinal)) return; + _aiWakeWord = value; + OnPropertyChanged(); + } + } + // ========== 公告相关 ========== /*string _lastAcceptedAnnouncement = string.Empty; @@ -281,7 +499,7 @@ public int FloatingWindowTheme get => _floatingWindowTheme; set { - var normalized = value is 1 or 2 ? value : 0; + var normalized = value is 1 or 2 or 3 ? value : 0; if (normalized == _floatingWindowTheme) return; _floatingWindowTheme = normalized; OnPropertyChanged(); @@ -316,6 +534,18 @@ public int FloatingWindowPositionY } } + [JsonPropertyName("actionFlowExecutionConfirmationPositionX")] + public int? ActionFlowExecutionConfirmationPositionX { get; set; } + + [JsonPropertyName("actionFlowExecutionConfirmationPositionY")] + public int? ActionFlowExecutionConfirmationPositionY { get; set; } + + [JsonPropertyName("actionFlowExecutionDelayPositionX")] + public int? ActionFlowExecutionDelayPositionX { get; set; } + + [JsonPropertyName("actionFlowExecutionDelayPositionY")] + public int? ActionFlowExecutionDelayPositionY { get; set; } + int _floatingWindowLayer = 1; [JsonPropertyName("floatingWindowLayer")] @@ -428,4 +658,83 @@ protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } + + private int _floatingWindowAppearanceStyle = 1; + + [JsonPropertyName("floatingWindowAppearanceStyle")] + public int FloatingWindowAppearanceStyle + { + get => _floatingWindowAppearanceStyle; + set + { + var normalized = value == 1 ? 1 : 0; + if (normalized == _floatingWindowAppearanceStyle) return; + _floatingWindowAppearanceStyle = normalized; + OnPropertyChanged(); + } + } + + private LiquidGlassSettings _floatingWindowLiquidGlass = CreateFloatingWindowLiquidGlassDefaults(); + + [JsonPropertyName("floatingWindowLiquidGlass")] + public LiquidGlassSettings FloatingWindowLiquidGlass + { + get => _floatingWindowLiquidGlass; + set + { + value ??= CreateFloatingWindowLiquidGlassDefaults(); + if (ReferenceEquals(value, _floatingWindowLiquidGlass)) return; + _floatingWindowLiquidGlass.PropertyChanged -= OnFloatingWindowLiquidGlassSettingsPropertyChanged; + _floatingWindowLiquidGlass = value; + _floatingWindowLiquidGlass.PropertyChanged += OnFloatingWindowLiquidGlassSettingsPropertyChanged; + OnPropertyChanged(); + } + } + + private double _floatingWindowGlassButtonScaleDip = 3.5; + + [JsonPropertyName("floatingWindowGlassButtonScaleDip")] + public double FloatingWindowGlassButtonScaleDip + { + get => _floatingWindowGlassButtonScaleDip; + set + { + var clamped = Math.Clamp(value, 0, 12); + if (Math.Abs(clamped - _floatingWindowGlassButtonScaleDip) < 0.0001) return; + _floatingWindowGlassButtonScaleDip = clamped; + OnPropertyChanged(); + } + } + + private void OnLiquidGlassSettingsPropertyChanged(object? sender, PropertyChangedEventArgs e) => + OnPropertyChanged(nameof(AiConversationLiquidGlass)); + + private void OnApprovalButtonGlassSettingsPropertyChanged(object? sender, PropertyChangedEventArgs e) => + OnPropertyChanged(nameof(AiConversationApprovalButtonGlass)); + + private void OnFloatingWindowLiquidGlassSettingsPropertyChanged(object? sender, PropertyChangedEventArgs e) => + OnPropertyChanged(nameof(FloatingWindowLiquidGlass)); + + private static LiquidGlassSettings CreateFloatingWindowLiquidGlassDefaults() + { + return new LiquidGlassSettings + { + CornerRadius = 20, + BackdropRefreshIntervalMs = 50, + RefractionHeight = 10, + RefractionAmount = 20, + BlurRadius = 4, + Vibrancy = 1.25, + BackdropOpacity = 0.96, + HighlightEnabled = true, + HighlightWidth = 0.5, + HighlightBlurRadius = 0.3, + HighlightOpacity = 0.65, + ShadowEnabled = true, + ShadowRadius = 20, + ShadowOffsetY = 4, + ShadowColor = "#40000000", + ShadowOpacity = 0.85 + }; + } } diff --git a/Controls/ActionFlowExecutionConfirmationSettingsControl.cs b/Controls/ActionFlowExecutionConfirmationSettingsControl.cs new file mode 100644 index 00000000..691767a1 --- /dev/null +++ b/Controls/ActionFlowExecutionConfirmationSettingsControl.cs @@ -0,0 +1,45 @@ +using Avalonia.Controls; +using Avalonia.Data; +using ClassIsland.Core.Abstractions.Controls; +using SystemTools.Settings; + +namespace SystemTools.Controls; + +public class ActionFlowExecutionConfirmationSettingsControl + : ActionSettingsControlBase +{ + private readonly TextBox _promptNameTextBox; + + public ActionFlowExecutionConfirmationSettingsControl() + { + var panel = new StackPanel + { + Spacing = 10, + Margin = new(10) + }; + + panel.Children.Add(new TextBlock + { + Text = "提示名称:" + }); + + _promptNameTextBox = new TextBox + { + PlaceholderText = "请输入将在确认窗口中显示的自动化名称", + AcceptsReturn = false + }; + panel.Children.Add(_promptNameTextBox); + + Content = panel; + } + + protected override void OnInitialized() + { + base.OnInitialized(); + _promptNameTextBox.Bind(TextBox.TextProperty, new Binding(nameof(Settings.PromptName)) + { + Source = Settings, + Mode = BindingMode.TwoWay + }); + } +} diff --git a/Controls/AdjustScreenBrightnessSettingsControl.cs b/Controls/AdjustScreenBrightnessSettingsControl.cs index 84d99376..539ececf 100644 --- a/Controls/AdjustScreenBrightnessSettingsControl.cs +++ b/Controls/AdjustScreenBrightnessSettingsControl.cs @@ -8,6 +8,7 @@ namespace SystemTools.Controls; public class AdjustScreenBrightnessSettingsControl : ActionSettingsControlBase { private NumericUpDown _brightnessInput; + private CheckBox _notifyCheckBox; public AdjustScreenBrightnessSettingsControl() { @@ -25,7 +26,7 @@ public AdjustScreenBrightnessSettingsControl() Maximum = 100, Increment = 1, FormatString = "0", - Watermark = "输入 0-100 的整数" + PlaceholderText = "输入 0-100 的整数" }; panel.Children.Add(_brightnessInput); @@ -44,7 +45,11 @@ public AdjustScreenBrightnessSettingsControl() Foreground = Avalonia.Media.Brushes.Gray, FontSize = 11, Margin = new(0, 10, 0, 0) - }); + }); _notifyCheckBox = new CheckBox { Content = "当执行时发出提醒" }; + _notifyCheckBox.IsCheckedChanged += (s, e) => { Settings.NotifyOnExecute = _notifyCheckBox.IsChecked ?? false; }; + panel.Children.Add(_notifyCheckBox); + + Content = panel; } @@ -52,11 +57,12 @@ public AdjustScreenBrightnessSettingsControl() protected override void OnInitialized() { base.OnInitialized(); + _notifyCheckBox.IsChecked = Settings.NotifyOnExecute; - _brightnessInput[!NumericUpDown.ValueProperty] = new Binding(nameof(Settings.BrightnessPercent)) + _brightnessInput.Bind(NumericUpDown.ValueProperty, new Binding(nameof(Settings.BrightnessPercent)) { Source = Settings, Mode = BindingMode.TwoWay - }; + }); } -} \ No newline at end of file +} diff --git a/Controls/AiAttachmentDropConfirmation.axaml b/Controls/AiAttachmentDropConfirmation.axaml new file mode 100644 index 00000000..66f5f5c8 --- /dev/null +++ b/Controls/AiAttachmentDropConfirmation.axaml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Controls/AiAttachmentDropConfirmation.axaml.cs b/Controls/AiAttachmentDropConfirmation.axaml.cs new file mode 100644 index 00000000..83ddb68f --- /dev/null +++ b/Controls/AiAttachmentDropConfirmation.axaml.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using Avalonia.Controls; +using SystemTools.Models; +using SystemTools.Services; + +namespace SystemTools.Controls; + +public partial class AiAttachmentDropConfirmation : UserControl +{ + public AiAttachmentDropConfirmation() + : this(new AiAttachmentLoadResult([], [])) + { + } + + public AiAttachmentDropConfirmation(AiAttachmentLoadResult result) + { + Accepted = result.Accepted; + Rejected = result.Rejected; + Summary = Accepted.Count == 1 + ? "将把以下附件添加到当前消息。" + : $"将把以下 {Accepted.Count} 个附件添加到当前消息。"; + InitializeComponent(); + DataContext = this; + RejectedItemsWarning.IsVisible = Rejected.Count > 0; + } + + public IReadOnlyList Accepted { get; } + + public IReadOnlyList Rejected { get; } + + public string Summary { get; } +} diff --git a/Controls/AiAttachmentDropOverlay.axaml b/Controls/AiAttachmentDropOverlay.axaml new file mode 100644 index 00000000..571fb265 --- /dev/null +++ b/Controls/AiAttachmentDropOverlay.axaml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + diff --git a/Controls/AiAttachmentDropOverlay.axaml.cs b/Controls/AiAttachmentDropOverlay.axaml.cs new file mode 100644 index 00000000..5f8e7c46 --- /dev/null +++ b/Controls/AiAttachmentDropOverlay.axaml.cs @@ -0,0 +1,54 @@ +using Avalonia.Controls; + +namespace SystemTools.Controls; + +public partial class AiAttachmentDropOverlay : UserControl +{ + public AiAttachmentDropOverlay() + { + InitializeComponent(); + } + + public bool ShowForFiles(int fileCount, int availableSlots, bool canModifyAttachments) + { + if (fileCount <= 0) + { + Hide(); + return false; + } + + IsVisible = true; + var isValid = canModifyAttachments && availableSlots > 0; + ValidIcon.IsVisible = isValid; + InvalidIcon.IsVisible = !isValid; + + if (!canModifyAttachments) + { + HintText.Text = "当前无法添加附件"; + SubHintText.Text = "请等待当前附件处理或 AI 回复完成"; + return false; + } + + if (availableSlots <= 0) + { + HintText.Text = "附件数量已达上限"; + SubHintText.Text = $"每条消息最多添加 {Services.AiAttachmentService.MaximumAttachmentCount} 个附件"; + return false; + } + + HintText.Text = fileCount == 1 + ? "松开以检查 1 个附件" + : $"松开以检查 {fileCount} 个附件"; + SubHintText.Text = fileCount > availableSlots + ? $"最多还可添加 {availableSlots} 个,其余文件会在确认时列出" + : "松开后可预览并确认上传"; + return true; + } + + public void Hide() + { + IsVisible = false; + ValidIcon.IsVisible = false; + InvalidIcon.IsVisible = false; + } +} diff --git a/Controls/AiChatGlassSurface.cs b/Controls/AiChatGlassSurface.cs new file mode 100644 index 00000000..42b75ad9 --- /dev/null +++ b/Controls/AiChatGlassSurface.cs @@ -0,0 +1,137 @@ +using System; +using Avalonia; +using Avalonia.Media; +using Avalonia.Threading; +using Avalonia.VisualTree; +using SystemTools.ConfigHandlers; + +namespace SystemTools.Controls; + +/// +/// A non-interactive liquid-glass surface that follows the AI conversation's +/// shared material settings. Keeping the copy in one control also makes the +/// repeated message templates cheap to configure. +/// +public sealed class AiChatGlassSurface : LiquidGlassAvaloniaUI.LiquidGlassSurface +{ + public static readonly StyledProperty SettingsProperty = + AvaloniaProperty.Register(nameof(Settings)); + + private LiquidGlassSettings? _observedSettings; + + public LiquidGlassSettings? Settings + { + get => GetValue(SettingsProperty); + set => SetValue(SettingsProperty, value); + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + SubscribeToSettings(); + ApplySettings(); + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + UnsubscribeFromSettings(); + base.OnDetachedFromVisualTree(e); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + if (change.Property == SettingsProperty) + { + SubscribeToSettings(); + ApplySettings(); + } + } + + private void SubscribeToSettings() + { + if (ReferenceEquals(_observedSettings, Settings)) + { + return; + } + + UnsubscribeFromSettings(); + _observedSettings = Settings; + if (_observedSettings is not null) + { + _observedSettings.PropertyChanged += OnSettingsPropertyChanged; + } + } + + private void UnsubscribeFromSettings() + { + if (_observedSettings is not null) + { + _observedSettings.PropertyChanged -= OnSettingsPropertyChanged; + _observedSettings = null; + } + } + + private void OnSettingsPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) + { + if (Dispatcher.UIThread.CheckAccess()) + { + ApplySettings(); + return; + } + + Dispatcher.UIThread.Post(ApplySettings); + } + + private void ApplySettings() + { + if (Settings is not { } settings) + { + return; + } + + BackdropZoom = settings.BackdropZoom; + BackdropOffset = new Vector(settings.BackdropOffsetX, settings.BackdropOffsetY); + RefractionHeight = settings.RefractionHeight; + RefractionAmount = settings.RefractionAmount; + DepthEffect = settings.DepthEffect; + ChromaticAberration = settings.ChromaticAberration; + BlurRadius = settings.BlurRadius; + Vibrancy = settings.Vibrancy; + Brightness = settings.Brightness; + Contrast = settings.Contrast; + ExposureEv = settings.ExposureEv; + GammaPower = settings.GammaPower; + BackdropOpacity = settings.BackdropOpacity; + TintColor = ParseColor(settings.TintColor, Colors.Transparent); + // SurfaceColor is intentionally owned by the theme resource in XAML. + // It stays faintly tinted in both modes while the backdrop remains visible. + ProgressiveBlurEnabled = settings.ProgressiveBlurEnabled; + ProgressiveBlurStart = settings.ProgressiveBlurStart; + ProgressiveBlurEnd = settings.ProgressiveBlurEnd; + ProgressiveTintColor = ParseColor(settings.ProgressiveTintColor, Colors.Transparent); + ProgressiveTintIntensity = settings.ProgressiveTintIntensity; + AdaptiveLuminanceEnabled = settings.AdaptiveLuminanceEnabled; + AdaptiveLuminanceUpdateIntervalMs = settings.AdaptiveLuminanceUpdateIntervalMs; + AdaptiveLuminanceSmoothing = settings.AdaptiveLuminanceSmoothing; + HighlightEnabled = settings.HighlightEnabled; + HighlightWidth = settings.HighlightWidth; + HighlightBlurRadius = settings.HighlightBlurRadius; + HighlightOpacity = settings.HighlightOpacity; + HighlightAngle = settings.HighlightAngle; + HighlightFalloff = settings.HighlightFalloff; + ShadowEnabled = settings.ShadowEnabled; + ShadowRadius = Math.Min(settings.ShadowRadius, 14); + ShadowOffset = new Vector(0, 2); + ShadowColor = ParseColor(settings.ShadowColor, Color.FromArgb(26, 0, 0, 0)); + ShadowOpacity = Math.Min(settings.ShadowOpacity, 0.55); + InnerShadowEnabled = settings.InnerShadowEnabled; + InnerShadowRadius = Math.Min(settings.InnerShadowRadius, 24); + InnerShadowOffset = new Vector(settings.InnerShadowOffsetX, settings.InnerShadowOffsetY); + InnerShadowColor = ParseColor(settings.InnerShadowColor, Color.FromArgb(38, 0, 0, 0)); + InnerShadowOpacity = settings.InnerShadowOpacity; + } + + private static Color ParseColor(string? value, Color fallback) => + Color.TryParse(value, out var color) ? color : fallback; +} diff --git a/Controls/AutoHideMainWindowWhenOccludedActionSettingsControl.cs b/Controls/AutoHideMainWindowWhenOccludedActionSettingsControl.cs new file mode 100644 index 00000000..eb1d1b3f --- /dev/null +++ b/Controls/AutoHideMainWindowWhenOccludedActionSettingsControl.cs @@ -0,0 +1,52 @@ +using Avalonia.Controls; +using ClassIsland.Core.Abstractions.Controls; +using SystemTools.Settings; + +namespace SystemTools.Controls; + +public class AutoHideMainWindowWhenOccludedActionSettingsControl : ActionSettingsControlBase +{ + private ToggleSwitch _enableToggle; + private CheckBox _notifyCheckBox; + + public AutoHideMainWindowWhenOccludedActionSettingsControl() + { + var panel = new StackPanel { Spacing = 10, Margin = new(10) }; + + panel.Children.Add(new TextBlock + { + Text = "遮挡文字时隐藏主界面", + FontWeight = Avalonia.Media.FontWeight.Bold, + FontSize = 14 + }); + + _enableToggle = new ToggleSwitch + { + OnContent = "开启", + OffContent = "关闭" + }; + _enableToggle.IsCheckedChanged += (s, e) => { Settings.Enable = _enableToggle.IsChecked ?? false; }; + panel.Children.Add(_enableToggle); + + panel.Children.Add(new TextBlock + { + Text = "设为“开启”时,触发行动将开启该功能;设为“关闭”时,触发行动将关闭该功能。", + TextWrapping = Avalonia.Media.TextWrapping.Wrap, + Opacity = 0.7, + FontSize = 12 + }); + + _notifyCheckBox = new CheckBox { Content = "当执行时发出提醒" }; + _notifyCheckBox.IsCheckedChanged += (s, e) => { Settings.NotifyOnExecute = _notifyCheckBox.IsChecked ?? false; }; + panel.Children.Add(_notifyCheckBox); + + Content = panel; + } + + protected override void OnInitialized() + { + base.OnInitialized(); + _enableToggle.IsChecked = Settings.Enable; + _notifyCheckBox.IsChecked = Settings.NotifyOnExecute; + } +} \ No newline at end of file diff --git a/Controls/AutoOpenUsbDriveOnInsertActionSettingsControl.cs b/Controls/AutoOpenUsbDriveOnInsertActionSettingsControl.cs new file mode 100644 index 00000000..c7e2b3bb --- /dev/null +++ b/Controls/AutoOpenUsbDriveOnInsertActionSettingsControl.cs @@ -0,0 +1,52 @@ +using Avalonia.Controls; +using ClassIsland.Core.Abstractions.Controls; +using SystemTools.Settings; + +namespace SystemTools.Controls; + +public class AutoOpenUsbDriveOnInsertActionSettingsControl : ActionSettingsControlBase +{ + private ToggleSwitch _enableToggle; + private CheckBox _notifyCheckBox; + + public AutoOpenUsbDriveOnInsertActionSettingsControl() + { + var panel = new StackPanel { Spacing = 10, Margin = new(10) }; + + panel.Children.Add(new TextBlock + { + Text = "自动播放", + FontWeight = Avalonia.Media.FontWeight.Bold, + FontSize = 14 + }); + + _enableToggle = new ToggleSwitch + { + OnContent = "开启", + OffContent = "关闭" + }; + _enableToggle.IsCheckedChanged += (s, e) => { Settings.Enable = _enableToggle.IsChecked ?? false; }; + panel.Children.Add(_enableToggle); + + panel.Children.Add(new TextBlock + { + Text = "设为“开启”时,触发行动将开启该功能;设为“关闭”时,触发行动将关闭该功能。", + TextWrapping = Avalonia.Media.TextWrapping.Wrap, + Opacity = 0.7, + FontSize = 12 + }); + + _notifyCheckBox = new CheckBox { Content = "当执行时发出提醒" }; + _notifyCheckBox.IsCheckedChanged += (s, e) => { Settings.NotifyOnExecute = _notifyCheckBox.IsChecked ?? false; }; + panel.Children.Add(_notifyCheckBox); + + Content = panel; + } + + protected override void OnInitialized() + { + base.OnInitialized(); + _enableToggle.IsChecked = Settings.Enable; + _notifyCheckBox.IsChecked = Settings.NotifyOnExecute; + } +} \ No newline at end of file diff --git a/Controls/AutoSwitchClassIslandThemeActionSettingsControl.cs b/Controls/AutoSwitchClassIslandThemeActionSettingsControl.cs new file mode 100644 index 00000000..fb6a0601 --- /dev/null +++ b/Controls/AutoSwitchClassIslandThemeActionSettingsControl.cs @@ -0,0 +1,52 @@ +using Avalonia.Controls; +using ClassIsland.Core.Abstractions.Controls; +using SystemTools.Settings; + +namespace SystemTools.Controls; + +public class AutoSwitchClassIslandThemeActionSettingsControl : ActionSettingsControlBase +{ + private ToggleSwitch _enableToggle; + private CheckBox _notifyCheckBox; + + public AutoSwitchClassIslandThemeActionSettingsControl() + { + var panel = new StackPanel { Spacing = 10, Margin = new(10) }; + + panel.Children.Add(new TextBlock + { + Text = "自动切换 ClassIsland 主题", + FontWeight = Avalonia.Media.FontWeight.Bold, + FontSize = 14 + }); + + _enableToggle = new ToggleSwitch + { + OnContent = "开启", + OffContent = "关闭" + }; + _enableToggle.IsCheckedChanged += (s, e) => { Settings.Enable = _enableToggle.IsChecked ?? false; }; + panel.Children.Add(_enableToggle); + + panel.Children.Add(new TextBlock + { + Text = "设为“开启”时,触发行动将开启该功能;设为“关闭”时,触发行动将关闭该功能。", + TextWrapping = Avalonia.Media.TextWrapping.Wrap, + Opacity = 0.7, + FontSize = 12 + }); + + _notifyCheckBox = new CheckBox { Content = "当执行时发出提醒" }; + _notifyCheckBox.IsCheckedChanged += (s, e) => { Settings.NotifyOnExecute = _notifyCheckBox.IsChecked ?? false; }; + panel.Children.Add(_notifyCheckBox); + + Content = panel; + } + + protected override void OnInitialized() + { + base.OnInitialized(); + _enableToggle.IsChecked = Settings.Enable; + _notifyCheckBox.IsChecked = Settings.NotifyOnExecute; + } +} \ No newline at end of file diff --git a/Controls/BackgroundPlayAudioSettingsControl.cs b/Controls/BackgroundPlayAudioSettingsControl.cs index 3d16cba4..256878db 100644 --- a/Controls/BackgroundPlayAudioSettingsControl.cs +++ b/Controls/BackgroundPlayAudioSettingsControl.cs @@ -11,6 +11,7 @@ namespace SystemTools.Controls; public class BackgroundPlayAudioSettingsControl : ActionSettingsControlBase { + private readonly CheckBox _notifyCheckBox; private readonly TextBox _audioPathBox; private readonly CheckBox _waitForCompletedCheckBox; private readonly TextBlock _validationHintTextBlock; @@ -34,7 +35,7 @@ public BackgroundPlayAudioSettingsControl() _audioPathBox = new TextBox { - Watermark = "点击“浏览...”选择音频文件", + PlaceholderText = "点击“浏览...”选择音频文件", Width = 320, IsReadOnly = true }; @@ -58,6 +59,8 @@ public BackgroundPlayAudioSettingsControl() }; panel.Children.Add(_validationHintTextBlock); + _notifyCheckBox = new CheckBox { Content = "当执行时发出提醒" }; + _waitForCompletedCheckBox = new CheckBox { Content = "播放后等待播放完成", @@ -66,15 +69,25 @@ public BackgroundPlayAudioSettingsControl() _waitForCompletedCheckBox.IsCheckedChanged += (_, _) => { Settings.WaitForPlaybackCompleted = _waitForCompletedCheckBox.IsChecked == true; + _notifyCheckBox.IsEnabled = _waitForCompletedCheckBox.IsChecked != true; + if (!_notifyCheckBox.IsEnabled) + _notifyCheckBox.IsChecked = false; }; panel.Children.Add(_waitForCompletedCheckBox); + _notifyCheckBox.IsCheckedChanged += (s, e) => { Settings.NotifyOnExecute = _notifyCheckBox.IsChecked ?? false; }; + panel.Children.Add(_notifyCheckBox); + Content = panel; } protected override void OnInitialized() { base.OnInitialized(); + _notifyCheckBox.IsChecked = Settings.NotifyOnExecute; + _notifyCheckBox.IsEnabled = !Settings.WaitForPlaybackCompleted; + if (Settings.WaitForPlaybackCompleted) + _notifyCheckBox.IsChecked = false; _audioPathBox.Text = Settings.AudioFilePath; _waitForCompletedCheckBox.IsChecked = Settings.WaitForPlaybackCompleted; } diff --git a/Controls/CameraCaptureSettingsControl.cs b/Controls/CameraCaptureSettingsControl.cs index acf5145b..b9b205bb 100644 --- a/Controls/CameraCaptureSettingsControl.cs +++ b/Controls/CameraCaptureSettingsControl.cs @@ -26,7 +26,7 @@ public CameraCaptureSettingsControl() _deviceNameBox = new TextBox { - Watermark = "输入摄像头名(在系统 设备管理器 中查询)" + PlaceholderText = "输入摄像头名(在系统 设备管理器 中查询)" }; panel.Children.Add(_deviceNameBox); @@ -38,7 +38,7 @@ public CameraCaptureSettingsControl() _folderPathBox = new TextBox { - Watermark = "点击\"浏览...\"以选择保存文件夹", + PlaceholderText = "点击\"浏览...\"以选择保存文件夹", IsReadOnly = true }; panel.Children.Add(_folderPathBox); @@ -109,4 +109,4 @@ private async Task BrowseFolder_Click() logger?.LogError(ex, "选择保存文件夹失败"); } } -} \ No newline at end of file +} diff --git a/Controls/Components/BetterCarouselContainerSettingsControl.axaml b/Controls/Components/BetterCarouselContainerSettingsControl.axaml index d709b3c5..31e688eb 100644 --- a/Controls/Components/BetterCarouselContainerSettingsControl.axaml +++ b/Controls/Components/BetterCarouselContainerSettingsControl.axaml @@ -1,4 +1,4 @@ - - - + @@ -34,21 +34,21 @@ - - + + - - + - - + - + @@ -58,14 +58,14 @@ - - - + + + - - + - - + + - - + - - + + - - + - - + + - @@ -134,7 +134,7 @@ - +