From 3cd8c94a947fcb5c26bf46cfa637c7e02149f2c1 Mon Sep 17 00:00:00 2001 From: Justin Paul Date: Fri, 8 May 2026 11:28:53 -0400 Subject: [PATCH] Add File -> Minimize to tray toggle (default on) Adds a checkable MenuItem so the user can opt out of the hide-to-tray behavior. Persisted per-user to %APPDATA%\WebhookServer\gui.json so the choice survives restarts. When ticked (default): X / Alt+F4 / minimize hide to tray, GUI process keeps running, tray icon persists. When unticked: X actually closes the app, minimize is a regular Windows minimize. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/WebhookServer.Gui/MainWindow.xaml | 5 ++ src/WebhookServer.Gui/MainWindow.xaml.cs | 9 ++-- src/WebhookServer.Gui/Services/GuiSettings.cs | 50 +++++++++++++++++++ .../ViewModels/MainViewModel.cs | 11 ++++ 4 files changed, 71 insertions(+), 4 deletions(-) create mode 100644 src/WebhookServer.Gui/Services/GuiSettings.cs diff --git a/src/WebhookServer.Gui/MainWindow.xaml b/src/WebhookServer.Gui/MainWindow.xaml index 466c38a..350d563 100644 --- a/src/WebhookServer.Gui/MainWindow.xaml +++ b/src/WebhookServer.Gui/MainWindow.xaml @@ -31,6 +31,11 @@ + + diff --git a/src/WebhookServer.Gui/MainWindow.xaml.cs b/src/WebhookServer.Gui/MainWindow.xaml.cs index 244e345..e140b88 100644 --- a/src/WebhookServer.Gui/MainWindow.xaml.cs +++ b/src/WebhookServer.Gui/MainWindow.xaml.cs @@ -38,7 +38,7 @@ public partial class MainWindow : Window private void OnClosing(object? sender, CancelEventArgs e) { - if (ExitForReal) + if (ExitForReal || !_vm.MinimizeToTrayEnabled) { _tray.Dispose(); return; @@ -58,9 +58,10 @@ public partial class MainWindow : Window private void OnStateChanged(object? sender, EventArgs e) { - // Minimize-to-tray: hide the window when the user minimizes; restoring is - // via the tray icon's double-click or context menu. - if (WindowState == WindowState.Minimized) + // Minimize-to-tray: hide the window when the user minimizes IF they've + // opted in via File -> Minimize to tray. Otherwise behave like a normal + // Windows minimize. + if (WindowState == WindowState.Minimized && _vm.MinimizeToTrayEnabled) { Hide(); ShowInTaskbar = false; diff --git a/src/WebhookServer.Gui/Services/GuiSettings.cs b/src/WebhookServer.Gui/Services/GuiSettings.cs new file mode 100644 index 0000000..fd511b6 --- /dev/null +++ b/src/WebhookServer.Gui/Services/GuiSettings.cs @@ -0,0 +1,50 @@ +using System.IO; +using System.Text.Json; + +namespace WebhookServer.Gui.Services; + +/// +/// Per-user GUI preferences that don't belong in the service-side ServerConfig. +/// Persisted to %APPDATA%\WebhookServer\gui.json. Best-effort: failures to read +/// or write fall back silently to defaults. +/// +public sealed class GuiSettings +{ + /// + /// When true, the X / Alt+F4 / minimize buttons hide the window to the tray + /// and keep the GUI process alive. When false, X exits the app and minimize + /// behaves like a normal Windows minimize. + /// + public bool MinimizeToTrayEnabled { get; set; } = true; + + private static string FilePath => Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "WebhookServer", + "gui.json"); + + public static GuiSettings Load() + { + try + { + if (File.Exists(FilePath)) + { + var json = File.ReadAllText(FilePath); + if (!string.IsNullOrWhiteSpace(json)) + return JsonSerializer.Deserialize(json) ?? new GuiSettings(); + } + } + catch { /* fall through to defaults */ } + return new GuiSettings(); + } + + public void Save() + { + try + { + var dir = Path.GetDirectoryName(FilePath); + if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir); + File.WriteAllText(FilePath, JsonSerializer.Serialize(this, new JsonSerializerOptions { WriteIndented = true })); + } + catch { /* best effort */ } + } +} diff --git a/src/WebhookServer.Gui/ViewModels/MainViewModel.cs b/src/WebhookServer.Gui/ViewModels/MainViewModel.cs index e2bed34..60d46ba 100644 --- a/src/WebhookServer.Gui/ViewModels/MainViewModel.cs +++ b/src/WebhookServer.Gui/ViewModels/MainViewModel.cs @@ -29,17 +29,28 @@ public sealed partial class MainViewModel : ObservableObject [ObservableProperty] private ServerConfig _serverConfig = new(); [ObservableProperty] private string _httpBaseUrl = "http://localhost:8080"; [ObservableProperty] private string? _httpsBaseUrl; + [ObservableProperty] private bool _minimizeToTrayEnabled; private readonly DispatcherTimer _logTimer; + private readonly GuiSettings _settings; public MainViewModel(AdminPipeClient client) { _client = client; + _settings = GuiSettings.Load(); + _minimizeToTrayEnabled = _settings.MinimizeToTrayEnabled; + _logTimer = new DispatcherTimer(DispatcherPriority.Background) { Interval = TimeSpan.FromSeconds(3) }; _logTimer.Tick += async (_, _) => await RefreshLogTailAsync(); _logTimer.Start(); } + partial void OnMinimizeToTrayEnabledChanged(bool value) + { + _settings.MinimizeToTrayEnabled = value; + _settings.Save(); + } + [RelayCommand] private async Task RefreshAsync() {