feat(app): 初始化本地通知桌面应用
搭建 .NET 8 WPF 应用骨架,加入系统托盘、单实例启动与主控制面板。 实现本地 HTTP /notify 消息接入、频道严格匹配、免打扰、熔断限流与历史持久化。 补充弹窗样式配置、队列/推挤/替换展示、溢出处理、应用图标和项目文档。 Initial-Commit: true
This commit is contained in:
152
App.xaml.cs
Normal file
152
App.xaml.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user