From c353845fada661c5a8fa3c1c3da4899019932011 Mon Sep 17 00:00:00 2001 From: home-PC Date: Tue, 19 May 2026 01:32:41 +0800 Subject: [PATCH] =?UTF-8?q?feat(app):=20=E5=88=9D=E5=A7=8B=E5=8C=96?= =?UTF-8?q?=E6=9C=AC=E5=9C=B0=E9=80=9A=E7=9F=A5=E6=A1=8C=E9=9D=A2=E5=BA=94?= =?UTF-8?q?=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 搭建 .NET 8 WPF 应用骨架,加入系统托盘、单实例启动与主控制面板。 实现本地 HTTP /notify 消息接入、频道严格匹配、免打扰、熔断限流与历史持久化。 补充弹窗样式配置、队列/推挤/替换展示、溢出处理、应用图标和项目文档。 Initial-Commit: true --- .gitignore | 5 + App.xaml | 7 + App.xaml.cs | 152 ++++++++++++++++++ AssemblyInfo.cs | 10 ++ MainWindow.xaml | 206 ++++++++++++++++++++++++ MainWindow.xaml.cs | 372 ++++++++++++++++++++++++++++++++++++++++++++ Models.cs | 119 ++++++++++++++ OmniNotify.csproj | 17 ++ PRD.md | 67 ++++++++ PopupCoordinator.cs | 278 +++++++++++++++++++++++++++++++++ PopupWindow.xaml | 30 ++++ PopupWindow.xaml.cs | 312 +++++++++++++++++++++++++++++++++++++ README.md | 36 +++++ Services.cs | 298 +++++++++++++++++++++++++++++++++++ app.ico | Bin 0 -> 370070 bytes 15 files changed, 1909 insertions(+) create mode 100644 .gitignore create mode 100644 App.xaml create mode 100644 App.xaml.cs create mode 100644 AssemblyInfo.cs create mode 100644 MainWindow.xaml create mode 100644 MainWindow.xaml.cs create mode 100644 Models.cs create mode 100644 OmniNotify.csproj create mode 100644 PRD.md create mode 100644 PopupCoordinator.cs create mode 100644 PopupWindow.xaml create mode 100644 PopupWindow.xaml.cs create mode 100644 README.md create mode 100644 Services.cs create mode 100644 app.ico diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7800585 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +bin/ +obj/ +.vs/ +*.user +*.suo diff --git a/App.xaml b/App.xaml new file mode 100644 index 0000000..9e95694 --- /dev/null +++ b/App.xaml @@ -0,0 +1,7 @@ + + + + diff --git a/App.xaml.cs b/App.xaml.cs new file mode 100644 index 0000000..e5262fe --- /dev/null +++ b/App.xaml.cs @@ -0,0 +1,152 @@ +using System.IO; +using System.Threading; +using System.Windows; +using Forms = System.Windows.Forms; + +namespace OmniNotify; + +public partial class App : System.Windows.Application +{ + private const string SingleInstanceMutexName = @"Local\OmniNotify.SingleInstance"; + private const string ActivateEventName = @"Local\OmniNotify.Activate"; + private Forms.NotifyIcon? _notifyIcon; + private AppStore? _store; + private LocalHttpServer? _server; + private Mutex? _singleInstanceMutex; + private bool _ownsSingleInstanceMutex; + private EventWaitHandle? _activateEvent; + private RegisteredWaitHandle? _activateWait; + + public AppState State { get; private set; } = new(); + public NotificationRouter Router { get; private set; } = null!; + public PopupCoordinator PopupCoordinator { get; private set; } = null!; + public string ListenUrl => _server?.Url ?? ""; + public string? ServerError => _server?.LastError; + + protected override void OnStartup(StartupEventArgs e) + { + _singleInstanceMutex = new Mutex(true, SingleInstanceMutexName, out var createdNew); + _ownsSingleInstanceMutex = createdNew; + _activateEvent = new EventWaitHandle(false, EventResetMode.AutoReset, ActivateEventName); + if (!createdNew) + { + _activateEvent.Set(); + Shutdown(); + return; + } + + RegisterActivationListener(); + base.OnStartup(e); + + _store = new AppStore(); + State = _store.Load(); + PopupCoordinator = new PopupCoordinator(); + Router = new NotificationRouter(State, _store, PopupCoordinator); + Router.StateChanged += UpdateTrayMenu; + + _server = new LocalHttpServer(Router); + _server.Start(State.Settings.LocalPort); + + CreateTrayIcon(); + ShowMainWindow(); + } + + public void SaveState() + { + _store?.Save(State); + UpdateTrayMenu(); + } + + public void ShowMainWindow() + { + if (MainWindow is null) + { + MainWindow = new MainWindow(); + } + + MainWindow.Show(); + MainWindow.WindowState = WindowState.Normal; + MainWindow.Activate(); + } + + private void RegisterActivationListener() + { + if (_activateEvent is null) + { + return; + } + + _activateWait = ThreadPool.RegisterWaitForSingleObject( + _activateEvent, + (_, _) => Dispatcher.Invoke(ShowMainWindow), + null, + Timeout.InfiniteTimeSpan, + false); + } + + private void CreateTrayIcon() + { + var iconPath = Path.Combine(AppContext.BaseDirectory, "app.ico"); + _notifyIcon = new Forms.NotifyIcon + { + Icon = new System.Drawing.Icon(iconPath), + Text = "Omni-Notify", + Visible = true + }; + _notifyIcon.MouseClick += (_, args) => + { + if (args.Button == Forms.MouseButtons.Left) + { + Dispatcher.Invoke(ShowMainWindow); + } + }; + UpdateTrayMenu(); + } + + private void UpdateTrayMenu() + { + if (_notifyIcon is null) + { + return; + } + + var menu = new Forms.ContextMenuStrip(); + menu.Items.Add("打开主控制面板", null, (_, _) => Dispatcher.Invoke(ShowMainWindow)); + menu.Items.Add("全局设置", null, (_, _) => Dispatcher.Invoke(() => + { + ShowMainWindow(); + if (MainWindow is MainWindow window) + { + window.FocusSettingsTab(); + } + })); + menu.Items.Add(State.Settings.DndEnabled ? "关闭免打扰" : "开启免打扰", null, (_, _) => Dispatcher.Invoke(() => + { + State.Settings.DndEnabled = !State.Settings.DndEnabled; + SaveState(); + })); + + var resetBreaker = menu.Items.Add("解除熔断", null, (_, _) => Dispatcher.Invoke(() => Router.ResetCircuitBreaker())); + resetBreaker.Enabled = State.Settings.CircuitBreakerOpen; + resetBreaker.ForeColor = State.Settings.CircuitBreakerOpen ? System.Drawing.Color.Firebrick : System.Drawing.SystemColors.ControlText; + + menu.Items.Add(new Forms.ToolStripSeparator()); + menu.Items.Add("退出软件", null, (_, _) => Dispatcher.Invoke(Shutdown)); + _notifyIcon.ContextMenuStrip = menu; + } + + protected override void OnExit(ExitEventArgs e) + { + _activateWait?.Unregister(null); + _activateEvent?.Dispose(); + if (_ownsSingleInstanceMutex) + { + _singleInstanceMutex?.ReleaseMutex(); + } + _singleInstanceMutex?.Dispose(); + _notifyIcon?.Dispose(); + _server?.Dispose(); + _store?.Save(State); + base.OnExit(e); + } +} diff --git a/AssemblyInfo.cs b/AssemblyInfo.cs new file mode 100644 index 0000000..cc29e7f --- /dev/null +++ b/AssemblyInfo.cs @@ -0,0 +1,10 @@ +using System.Windows; + +[assembly:ThemeInfo( + ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located + //(used if a resource is not found in the page, + // or application resource dictionaries) + ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located + //(used if a resource is not found in the page, + // app, or any theme specific resource dictionaries) +)] diff --git a/MainWindow.xaml b/MainWindow.xaml new file mode 100644 index 0000000..da49b20 --- /dev/null +++ b/MainWindow.xaml @@ -0,0 +1,206 @@ + + + + + + + + + + + + + +