From 1029b5e1fc40fffdf894266610aae12aaf66d01c Mon Sep 17 00:00:00 2001 From: Wang Haoyu Date: Mon, 30 Mar 2026 22:40:14 +0800 Subject: [PATCH 01/15] Add hidden more-options page, time-period rule, and action/theme fixes --- Actions/AdvancedShutdownAction.cs | 27 ++++ Actions/LoadTemporaryClassPlanAction.cs | 30 +++++ ConfigHandlers/MainConfigData.cs | 14 ++ .../Components/LocalQuoteComponent.axaml.cs | 2 +- .../LocalQuoteSettingsControl.axaml | 2 +- Controls/InTimePeriodRuleSettingsControl.cs | 85 ++++++++++++ Plugin.cs | 29 +++++ Rules/InTimePeriodRuleSettings.cs | 12 ++ Services/AdaptiveThemeSyncService.cs | 122 ++++++++++++++++++ .../MoreFeaturesOptionsSettingsPage.axaml | 27 ++++ .../MoreFeaturesOptionsSettingsPage.axaml.cs | 32 +++++ SettingsPage/SystemToolsSettingsPage.axaml | 12 +- SettingsPage/SystemToolsSettingsPage.axaml.cs | 7 + SettingsPage/SystemToolsSettingsViewModel.cs | 3 +- 14 files changed, 400 insertions(+), 4 deletions(-) create mode 100644 Controls/InTimePeriodRuleSettingsControl.cs create mode 100644 Rules/InTimePeriodRuleSettings.cs create mode 100644 Services/AdaptiveThemeSyncService.cs create mode 100644 SettingsPage/MoreFeaturesOptionsSettingsPage.axaml create mode 100644 SettingsPage/MoreFeaturesOptionsSettingsPage.axaml.cs diff --git a/Actions/AdvancedShutdownAction.cs b/Actions/AdvancedShutdownAction.cs index 1a3efca..bf016d7 100644 --- a/Actions/AdvancedShutdownAction.cs +++ b/Actions/AdvancedShutdownAction.cs @@ -35,6 +35,12 @@ public class AdvancedShutdownAction(ILogger logger) : Ac private static bool _allowMainDialogClose; private static bool _allowFloatingWindowClose; + public static void CancelPlanOnAppStopping() + { + StopCountdownProcess(); + TryAbortSystemShutdown(); + } + protected override async Task OnInvoke() { _logger.LogDebug("AdvancedShutdownAction OnInvoke 开始"); @@ -180,6 +186,26 @@ private static void StopCountdownProcess() } } + private static void TryAbortSystemShutdown() + { + try + { + var psi = new ProcessStartInfo + { + FileName = "shutdown", + Arguments = "/a", + UseShellExecute = false, + CreateNoWindow = true, + WindowStyle = ProcessWindowStyle.Hidden + }; + using var process = Process.Start(psi); + process?.WaitForExit(1000); + } + catch + { + } + } + private static int GetRemainingSeconds() { lock (StateLock) @@ -255,6 +281,7 @@ private void StopWatchdog() private void StopAllStates() { StopCountdownProcess(); + TryAbortSystemShutdown(); lock (StateLock) { diff --git a/Actions/LoadTemporaryClassPlanAction.cs b/Actions/LoadTemporaryClassPlanAction.cs index 473f9cb..d5fc783 100644 --- a/Actions/LoadTemporaryClassPlanAction.cs +++ b/Actions/LoadTemporaryClassPlanAction.cs @@ -3,6 +3,7 @@ using ClassIsland.Core.Attributes; using Microsoft.Extensions.Logging; using System; +using System.Collections.Concurrent; using System.Threading.Tasks; using SystemTools.Settings; @@ -18,6 +19,8 @@ public class LoadTemporaryClassPlanAction( private readonly IProfileService _profileService = profileService; private readonly IExactTimeService _exactTimeService = exactTimeService; + private static readonly ConcurrentDictionary PreviousSnapshots = new(); + protected override async Task OnInvoke() { if (!Guid.TryParse(Settings.ClassPlanId, out var classPlanId)) @@ -32,6 +35,13 @@ protected override async Task OnInvoke() return; } + if (IsRevertable) + { + PreviousSnapshots[ActionSet.Guid] = new TempClassPlanSnapshot( + _profileService.Profile.TempClassPlanId, + _profileService.Profile.TempClassPlanSetupTime); + } + _profileService.Profile.TempClassPlanId = classPlanId; _profileService.Profile.TempClassPlanSetupTime = _exactTimeService.GetCurrentLocalDateTime(); _profileService.SaveProfile(); @@ -39,4 +49,24 @@ protected override async Task OnInvoke() await base.OnInvoke(); } + + protected override async Task OnRevert() + { + await base.OnRevert(); + + if (PreviousSnapshots.TryRemove(ActionSet.Guid, out var snapshot)) + { + _profileService.Profile.TempClassPlanId = snapshot.TempClassPlanId; + _profileService.Profile.TempClassPlanSetupTime = snapshot.TempClassPlanSetupTime; + _profileService.SaveProfile(); + _logger.LogInformation("已恢复临时课表为触发前状态。ActionSet={ActionSetGuid}", ActionSet.Guid); + return; + } + + _profileService.Profile.TempClassPlanId = null; + _profileService.SaveProfile(); + _logger.LogInformation("未找到触发前状态,已清除临时课表。ActionSet={ActionSetGuid}", ActionSet.Guid); + } + + private readonly record struct TempClassPlanSnapshot(Guid? TempClassPlanId, DateTime TempClassPlanSetupTime); } diff --git a/ConfigHandlers/MainConfigData.cs b/ConfigHandlers/MainConfigData.cs index 3d2e454..8981084 100644 --- a/ConfigHandlers/MainConfigData.cs +++ b/ConfigHandlers/MainConfigData.cs @@ -86,6 +86,20 @@ public bool EnableFaceRecognition RestartPropertyChanged?.Invoke(this, EventArgs.Empty); } } + + bool _autoMatchMainBackgroundTheme; + + [JsonPropertyName("autoMatchMainBackgroundTheme")] + public bool AutoMatchMainBackgroundTheme + { + get => _autoMatchMainBackgroundTheme; + set + { + if (value == _autoMatchMainBackgroundTheme) return; + _autoMatchMainBackgroundTheme = value; + OnPropertyChanged(); + } + } // ========== 公告相关 ========== /*string _lastAcceptedAnnouncement = string.Empty; diff --git a/Controls/Components/LocalQuoteComponent.axaml.cs b/Controls/Components/LocalQuoteComponent.axaml.cs index 4156c75..c148483 100644 --- a/Controls/Components/LocalQuoteComponent.axaml.cs +++ b/Controls/Components/LocalQuoteComponent.axaml.cs @@ -164,7 +164,7 @@ private void OnCarouselTicked(object? sender, EventArgs e) private void RefreshTimerInterval() { - var interval = Math.Max(1, Settings.CarouselIntervalSeconds); + var interval = Math.Clamp(Settings.CarouselIntervalSeconds, 1, 8000); _carouselTimer.Interval = TimeSpan.FromSeconds(interval); } diff --git a/Controls/Components/LocalQuoteSettingsControl.axaml b/Controls/Components/LocalQuoteSettingsControl.axaml index 17ce7e2..a4c23ee 100644 --- a/Controls/Components/LocalQuoteSettingsControl.axaml +++ b/Controls/Components/LocalQuoteSettingsControl.axaml @@ -46,7 +46,7 @@ IconSource="{ci:FluentIconSource }"> diff --git a/Controls/InTimePeriodRuleSettingsControl.cs b/Controls/InTimePeriodRuleSettingsControl.cs new file mode 100644 index 0000000..81ab63b --- /dev/null +++ b/Controls/InTimePeriodRuleSettingsControl.cs @@ -0,0 +1,85 @@ +using Avalonia.Controls; +using Avalonia.Layout; +using ClassIsland.Core.Abstractions.Controls; +using System; +using SystemTools.Rules; + +namespace SystemTools.Controls; + +public class InTimePeriodRuleSettingsControl : RuleSettingsControlBase +{ + private readonly TextBox _startTextBox; + private readonly TextBox _endTextBox; + + public InTimePeriodRuleSettingsControl() + { + var panel = new StackPanel { Spacing = 10, Margin = new(10) }; + panel.Children.Add(new TextBlock + { + Text = "设置时间段(24 小时制,格式 HH:mm 或 HH:mm:ss):", + FontWeight = Avalonia.Media.FontWeight.Bold + }); + + var row = new Grid + { + ColumnDefinitions = new ColumnDefinitions("Auto,*,Auto,*,Auto"), + ColumnSpacing = 8, + VerticalAlignment = VerticalAlignment.Center + }; + + row.Children.Add(new TextBlock { Text = "起始", VerticalAlignment = VerticalAlignment.Center }); + _startTextBox = new TextBox { Watermark = "08:00", HorizontalAlignment = HorizontalAlignment.Stretch }; + Grid.SetColumn(_startTextBox, 1); + row.Children.Add(_startTextBox); + + var sep = new TextBlock { Text = "至", VerticalAlignment = VerticalAlignment.Center }; + Grid.SetColumn(sep, 2); + row.Children.Add(sep); + + _endTextBox = new TextBox { Watermark = "18:00", HorizontalAlignment = HorizontalAlignment.Stretch }; + Grid.SetColumn(_endTextBox, 3); + row.Children.Add(_endTextBox); + + var hint = new TextBlock + { + Text = "若起始晚于结束,将按跨天时间段处理(例如 22:00 - 06:00)。", + TextWrapping = Avalonia.Media.TextWrapping.Wrap, + Foreground = Avalonia.Media.Brushes.Gray + }; + + panel.Children.Add(row); + panel.Children.Add(hint); + Content = panel; + + _startTextBox.LostFocus += (_, _) => ApplyInput(_startTextBox, true); + _endTextBox.LostFocus += (_, _) => ApplyInput(_endTextBox, false); + } + + protected override void OnInitialized() + { + base.OnInitialized(); + _startTextBox.Text = Settings.StartTime; + _endTextBox.Text = Settings.EndTime; + } + + private void ApplyInput(TextBox box, bool isStart) + { + var text = box.Text?.Trim(); + if (!TimeSpan.TryParse(text, out var parsed)) + { + box.Text = isStart ? Settings.StartTime : Settings.EndTime; + return; + } + + var normalized = parsed.ToString(@"hh\:mm\:ss"); + box.Text = normalized; + if (isStart) + { + Settings.StartTime = normalized; + } + else + { + Settings.EndTime = normalized; + } + } +} diff --git a/Plugin.cs b/Plugin.cs index e88ec7a..c294af7 100644 --- a/Plugin.cs +++ b/Plugin.cs @@ -62,6 +62,7 @@ public override void Initialize(HostBuilderContext context, IServiceCollection s services.AddLogging(); services.AddSingleton(GlobalConstants.MainConfig); services.AddSingleton(); + services.AddSingleton(); // ========== 注册可选人脸识别 ========== if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) @@ -82,6 +83,7 @@ public override void Initialize(HostBuilderContext context, IServiceCollection s // ========== 注册设置页面 ========== services.AddSettingsPage(); + services.AddSettingsPage(); if (GlobalConstants.MainConfig.Data.EnableFloatingWindowFeature) { services.AddSettingsPage(); @@ -106,6 +108,7 @@ public override void Initialize(HostBuilderContext context, IServiceCollection s { IAppHost.GetService().Start(); } + IAppHost.GetService().Start(); _logger = IAppHost.GetService>(); _logger?.LogInformation("[SystemTools]实验性功能状态: {Status}", experimentalEnabled); @@ -316,6 +319,12 @@ private void RegisterBaseRules(IServiceCollection services) services.AddRule( "SystemTools.UsingTimeLayoutRule", "正在使用某时间表", "\uE823", HandleUsingTimeLayoutRule); } + + if (config.IsRuleEnabled("SystemTools.InTimePeriodRule")) + { + services.AddRule( + "SystemTools.InTimePeriodRule", "是否在某时间段", "\uE823", HandleInTimePeriodRule); + } } private void RegisterBaseComponents(IServiceCollection services) @@ -560,6 +569,24 @@ private static bool HandleUsingTimeLayoutRule(object? settings) return timeLayout.IsActivated; } + private static bool HandleInTimePeriodRule(object? settings) + { + if (settings is not InTimePeriodRuleSettings ruleSettings || + !TimeSpan.TryParse(ruleSettings.StartTime, out var start) || + !TimeSpan.TryParse(ruleSettings.EndTime, out var end)) + { + return false; + } + + var current = IAppHost.TryGetService()?.GetCurrentLocalDateTime().TimeOfDay ?? DateTime.Now.TimeOfDay; + if (start <= end) + { + return current >= start && current <= end; + } + + return current >= start || current <= end; + } + private void BuildSimulationMenu(MainConfigData config) { var items = new List(); @@ -775,6 +802,8 @@ private void RegisterSettingsPageGroup(IServiceCollection services) private void OnAppStopping(object? sender, EventArgs e) { + IAppHost.GetService().Stop(); + AdvancedShutdownAction.CancelPlanOnAppStopping(); if (GlobalConstants.MainConfig?.Data.EnableFloatingWindowFeature == true) { IAppHost.GetService().Stop(); diff --git a/Rules/InTimePeriodRuleSettings.cs b/Rules/InTimePeriodRuleSettings.cs new file mode 100644 index 0000000..947d4e3 --- /dev/null +++ b/Rules/InTimePeriodRuleSettings.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace SystemTools.Rules; + +public class InTimePeriodRuleSettings +{ + [JsonPropertyName("startTime")] + public string StartTime { get; set; } = "08:00:00"; + + [JsonPropertyName("endTime")] + public string EndTime { get; set; } = "18:00:00"; +} diff --git a/Services/AdaptiveThemeSyncService.cs b/Services/AdaptiveThemeSyncService.cs new file mode 100644 index 0000000..64af314 --- /dev/null +++ b/Services/AdaptiveThemeSyncService.cs @@ -0,0 +1,122 @@ +using Avalonia.Threading; +using ClassIsland.Core; +using ClassIsland.Core.Abstractions.Services; +using Microsoft.Extensions.Logging; +using System; +using System.Diagnostics; +using System.Drawing; +using System.Runtime.InteropServices; +using SystemTools.Shared; + +namespace SystemTools.Services; + +public class AdaptiveThemeSyncService(ILogger logger) +{ + private readonly ILogger _logger = logger; + private readonly DispatcherTimer _timer = new() { Interval = TimeSpan.FromSeconds(2) }; + private int? _lastAppliedTheme; + + public void Start() + { + _timer.Tick -= OnTick; + _timer.Tick += OnTick; + _timer.Start(); + } + + public void Stop() + { + _timer.Stop(); + } + + public void RefreshNow() + { + OnTick(this, EventArgs.Empty); + } + + private void OnTick(object? sender, EventArgs e) + { + if (GlobalConstants.MainConfig?.Data.AutoMatchMainBackgroundTheme != true) + { + return; + } + + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return; + } + + try + { + var targetTheme = DetectThemeByMainWindowBackground(); + if (targetTheme == null || targetTheme == _lastAppliedTheme) + { + return; + } + + var themeService = IAppHost.TryGetService(); + if (themeService == null) + { + return; + } + + themeService.SetTheme(targetTheme.Value, null); + _lastAppliedTheme = targetTheme; + _logger.LogDebug("已自动匹配主题为:{Theme}", targetTheme == 2 ? "黑暗" : "明亮"); + } + catch (Exception ex) + { + _logger.LogDebug(ex, "自动匹配主界面背景色失败,将在下次计时重试。"); + } + } + + private static int? DetectThemeByMainWindowBackground() + { + var handle = Process.GetCurrentProcess().MainWindowHandle; + if (handle == IntPtr.Zero) + { + return null; + } + + if (!GetWindowRect(handle, out var rect)) + { + return null; + } + + var width = Math.Max(1, rect.Right - rect.Left); + var height = Math.Max(1, rect.Bottom - rect.Top); + + using var bitmap = new Bitmap(width, height); + using var graphics = Graphics.FromImage(bitmap); + graphics.CopyFromScreen(rect.Left, rect.Top, 0, 0, new Size(width, height)); + + var samples = new (int X, int Y)[] + { + (width / 2, height / 2), + (Math.Max(0, width / 4), Math.Max(0, height / 4)), + (Math.Max(0, width * 3 / 4), Math.Max(0, height / 4)), + (Math.Max(0, width / 4), Math.Max(0, height * 3 / 4)), + (Math.Max(0, width * 3 / 4), Math.Max(0, height * 3 / 4)), + }; + + double luminance = 0; + foreach (var sample in samples) + { + var color = bitmap.GetPixel(Math.Clamp(sample.X, 0, width - 1), Math.Clamp(sample.Y, 0, height - 1)); + luminance += 0.299 * color.R + 0.587 * color.G + 0.114 * color.B; + } + luminance /= samples.Length; + + return luminance < 128 ? 2 : 1; // 2=黑暗,1=明亮 + } + + [DllImport("user32.dll")] + private static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect); + + private struct RECT + { + public int Left; + public int Top; + public int Right; + public int Bottom; + } +} diff --git a/SettingsPage/MoreFeaturesOptionsSettingsPage.axaml b/SettingsPage/MoreFeaturesOptionsSettingsPage.axaml new file mode 100644 index 0000000..03ba941 --- /dev/null +++ b/SettingsPage/MoreFeaturesOptionsSettingsPage.axaml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + diff --git a/SettingsPage/MoreFeaturesOptionsSettingsPage.axaml.cs b/SettingsPage/MoreFeaturesOptionsSettingsPage.axaml.cs new file mode 100644 index 0000000..265f545 --- /dev/null +++ b/SettingsPage/MoreFeaturesOptionsSettingsPage.axaml.cs @@ -0,0 +1,32 @@ +using Avalonia.Interactivity; +using ClassIsland.Core.Abstractions.Controls; +using ClassIsland.Core.Attributes; +using SystemTools.ConfigHandlers; +using SystemTools.Services; +using SystemTools.Shared; + +namespace SystemTools; + +[HidePageTitle] +[SettingsPageInfo("systemtools.settings.more", "更多功能选项", "\uE712", "\uE712", true)] +public partial class MoreFeaturesOptionsSettingsPage : SettingsPageBase +{ + public MainConfigData Config => GlobalConstants.MainConfig!.Data; + + public MoreFeaturesOptionsSettingsPage() + { + InitializeComponent(); + DataContext = this; + } + + private void AutoMatchThemeToggle_OnChanged(object? sender, RoutedEventArgs e) + { + var service = ClassIsland.Shared.IAppHost.GetService(); + if (Config.AutoMatchMainBackgroundTheme) + { + service.RefreshNow(); + } + + GlobalConstants.MainConfig?.Save(); + } +} diff --git a/SettingsPage/SystemToolsSettingsPage.axaml b/SettingsPage/SystemToolsSettingsPage.axaml index f329078..baec749 100644 --- a/SettingsPage/SystemToolsSettingsPage.axaml +++ b/SettingsPage/SystemToolsSettingsPage.axaml @@ -181,6 +181,16 @@ + + +