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
92 changes: 91 additions & 1 deletion Source/NETworkManager.Localization/Resources/Strings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 30 additions & 0 deletions Source/NETworkManager.Localization/Resources/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -2367,6 +2367,36 @@ $$hostname$$ --&gt; Hostname</value>
<data name="CouldNotResolveIPAddressFor" xml:space="preserve">
<value>Could not resolve ip address for: "{0}"</value>
</data>
<data name="PingMonitorNotification" xml:space="preserve">
<value>Notification</value>
</data>
<data name="ShowNotificationPopupOnStatusChange" xml:space="preserve">
<value>Show notification popup on status change</value>
</data>
<data name="PlaySoundOnStatusChange" xml:space="preserve">
<value>Play sound on status change</value>
</data>
<data name="NotificationSuccessThreshold" xml:space="preserve">
<value>Success threshold</value>
</data>
<data name="HelpMessage_NotificationSuccessThreshold" xml:space="preserve">
<value>Number of consecutive successful pings required before a "Host is up" notification is shown. Higher values reduce noise from flapping hosts.</value>
</data>
<data name="NotificationFailureThreshold" xml:space="preserve">
<value>Failure threshold</value>
</data>
<data name="HelpMessage_NotificationFailureThreshold" xml:space="preserve">
<value>Number of consecutive failed pings (timeouts) required before a "Host is down" notification is shown. Higher values reduce noise from flapping hosts.</value>
</data>
<data name="NotificationDurationInSeconds" xml:space="preserve">
<value>Display duration (seconds)</value>
</data>
<data name="HostIsUp" xml:space="preserve">
<value>Host is up</value>
</data>
<data name="HostIsDown" xml:space="preserve">
<value>Host is down</value>
</data>
<data name="TheApplicationWillBeRestarted" xml:space="preserve">
<value>The application will be restarted...</value>
</data>
Expand Down
12 changes: 12 additions & 0 deletions Source/NETworkManager.Settings/GlobalStaticConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ public static class GlobalStaticConfiguration
// Network config
public static int NetworkChangeDetectionDelay => 5000;

// Notification config
// Minimum interval (ms) between two notification sounds, so a burst of near-simultaneous
// status changes collapses into a single sound instead of an overlapping cacophony.
public static int NotificationSoundThrottle => 3000;

// Profile config
public static bool Profile_TagsMatchAny => true;
public static bool Profile_ExpandProfileView => true;
Expand Down Expand Up @@ -147,6 +152,13 @@ public static class GlobalStaticConfiguration
public static int PingMonitor_ChartTime => 120;
public static ExportFileType PingMonitor_ExportFileType => ExportFileType.Csv;

// Application: Ping Monitor (notifications)
public static bool PingMonitor_ShowNotificationPopup => true;
public static bool PingMonitor_NotificationSound => true;
public static int PingMonitor_NotificationSuccessThreshold => 1;
public static int PingMonitor_NotificationFailureThreshold => 3;
public static int PingMonitor_NotificationCloseTime => 10;

// Application: Traceroute
public static int Traceroute_MaximumHops => 30;
public static int Traceroute_Timeout => 4000;
Expand Down
65 changes: 65 additions & 0 deletions Source/NETworkManager.Settings/SettingsInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1537,6 +1537,71 @@ public int PingMonitor_ChartTime
}
} = GlobalStaticConfiguration.PingMonitor_ChartTime;

public bool PingMonitor_ShowNotificationPopup
{
get;
set
{
if (value == field)
return;

field = value;
OnPropertyChanged();
}
} = GlobalStaticConfiguration.PingMonitor_ShowNotificationPopup;

public bool PingMonitor_NotificationSound
{
get;
set
{
if (value == field)
return;

field = value;
OnPropertyChanged();
}
} = GlobalStaticConfiguration.PingMonitor_NotificationSound;

public int PingMonitor_NotificationSuccessThreshold
{
get;
set
{
if (value == field)
return;

field = value;
OnPropertyChanged();
}
} = GlobalStaticConfiguration.PingMonitor_NotificationSuccessThreshold;

public int PingMonitor_NotificationFailureThreshold
{
get;
set
{
if (value == field)
return;

field = value;
OnPropertyChanged();
}
} = GlobalStaticConfiguration.PingMonitor_NotificationFailureThreshold;

public int PingMonitor_NotificationCloseTime
{
get;
set
{
if (value == field)
return;

field = value;
OnPropertyChanged();
}
} = GlobalStaticConfiguration.PingMonitor_NotificationCloseTime;

public string PingMonitor_ExportFilePath
{
get;
Expand Down
98 changes: 98 additions & 0 deletions Source/NETworkManager/NotificationManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
using MahApps.Metro.IconPacks;
using NETworkManager.Settings;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Media;
using System.Threading;
using System.Windows;

namespace NETworkManager;

/// <summary>
/// Shows and manages stackable notification popups. Follows the same pattern as
/// <see cref="DialogHelper"/> — callers pass display parameters; internals are hidden.
/// </summary>
public static class NotificationManager
{
private static readonly List<NotificationWindow> ActiveWindows = [];

/// <summary>
/// Margin (in DIP) between the screen edges and between stacked notification windows.
/// </summary>
internal const double WindowMargin = 10.0;

// Throttles the notification sound (see GlobalStaticConfiguration.NotificationSoundThrottle)
// so a burst of near-simultaneous status changes collapses into a single sound.
private static readonly Lock SoundLock = new();
private static DateTime _lastSoundPlayed = DateTime.MinValue;

/// <summary>
/// Shows a notification popup in the bottom-right corner of the primary screen.
/// Safe to call from any thread.
/// </summary>
/// <param name="iconKind">The Material icon shown on the left of the popup.</param>
/// <param name="iconColor">The icon fill color (e.g. "Red" or "#badc58").</param>
/// <param name="title">The bold header text (e.g. the host title).</param>
/// <param name="message">The gray subtext below the header (e.g. "Host is up").</param>
/// <param name="closeTimeSeconds">Seconds before the popup closes automatically.</param>
public static void Show(PackIconMaterialKind iconKind, string iconColor, string title, string message, int closeTimeSeconds)
{
// A late ping callback during application shutdown may find no Application instance.
if (Application.Current is null)
return;

Application.Current.Dispatcher.BeginInvoke(() =>
{
var window = new NotificationWindow(iconKind, iconColor, title, message, closeTimeSeconds);

window.Closed += (_, _) =>
{
ActiveWindows.Remove(window);
RepositionAll();
};

ActiveWindows.Add(window);
window.Show(); // triggers OnSourceInitialized → window positions itself
});
}

/// <summary>
/// Returns the combined height (including margins) of all windows stacked below the given
/// window, i.e. the vertical offset from the bottom edge at which it should be placed.
/// </summary>
internal static double GetStackOffset(NotificationWindow window)
{
return ActiveWindows.TakeWhile(w => !ReferenceEquals(w, window)).Sum(w => w.ActualHeight + WindowMargin);
}

/// <summary>
/// Plays a notification sound, throttled so that a burst of near-simultaneous status changes
/// (e.g. many hosts going down at once) results in a single sound. Safe to call from any thread.
/// </summary>
/// <param name="sound">The system sound to play.</param>
public static void PlaySound(SystemSound sound)
{
lock (SoundLock)
{
var now = DateTime.UtcNow;

if ((now - _lastSoundPlayed).TotalMilliseconds < GlobalStaticConfiguration.NotificationSoundThrottle)
return;

_lastSoundPlayed = now;
}

sound.Play();
}

/// <summary>
/// Repositions all active windows, e.g. after one closes or its height changes so the stack
/// stays bottom-anchored without gaps or overlap.
/// </summary>
internal static void RepositionAll()
{
foreach (var window in ActiveWindows)
window.Reposition();
}
}
Loading
Loading