feat(app): 初始化本地通知桌面应用

搭建 .NET 8 WPF 应用骨架,加入系统托盘、单实例启动与主控制面板。
实现本地 HTTP /notify 消息接入、频道严格匹配、免打扰、熔断限流与历史持久化。
补充弹窗样式配置、队列/推挤/替换展示、溢出处理、应用图标和项目文档。

Initial-Commit: true
This commit is contained in:
2026-05-19 01:32:41 +08:00
commit c353845fad
15 changed files with 1909 additions and 0 deletions

152
App.xaml.cs Normal file
View File

@@ -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);
}
}