From 59830df7867f0f9e594e758c5cef526dcedd49e1 Mon Sep 17 00:00:00 2001 From: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> Date: Sun, 7 Jun 2026 00:07:32 +0200 Subject: [PATCH 1/7] Feature: NotificationManager + Ping notifications --- .../Resources/Strings.Designer.cs | 92 ++++++++- .../Resources/Strings.resx | 30 +++ .../GlobalStaticConfiguration.cs | 12 ++ .../NETworkManager.Settings/SettingsInfo.cs | 65 ++++++ Source/NETworkManager/NotificationManager.cs | 93 +++++++++ Source/NETworkManager/NotificationWindow.xaml | 119 +++++++++++ .../NETworkManager/NotificationWindow.xaml.cs | 195 ++++++++++++++++++ Source/NETworkManager/StatusWindow.xaml.cs | 2 - .../PingMonitorSettingsViewModel.cs | 100 +++++++++ .../ViewModels/PingMonitorViewModel.cs | 71 ++++++- .../Views/PingMonitorSettingsView.xaml | 41 +++- .../NETworkManager/Views/PingMonitorView.xaml | 2 +- Website/docs/application/ping-monitor.md | 57 +++++ Website/docs/changelog/next-release.md | 12 ++ 14 files changed, 884 insertions(+), 7 deletions(-) create mode 100644 Source/NETworkManager/NotificationManager.cs create mode 100644 Source/NETworkManager/NotificationWindow.xaml create mode 100644 Source/NETworkManager/NotificationWindow.xaml.cs diff --git a/Source/NETworkManager.Localization/Resources/Strings.Designer.cs b/Source/NETworkManager.Localization/Resources/Strings.Designer.cs index 5379e77ebd..a6e70ad0e0 100644 --- a/Source/NETworkManager.Localization/Resources/Strings.Designer.cs +++ b/Source/NETworkManager.Localization/Resources/Strings.Designer.cs @@ -11118,7 +11118,97 @@ public static string StatusChange { return ResourceManager.GetString("StatusChange", resourceCulture); } } - + + /// + /// Looks up a localized string similar to Notification. + /// + public static string PingMonitorNotification { + get { + return ResourceManager.GetString("PingMonitorNotification", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Show notification popup on status change. + /// + public static string ShowNotificationPopupOnStatusChange { + get { + return ResourceManager.GetString("ShowNotificationPopupOnStatusChange", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Play sound on status change. + /// + public static string PlaySoundOnStatusChange { + get { + return ResourceManager.GetString("PlaySoundOnStatusChange", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Success threshold. + /// + public static string NotificationSuccessThreshold { + get { + return ResourceManager.GetString("NotificationSuccessThreshold", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Number of consecutive successful pings required before an "Host is up" notification is shown. Higher values reduce noise from flapping hosts. + /// + public static string HelpMessage_NotificationSuccessThreshold { + get { + return ResourceManager.GetString("HelpMessage_NotificationSuccessThreshold", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Failure threshold. + /// + public static string NotificationFailureThreshold { + get { + return ResourceManager.GetString("NotificationFailureThreshold", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Number of consecutive failed pings (timeouts) required before an "Host is down" notification is shown. Higher values reduce noise from flapping hosts. + /// + public static string HelpMessage_NotificationFailureThreshold { + get { + return ResourceManager.GetString("HelpMessage_NotificationFailureThreshold", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Display duration (seconds). + /// + public static string NotificationDurationInSeconds { + get { + return ResourceManager.GetString("NotificationDurationInSeconds", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Host is up. + /// + public static string HostIsUp { + get { + return ResourceManager.GetString("HostIsUp", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Host is down. + /// + public static string HostIsDown { + get { + return ResourceManager.GetString("HostIsDown", resourceCulture); + } + } + /// /// Looks up a localized string similar to Status window. /// diff --git a/Source/NETworkManager.Localization/Resources/Strings.resx b/Source/NETworkManager.Localization/Resources/Strings.resx index 3539a167ac..661cfe4e7e 100644 --- a/Source/NETworkManager.Localization/Resources/Strings.resx +++ b/Source/NETworkManager.Localization/Resources/Strings.resx @@ -2367,6 +2367,36 @@ $$hostname$$ --> Hostname Could not resolve ip address for: "{0}" + + Notification + + + Show notification popup on status change + + + Play sound on status change + + + Success threshold + + + Number of consecutive successful pings required before an "Host is up" notification is shown. Higher values reduce noise from flapping hosts. + + + Failure threshold + + + Number of consecutive failed pings (timeouts) required before an "Host is down" notification is shown. Higher values reduce noise from flapping hosts. + + + Display duration (seconds) + + + Host is up + + + Host is down + The application will be restarted... diff --git a/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs b/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs index 0252eb58a4..ee66caad44 100644 --- a/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs +++ b/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs @@ -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; @@ -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; diff --git a/Source/NETworkManager.Settings/SettingsInfo.cs b/Source/NETworkManager.Settings/SettingsInfo.cs index ffbcdc1f37..1a739e5724 100644 --- a/Source/NETworkManager.Settings/SettingsInfo.cs +++ b/Source/NETworkManager.Settings/SettingsInfo.cs @@ -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; diff --git a/Source/NETworkManager/NotificationManager.cs b/Source/NETworkManager/NotificationManager.cs new file mode 100644 index 0000000000..866e4f5e69 --- /dev/null +++ b/Source/NETworkManager/NotificationManager.cs @@ -0,0 +1,93 @@ +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; + +/// +/// Shows and manages stackable notification popups. Follows the same pattern as +/// — callers pass display parameters; internals are hidden. +/// +public static class NotificationManager +{ + private static readonly List ActiveWindows = []; + + /// + /// Margin (in DIP) between the screen edges and between stacked notification windows. + /// + 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; + + /// + /// Shows a notification popup in the bottom-right corner of the primary screen. + /// Safe to call from any thread. + /// + /// The Material icon shown on the left of the popup. + /// The icon fill color (e.g. "Red" or "#badc58"). + /// The bold header text (e.g. the host title). + /// The gray subtext below the header (e.g. "Host is up"). + /// Seconds before the popup closes automatically. + public static void Show(PackIconMaterialKind iconKind, string iconColor, string title, string message, int closeTimeSeconds) + { + 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 + }); + } + + /// + /// 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. + /// + internal static double GetStackOffset(NotificationWindow window) + { + return ActiveWindows.TakeWhile(w => !ReferenceEquals(w, window)).Sum(w => w.ActualHeight + WindowMargin); + } + + /// + /// 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. + /// + /// The system sound to play. + public static void PlaySound(SystemSound sound) + { + lock (SoundLock) + { + var now = DateTime.UtcNow; + + if ((now - _lastSoundPlayed).TotalMilliseconds < GlobalStaticConfiguration.NotificationSoundThrottle) + return; + + _lastSoundPlayed = now; + } + + sound.Play(); + } + + /// + /// Repositions all active windows after a sibling closes and stack indices shift. + /// + internal static void RepositionAll() + { + foreach (var window in ActiveWindows) + window.Reposition(); + } +} diff --git a/Source/NETworkManager/NotificationWindow.xaml b/Source/NETworkManager/NotificationWindow.xaml new file mode 100644 index 0000000000..fba46904b6 --- /dev/null +++ b/Source/NETworkManager/NotificationWindow.xaml @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Source/NETworkManager/NotificationWindow.xaml.cs b/Source/NETworkManager/NotificationWindow.xaml.cs new file mode 100644 index 0000000000..ca97851569 --- /dev/null +++ b/Source/NETworkManager/NotificationWindow.xaml.cs @@ -0,0 +1,195 @@ +using MahApps.Metro.IconPacks; +using NETworkManager.Utilities; +using System; +using System.ComponentModel; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Forms; +using System.Windows.Input; +using System.Windows.Threading; + +namespace NETworkManager; + +/// +/// A generic, stackable notification popup shown in the bottom-right corner of the primary +/// screen. Contains no feature-specific knowledge — icon, color, title and message are all +/// supplied by the caller (via ). +/// +public partial class NotificationWindow : INotifyPropertyChanged +{ + #region PropertyChangedEventHandler + + public event PropertyChangedEventHandler PropertyChanged; + + private void OnPropertyChanged([CallerMemberName] string propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + #endregion + + #region Variables + + private readonly DispatcherTimer _timer = new(DispatcherPriority.Render); + private readonly Stopwatch _stopwatch = new(); + + // Bound properties — all set once in the constructor, no change notifications needed. + // Note: the header property is named NotificationTitle (not Title) to avoid clashing with + // the inherited Window.Title property, which a "{Binding Title}" would otherwise resolve to. + public PackIconMaterialKind IconKind { get; } + public string IconColor { get; } + public string NotificationTitle { get; } + public string Message { get; } + + // Timestamp shown in the header — captured when the notification is created, which is the + // moment the status change occurred. + public string Time { get; } + + public double TimeMax { get; } + + public double TimeRemaining + { + get; + private set + { + if (Math.Abs(value - field) < 0.001) + return; + + field = value; + OnPropertyChanged(); + } + } + + // Command — initialized once in the constructor, not recreated on every binding access + public ICommand CloseCommand { get; } + + #endregion + + #region Constructor + + public NotificationWindow(PackIconMaterialKind iconKind, string iconColor, string title, string message, int closeTimeSeconds) + { + InitializeComponent(); + DataContext = this; + + IconKind = iconKind; + IconColor = iconColor; + NotificationTitle = title; + Message = message; + Time = DateTime.Now.ToString("HH:mm:ss"); + TimeMax = closeTimeSeconds; + TimeRemaining = closeTimeSeconds; + + CloseCommand = new RelayCommand(_ => CloseWindow()); + + // The window auto-sizes its height to the content (SizeToContent=Height), so when the + // message wraps to more lines the whole stack must re-anchor to the bottom edge. + SizeChanged += (_, _) => NotificationManager.RepositionAll(); + + _timer.Interval = TimeSpan.FromMilliseconds(16); + _timer.Tick += Timer_Tick; + } + + #endregion + + #region ICommands & Actions + + private void ShowMainWindow() + { + CloseWindow(); + + if (System.Windows.Application.Current.MainWindow is MainWindow mainWindow && + mainWindow.ShowWindowCommand.CanExecute(null)) + mainWindow.ShowWindowCommand.Execute(null); + } + + #endregion + + #region Methods + + /// + /// Called by when the stack changes (a sibling closes or + /// resizes) and this window may need to move. + /// + internal void Reposition() + { + ApplyPosition(); + } + + private void ApplyPosition() + { + if (Screen.PrimaryScreen == null) + return; + + var scale = System.Windows.Media.VisualTreeHelper.GetDpi(this).DpiScaleX; + var area = Screen.PrimaryScreen.WorkingArea; + + // Offset = total height (incl. margins) of all windows stacked below this one. Using the + // actual heights keeps variable-height popups (wrapped messages) bottom-anchored. + var offset = NotificationManager.GetStackOffset(this); + + Left = area.Right / scale - Width - NotificationManager.WindowMargin; + Top = area.Bottom / scale - ActualHeight - NotificationManager.WindowMargin - offset; + } + + private void CloseWindow() + { + _timer.Stop(); + _stopwatch.Stop(); + + Closing -= MetroWindow_Closing; + Close(); // fires Window.Closed → NotificationManager removes from stack and repositions + } + + #endregion + + #region Events + + // Lifecycle — the HWND exists here, so DPI is safe to read for positioning + protected override void OnSourceInitialized(EventArgs e) + { + base.OnSourceInitialized(e); + + ApplyPosition(); + + _stopwatch.Restart(); + _timer.Start(); + } + + // Clicking anywhere on the popup opens the main window. The close button handles its own + // mouse events, so clicking [×] does not bubble up here. + private void Root_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) + { + ShowMainWindow(); + } + + // Intercept Alt+F4 / OS close so the stack cleanup always runs through CloseWindow() + private void MetroWindow_Closing(object sender, CancelEventArgs e) + { + e.Cancel = true; + CloseWindow(); + } + + private async void Timer_Tick(object sender, EventArgs e) + { + // Use a local for the close decision — the TimeRemaining setter ignores sub-0.001 changes + // (to throttle the progress bar), so reading the property back could stay stuck at a tiny + // positive value and the window would never close. + var remaining = Math.Max(0.0, TimeMax - _stopwatch.Elapsed.TotalSeconds); + TimeRemaining = remaining; + + if (remaining > 0) + return; + + _timer.Stop(); + _stopwatch.Stop(); + + await Task.Delay(250); // let the bar visually reach zero before closing + + CloseWindow(); + } + + #endregion +} diff --git a/Source/NETworkManager/StatusWindow.xaml.cs b/Source/NETworkManager/StatusWindow.xaml.cs index ade8b9b17c..f767076755 100644 --- a/Source/NETworkManager/StatusWindow.xaml.cs +++ b/Source/NETworkManager/StatusWindow.xaml.cs @@ -144,8 +144,6 @@ public void ShowWindow(bool enableCloseTimer = false) // Check the network connection Check(); - enableCloseTimer = true; - // Close the window after a certain time if (enableCloseTimer) { diff --git a/Source/NETworkManager/ViewModels/PingMonitorSettingsViewModel.cs b/Source/NETworkManager/ViewModels/PingMonitorSettingsViewModel.cs index 462b907090..f3692fbaa7 100644 --- a/Source/NETworkManager/ViewModels/PingMonitorSettingsViewModel.cs +++ b/Source/NETworkManager/ViewModels/PingMonitorSettingsViewModel.cs @@ -144,6 +144,101 @@ public int ChartTime } } + /// + /// Gets or sets a value indicating whether a notification popup is shown on status change. + /// + public bool ShowNotificationPopup + { + get; + set + { + if (value == field) + return; + + if (!_isLoading) + SettingsManager.Current.PingMonitor_ShowNotificationPopup = value; + + field = value; + OnPropertyChanged(); + } + } + + /// + /// Gets or sets a value indicating whether a sound is played on status change. + /// + public bool NotificationSound + { + get; + set + { + if (value == field) + return; + + if (!_isLoading) + SettingsManager.Current.PingMonitor_NotificationSound = value; + + field = value; + OnPropertyChanged(); + } + } + + /// + /// Gets or sets the number of consecutive successful pings required before an UP notification fires. + /// + public int NotificationSuccessThreshold + { + get; + set + { + if (value == field) + return; + + if (!_isLoading) + SettingsManager.Current.PingMonitor_NotificationSuccessThreshold = value; + + field = value; + OnPropertyChanged(); + } + } + + /// + /// Gets or sets the number of consecutive failed pings required before a DOWN notification fires. + /// + public int NotificationFailureThreshold + { + get; + set + { + if (value == field) + return; + + if (!_isLoading) + SettingsManager.Current.PingMonitor_NotificationFailureThreshold = value; + + field = value; + OnPropertyChanged(); + } + } + + /// + /// Gets or sets the time in seconds the notification popup is shown before it closes automatically. + /// + public int NotificationCloseTime + { + get; + set + { + if (value == field) + return; + + if (!_isLoading) + SettingsManager.Current.PingMonitor_NotificationCloseTime = value; + + field = value; + OnPropertyChanged(); + } + } + #endregion #region Contructor, load settings @@ -169,6 +264,11 @@ private void LoadSettings() WaitTime = SettingsManager.Current.PingMonitor_WaitTime; ExpandHostView = SettingsManager.Current.PingMonitor_ExpandHostView; ChartTime = SettingsManager.Current.PingMonitor_ChartTime; + ShowNotificationPopup = SettingsManager.Current.PingMonitor_ShowNotificationPopup; + NotificationSound = SettingsManager.Current.PingMonitor_NotificationSound; + NotificationSuccessThreshold = SettingsManager.Current.PingMonitor_NotificationSuccessThreshold; + NotificationFailureThreshold = SettingsManager.Current.PingMonitor_NotificationFailureThreshold; + NotificationCloseTime = SettingsManager.Current.PingMonitor_NotificationCloseTime; } #endregion diff --git a/Source/NETworkManager/ViewModels/PingMonitorViewModel.cs b/Source/NETworkManager/ViewModels/PingMonitorViewModel.cs index 2558a171ea..72e8fc5913 100644 --- a/Source/NETworkManager/ViewModels/PingMonitorViewModel.cs +++ b/Source/NETworkManager/ViewModels/PingMonitorViewModel.cs @@ -5,6 +5,7 @@ using LiveChartsCore.SkiaSharpView.Painting; using LiveChartsCore.SkiaSharpView.Painting.Effects; using log4net; +using MahApps.Metro.IconPacks; using MahApps.Metro.SimpleChildWindow; using NETworkManager.Localization.Resources; using NETworkManager.Models.Export; @@ -76,6 +77,13 @@ public PingMonitorViewModel(Guid hostId, Action removeHostByGuid, private List _pingInfoList = []; + // Notification threshold tracking. The status transition (and notification) only fires once + // the configured number of consecutive successes/failures is reached, to avoid noise from + // flapping hosts. The initial state is established silently (no notification). + private int _consecutiveSuccesses; + private int _consecutiveFailures; + private bool _initialStateEstablished; + /// /// Gets the title of the monitor, typically "Hostname # IP". /// @@ -92,6 +100,13 @@ private set } } + /// + /// Gets the host display name used as the notification header: the hostname if available, + /// otherwise the IP address as a fallback. + /// + private string NotificationHostDisplay => + string.IsNullOrEmpty(Hostname) ? IPAddress.ToString() : Hostname.TrimEnd('.'); + /// /// Gets the hostname of the monitored host. /// @@ -529,6 +544,11 @@ public void Start() Lost = 0; PacketLoss = 0; + // Reset notification threshold tracking so the initial state is re-established silently + _consecutiveSuccesses = 0; + _consecutiveFailures = 0; + _initialStateEstablished = false; + // Reset chart _sessionStartTime = DateTime.Now; ResetTimeChart(); @@ -653,12 +673,37 @@ private void Ping_PingReceived(object sender, PingReceivedArgs e) LvlChartsDefaultInfo timeInfo; + var successThreshold = SettingsManager.Current.PingMonitor_NotificationSuccessThreshold; + var failureThreshold = SettingsManager.Current.PingMonitor_NotificationFailureThreshold; + if (e.Args.Status == IPStatus.Success) { - if (!IsReachable) + _consecutiveSuccesses++; + _consecutiveFailures = 0; + + if (!_initialStateEstablished) + { + if (_consecutiveSuccesses >= successThreshold) + { + _initialStateEstablished = true; + StatusTime = DateTime.Now; + IsReachable = true; + // No notification — this is the initial state being established. + } + } + else if (!IsReachable && _consecutiveSuccesses >= successThreshold) { StatusTime = DateTime.Now; IsReachable = true; + + if (SettingsManager.Current.PingMonitor_NotificationSound) + NotificationManager.PlaySound(System.Media.SystemSounds.Asterisk); // gentle info sound for UP + + if (SettingsManager.Current.PingMonitor_ShowNotificationPopup) + NotificationManager.Show( + PackIconMaterialKind.LanConnect, "#badc58", + NotificationHostDisplay, Strings.HostIsUp, + SettingsManager.Current.PingMonitor_NotificationCloseTime); } Received++; @@ -667,10 +712,32 @@ private void Ping_PingReceived(object sender, PingReceivedArgs e) } else { - if (IsReachable) + _consecutiveFailures++; + _consecutiveSuccesses = 0; + + if (!_initialStateEstablished) + { + if (_consecutiveFailures >= failureThreshold) + { + _initialStateEstablished = true; + StatusTime = DateTime.Now; + IsReachable = false; + // No notification or sound — initial state. + } + } + else if (IsReachable && _consecutiveFailures >= failureThreshold) { StatusTime = DateTime.Now; IsReachable = false; + + if (SettingsManager.Current.PingMonitor_NotificationSound) + NotificationManager.PlaySound(System.Media.SystemSounds.Exclamation); // warning sound for DOWN + + if (SettingsManager.Current.PingMonitor_ShowNotificationPopup) + NotificationManager.Show( + PackIconMaterialKind.LanDisconnect, "Red", + NotificationHostDisplay, Strings.HostIsDown, + SettingsManager.Current.PingMonitor_NotificationCloseTime); } Lost++; diff --git a/Source/NETworkManager/Views/PingMonitorSettingsView.xaml b/Source/NETworkManager/Views/PingMonitorSettingsView.xaml index f14a3e4584..0d3fc125ee 100644 --- a/Source/NETworkManager/Views/PingMonitorSettingsView.xaml +++ b/Source/NETworkManager/Views/PingMonitorSettingsView.xaml @@ -27,6 +27,45 @@ - + + + + + + + + +