Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project>

<PropertyGroup>
<Version>0.1.2</Version>
<Version>0.1.3</Version>
<Authors>Justin Paul</Authors>
<Company>Justin Paul</Company>
<Product>Webhook Server</Product>
Expand Down
24 changes: 19 additions & 5 deletions src/WebhookServer.Gui/MainWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@
<MenuItem Header="_Export config…" Command="{Binding ExportConfigCommand}"/>
<MenuItem Header="Config _Checkpoints…" Command="{Binding ShowConfigCheckpointsCommand}"/>
<Separator/>
<MenuItem Header="_Minimize to tray"
IsCheckable="True"
IsChecked="{Binding MinimizeToTrayEnabled, Mode=TwoWay}"
ToolTip="When ticked, closing or minimizing the window hides it to the tray and keeps the GUI process alive. Untick to make the X button quit the app."/>
<Separator/>
<MenuItem Header="E_xit" Command="{Binding ExitCommand}"/>
</MenuItem>
<MenuItem Header="_Server">
Expand Down Expand Up @@ -63,17 +68,26 @@
<DataGrid.RowStyle>
<Style TargetType="DataGridRow">
<EventSetter Event="MouseDoubleClick" Handler="OnRowDoubleClick"/>
<!-- The ContextMenu lives in its own visual tree (a popup), so
AncestorType=Window doesn't resolve from inside menu items.
Stash MainViewModel on the row's Tag here (still in the
Window's tree), then reach it from the menu via
PlacementTarget.Tag. -->
<Setter Property="Tag" Value="{Binding DataContext, RelativeSource={RelativeSource AncestorType=Window}}"/>
<Setter Property="ContextMenu">
<Setter.Value>
<ContextMenu>
<MenuItem Header="_Edit…" Command="{Binding DataContext.EditEndpointCommand, RelativeSource={RelativeSource AncestorType=Window}}"/>
<MenuItem Header="_Copy URL" Command="{Binding DataContext.CopyEndpointUrlCommand, RelativeSource={RelativeSource AncestorType=Window}}"/>
<MenuItem Header="_Edit…"
Command="{Binding PlacementTarget.Tag.EditEndpointCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}"/>
<MenuItem Header="_Copy URL"
Command="{Binding PlacementTarget.Tag.CopyEndpointUrlCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}"/>
<Separator/>
<MenuItem Header="Toggle _enabled"
Command="{Binding DataContext.ToggleEnabledCommand, RelativeSource={RelativeSource AncestorType=Window}}"
CommandParameter="{Binding}"/>
Command="{Binding PlacementTarget.Tag.ToggleEnabledCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}"
CommandParameter="{Binding PlacementTarget.DataContext, RelativeSource={RelativeSource AncestorType=ContextMenu}}"/>
<Separator/>
<MenuItem Header="_Delete…" Command="{Binding DataContext.DeleteEndpointCommand, RelativeSource={RelativeSource AncestorType=Window}}"/>
<MenuItem Header="_Delete…"
Command="{Binding PlacementTarget.Tag.DeleteEndpointCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}"/>
</ContextMenu>
</Setter.Value>
</Setter>
Expand Down
41 changes: 36 additions & 5 deletions src/WebhookServer.Gui/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
Expand All @@ -11,26 +12,56 @@ public partial class MainWindow : Window
private readonly TrayIcon _tray;
private readonly MainViewModel _vm;

/// <summary>
/// Set to true when the user has explicitly asked to quit (File -> Exit or
/// Tray -> Exit). The OnClosing handler reads this to decide whether to
/// actually let the window close or hide it to the tray.
/// </summary>
public bool ExitForReal { get; set; }

public MainWindow()
{
InitializeComponent();
_vm = new MainViewModel(new AdminPipeClient());
DataContext = _vm;
_vm.RealExitRequested += OnRealExitRequested;

_tray = new TrayIcon(
resolveMainWindow: () => Application.Current.MainWindow,
restartServiceAsync: async () => await new AdminPipeClient().RestartListenerAsync());
restartServiceAsync: async () => await new AdminPipeClient().RestartListenerAsync(),
onExit: OnRealExitRequested);

Loaded += async (_, _) => await _vm.RefreshCommand.ExecuteAsync(null);
StateChanged += OnStateChanged;
Closed += (_, _) => _tray.Dispose();
Closing += OnClosing;
}

private void OnClosing(object? sender, CancelEventArgs e)
{
if (ExitForReal || !_vm.MinimizeToTrayEnabled)
{
_tray.Dispose();
return;
}
// Treat the X button / Alt+F4 like a minimize: hide to tray, keep the
// process alive so the tray icon persists.
e.Cancel = true;
Hide();
ShowInTaskbar = false;
}

private void OnRealExitRequested()
{
ExitForReal = true;
Application.Current.Shutdown();
}

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;
Expand Down
50 changes: 50 additions & 0 deletions src/WebhookServer.Gui/Services/GuiSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using System.IO;
using System.Text.Json;

namespace WebhookServer.Gui.Services;

/// <summary>
/// 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.
/// </summary>
public sealed class GuiSettings
{
/// <summary>
/// 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.
/// </summary>
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<GuiSettings>(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 */ }
}
}
6 changes: 4 additions & 2 deletions src/WebhookServer.Gui/Services/TrayIcon.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@ public sealed class TrayIcon : IDisposable
private readonly NotifyIcon _icon;
private readonly Func<Window?> _resolveMainWindow;
private readonly Func<Task> _restartServiceAsync;
private readonly Action _onExit;

public TrayIcon(Func<Window?> resolveMainWindow, Func<Task> restartServiceAsync)
public TrayIcon(Func<Window?> resolveMainWindow, Func<Task> restartServiceAsync, Action onExit)
{
_resolveMainWindow = resolveMainWindow;
_restartServiceAsync = restartServiceAsync;
_onExit = onExit;

_icon = new NotifyIcon
{
Expand All @@ -39,7 +41,7 @@ private ContextMenuStrip BuildMenu()
menu.Items.Add(new ToolStripSeparator());
menu.Items.Add("&Restart service", null, async (_, _) => await _restartServiceAsync().ConfigureAwait(false));
menu.Items.Add(new ToolStripSeparator());
menu.Items.Add("E&xit", null, (_, _) => Application.Current.Shutdown());
menu.Items.Add("E&xit", null, (_, _) => _onExit());
return menu;
}

Expand Down
17 changes: 16 additions & 1 deletion src/WebhookServer.Gui/ViewModels/MainViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down Expand Up @@ -286,10 +297,14 @@ private void OpenDocumentation()
}
}

/// <summary>Raised when the user picks File -> Exit. MainWindow flips its
/// ExitForReal flag and shuts down, bypassing the X-hides-to-tray logic.</summary>
public event Action? RealExitRequested;

[RelayCommand]
private void Exit()
{
Application.Current.Shutdown();
RealExitRequested?.Invoke();
}

[RelayCommand]
Expand Down
Loading