feat: add system tray UI with status icon, tooltip, and context menu

This commit is contained in:
2026-04-07 11:45:20 +01:00
parent a3a8f2e4be
commit f907f9e8f1

View File

@@ -0,0 +1,110 @@
// src/SpamGuard/Tray/TrayApplicationContext.cs
namespace SpamGuard.Tray;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using SpamGuard.Services;
public sealed class TrayApplicationContext : ApplicationContext
{
private readonly NotifyIcon _notifyIcon;
private readonly System.Windows.Forms.Timer _refreshTimer;
private readonly ActivityLog _activityLog;
private readonly InboxMonitorService _monitor;
private readonly IHost _host;
private ActivityLogForm? _logForm;
private readonly ToolStripMenuItem _pauseMenuItem;
public TrayApplicationContext(IHost host)
{
_host = host;
_activityLog = host.Services.GetRequiredService<ActivityLog>();
_monitor = host.Services.GetRequiredService<InboxMonitorService>();
_pauseMenuItem = new ToolStripMenuItem("Pause", null, OnPauseResume);
var contextMenu = new ContextMenuStrip();
contextMenu.Items.Add("View Activity Log", null, OnViewLog);
contextMenu.Items.Add(_pauseMenuItem);
contextMenu.Items.Add(new ToolStripSeparator());
contextMenu.Items.Add("Quit", null, OnQuit);
_notifyIcon = new NotifyIcon
{
Icon = IconGenerator.Green,
Text = "SpamGuard - Starting...",
Visible = true,
ContextMenuStrip = contextMenu
};
_notifyIcon.DoubleClick += OnViewLog;
_refreshTimer = new System.Windows.Forms.Timer { Interval = 5000 };
_refreshTimer.Tick += OnRefreshTick;
_refreshTimer.Start();
}
private void OnRefreshTick(object? sender, EventArgs e)
{
var checked_ = _activityLog.TodayChecked;
var spam = _activityLog.TodaySpam;
_notifyIcon.Text = $"SpamGuard - {checked_} checked, {spam} spam caught today";
// Update icon based on state
if (!_monitor.IsPaused)
{
var recent = _activityLog.GetRecent(1);
var hasRecentError = recent.Count > 0
&& recent[0].Verdict == Models.Verdict.Error
&& recent[0].Timestamp > DateTime.UtcNow.AddMinutes(-5);
_notifyIcon.Icon = hasRecentError ? IconGenerator.Red : IconGenerator.Green;
}
_logForm?.RefreshData();
}
private void OnViewLog(object? sender, EventArgs e)
{
if (_logForm == null || _logForm.IsDisposed)
{
_logForm = new ActivityLogForm(_activityLog);
}
_logForm.Show();
_logForm.BringToFront();
}
private void OnPauseResume(object? sender, EventArgs e)
{
if (_monitor.IsPaused)
{
_monitor.Resume();
_pauseMenuItem.Text = "Pause";
_notifyIcon.Icon = IconGenerator.Green;
}
else
{
_monitor.Pause();
_pauseMenuItem.Text = "Resume";
_notifyIcon.Icon = IconGenerator.Yellow;
}
}
private async void OnQuit(object? sender, EventArgs e)
{
_notifyIcon.Visible = false;
_refreshTimer.Stop();
await _host.StopAsync();
Application.Exit();
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
_refreshTimer.Dispose();
_notifyIcon.Dispose();
_logForm?.Dispose();
}
base.Dispose(disposing);
}
}