Skip to content

Commit

Permalink
New notification button to disable a particular level of notification…
Browse files Browse the repository at this point in the history
… and manual refresh button in details view.
  • Loading branch information
leonzhou-smokeball committed Dec 18, 2024
1 parent 17daacd commit 758d376
Show file tree
Hide file tree
Showing 8 changed files with 123 additions and 36 deletions.
42 changes: 38 additions & 4 deletions App/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public partial class App
internal const string Id = "f05f920a-c997-4817-84bd-c54d87e40625";
private static Exception _trayIconUpdateError;
internal static readonly FontFamily DefaultTrayIconFontFamily = new("Microsoft Sans Serif");
internal static readonly ISnackbarService SnackbarService = new SnackbarService();
internal static readonly ISnackbarService SnackBarService = new SnackbarService();

private readonly Mutex _appMutex;

Expand Down Expand Up @@ -68,9 +68,38 @@ public App()
// Subscribe to toast notification activations.
ToastNotificationManagerCompat.OnActivated += async toastArgs =>
{
if (ToastArguments.Parse(toastArgs.Argument).GetActionArgument() ==
ToastNotificationExtensions.Action.ViewDetails)
await Dispatcher.InvokeAsync(() => ActivateMainWindow().NavigateToPage<DetailsPage>());
var arguments = ToastArguments.Parse(toastArgs.Argument);
var action = arguments.GetActionArgument();
switch (action)
{
case ToastNotificationExtensions.Action.ViewDetails:
await Dispatcher.InvokeAsync(() => ActivateMainWindow().NavigateToPage<DetailsPage>());
break;
case ToastNotificationExtensions.Action.DisableBatteryNotification:
var type = arguments.GetNotificationTypeArgument();
switch (type)
{
case ToastNotificationExtensions.NotificationType.Critical:
Default.BatteryCriticalNotification = false;
break;
case ToastNotificationExtensions.NotificationType.Low:
Default.BatteryLowNotification = false;
break;
case ToastNotificationExtensions.NotificationType.High:
Default.BatteryHighNotification = false;
break;
case ToastNotificationExtensions.NotificationType.Full:
Default.BatteryFullNotification = false;
break;
case ToastNotificationExtensions.NotificationType.None:
default:
throw new ArgumentOutOfRangeException(nameof(type), type, $"{type} is not supported.");
}

break;
default:
throw new ArgumentOutOfRangeException(nameof(action), action, $"{action} is not supported.");
}
};
}

Expand All @@ -93,6 +122,11 @@ internal static Exception GetTrayIconUpdateError()
return _trayIconUpdateError;
}

internal static TrayIconWindow GetTrayIconWindow()
{
return Current.Windows.OfType<TrayIconWindow>().FirstOrDefault();
}

private static void HandleException(object exception)
{
var version = Helper.GetAppVersion();
Expand Down
17 changes: 14 additions & 3 deletions App/Controls/BatteryInformation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.ComponentModel;
using System.Linq;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Controls;
Expand Down Expand Up @@ -43,6 +44,8 @@ public partial class BatteryInformation : KeyValueItemsControl

private readonly BatteryInformationObservableValue _remainingChargeCapacity =
new(SymbolRegular.Battery020, "Remaining Charge Capacity");

private readonly Subject<bool> _updateSubject = new();

static BatteryInformation()
{
Expand Down Expand Up @@ -79,15 +82,18 @@ public BatteryInformation()
FindResource(typeof(CardControl)) is Style style)
card.Style = style;

Update();
_updateSubject.Throttle(TimeSpan.FromMilliseconds(500))
.ObserveOn(AsyncOperationManager.SynchronizationContext)
.Subscribe(_ => Update());

_updateSubject.OnNext(false);

SetupUpdateSubscription();

Observable.FromEventPattern<PropertyChangedEventHandler, PropertyChangedEventArgs>(
handler => Settings.Default.PropertyChanged += handler,
handler => Settings.Default.PropertyChanged -= handler)
.Throttle(TimeSpan.FromMilliseconds(500))
.ObserveOn(AsyncOperationManager.SynchronizationContext)
.Subscribe(_ => SetupUpdateSubscription());

return;
Expand All @@ -96,7 +102,7 @@ void SetupUpdateSubscription()
{
updateSubscription?.Dispose();
updateSubscription = Observable.Interval(TimeSpan.FromSeconds(Settings.Default.RefreshSeconds))
.ObserveOn(AsyncOperationManager.SynchronizationContext).Subscribe(_ => Update());
.ObserveOn(AsyncOperationManager.SynchronizationContext).Subscribe(_ => _updateSubject.OnNext(false));
}
};

Expand Down Expand Up @@ -189,4 +195,9 @@ public override string ToString()
return Value?.ToString();
}
}

internal void RequestUpdate()
{
_updateSubject.OnNext(false);
}
}
4 changes: 2 additions & 2 deletions App/Controls/CopyButton.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,13 @@ protected override void OnClick()
var targetStringValue = TargetObject?.ToString();
if (targetStringValue == null)
{
App.SnackbarService.Show("Nothing to copy", "There was nothing to copy to the clipboard.",
App.SnackBarService.Show("Nothing to copy", "There was nothing to copy to the clipboard.",
ControlAppearance.Caution);
}
else
{
Clipboard.SetText(targetStringValue);
App.SnackbarService.Show("Copied to clipboard", targetStringValue, ControlAppearance.Success);
App.SnackBarService.Show("Copied to clipboard", targetStringValue, ControlAppearance.Success);
}

base.OnClick();
Expand Down
33 changes: 29 additions & 4 deletions App/Extensions/ToastNotificationExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,29 +1,54 @@
using Microsoft.Toolkit.Uwp.Notifications;
using System;
using Microsoft.Toolkit.Uwp.Notifications;

namespace Percentage.App.Extensions;

internal static class ToastNotificationExtensions
{
private const string ActionArgumentKey = "action";
private const string NotificationTypeArgumentKey = "notificationType";

internal static Action GetActionArgument(this ToastArguments arguments)
{
return arguments.GetEnum<Action>(ActionArgumentKey);
}

internal static void ShowToastNotification(string header, string body)
internal static NotificationType GetNotificationTypeArgument(this ToastArguments arguments)
{
return arguments.GetEnum<NotificationType>(NotificationTypeArgumentKey);
}

internal static void ShowToastNotification(string header, string body, NotificationType notificationType)
{
if (notificationType == NotificationType.None)
{
throw new NotSupportedException($"Notification type {notificationType} is not supported.");
}

new ToastContentBuilder()
.AddText(header)
.AddText(body)
.AddButton(new ToastButton().SetContent("See more details")
.AddButton(new ToastButton().SetContent("Details")
.AddArgument(ActionArgumentKey, Action.ViewDetails))
.AddButton(new ToastButton().SetContent("Disable")
.AddArgument(ActionArgumentKey, Action.DisableBatteryNotification)
.AddArgument(NotificationTypeArgumentKey, notificationType))
.AddButton(new ToastButtonDismiss())
.Show();
}

internal enum Action
{
ViewDetails = 0
ViewDetails = 0,
DisableBatteryNotification
}

internal enum NotificationType
{
None = 0,
Critical,
Low,
High,
Full
}
}
2 changes: 1 addition & 1 deletion App/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public MainWindow()
{
SystemThemeWatcher.Watch(this);
InitializeComponent();
App.SnackbarService.SetSnackbarPresenter(SnackbarPresenter);
App.SnackBarService.SetSnackbarPresenter(SnackbarPresenter);
}

internal void NavigateToPage<T>() where T : Page
Expand Down
12 changes: 9 additions & 3 deletions App/Pages/DetailsPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,19 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:Percentage.App.Controls"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
DataContext="{Binding RelativeSource={RelativeSource Self}, Mode=OneTime}"
Title="Details">
<StackPanel>
<controls:BatteryInformation x:Name="BatteryInformation"
x:FieldModifier="private" />
<controls:CopyButton HorizontalAlignment="Right"
Content="Copy"
TargetObject="{Binding ElementName=BatteryInformation}" />
<Grid>
<ui:Button Icon="{ui:SymbolIcon ArrowClockwise20}"
Content="Refresh"
Click="OnRefreshButtonClick" />
<controls:CopyButton HorizontalAlignment="Right"
Content="Copy"
TargetObject="{Binding ElementName=BatteryInformation}" />
</Grid>
</StackPanel>
</Page>
8 changes: 8 additions & 0 deletions App/Pages/DetailsPage.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.Windows;

namespace Percentage.App.Pages;

public partial class DetailsPage
Expand All @@ -6,4 +8,10 @@ public DetailsPage()
{
InitializeComponent();
}

private void OnRefreshButtonClick(object sender, RoutedEventArgs e)
{
BatteryInformation.RequestUpdate();
App.GetTrayIconWindow().RequestBatteryStatusUpdate();
}
}
41 changes: 22 additions & 19 deletions App/TrayIconWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ public partial class TrayIconWindow
private static readonly TimeSpan DebounceTimeSpan = TimeSpan.FromMilliseconds(500);
private readonly Subject<bool> _batteryStatusUpdateSubject = new();
private readonly DispatcherTimer _refreshTimer;
private (NotificationType Type, DateTime DateTime) _lastNotification = (default, default);

private (ToastNotificationExtensions.NotificationType Type, DateTime DateTime) _lastNotification =
(default, default);

private string _notificationText;
private string _notificationTitle;

Expand Down Expand Up @@ -130,18 +133,18 @@ private void OnLoaded(object sender, RoutedEventArgs args)
_batteryStatusUpdateSubject.Throttle(DebounceTimeSpan).ObserveOn(AsyncOperationManager.SynchronizationContext)
.Subscribe(_ => UpdateBatteryStatus());

// Update battery status when the computer resumes or when the power status changes with debounce.
// Update battery status when the computer resumes or when the power status changes with debouncing.
SystemEvents.PowerModeChanged += (_, _) => _batteryStatusUpdateSubject.OnNext(false);

// Update battery status when the display settings change with debounce.
// Update battery status when the display settings change with debouncing.
// This will redraw the tray icon to ensure optimal icon resolution under the current display settings.
SystemEvents.DisplaySettingsChanged += (_, _) => _batteryStatusUpdateSubject.OnNext(false);

// This event can be triggered multiple times when Windows changes between dark and light theme.
// Update tray icon colour when user preference changes settled down.
SystemEvents.UserPreferenceChanged += (_, _) => _batteryStatusUpdateSubject.OnNext(false);

// Handle user settings change with debounce.
// Handle user settings change with debouncing.
Observable.FromEventPattern<PropertyChangedEventHandler, PropertyChangedEventArgs>(
handler => Default.PropertyChanged += handler,
handler => Default.PropertyChanged -= handler)
Expand Down Expand Up @@ -219,12 +222,13 @@ private void SetNotifyIconText(string text, Brush foreground, string fontFamily

var iconImageSource = GetImageSource(textBlock);

// There's a chance that some native exception may be thrown when setting the notify icon's image.
// There's a chance that some native exception may be thrown when setting the icon's image.
// Catch any exception here and retry a few times then fail silently with logs.
for (var i = 0; i < 5; i++)
try
{
NotifyIcon.Icon = iconImageSource;
App.SetTrayIconUpdateError(null);
break;
}
catch (Exception e)
Expand All @@ -241,7 +245,7 @@ private void UpdateBatteryStatus()
var powerStatus = SystemInformation.PowerStatus;
var batteryChargeStatus = powerStatus.BatteryChargeStatus;
var percent = (int)Math.Round(powerStatus.BatteryLifePercent * 100);
var notificationType = NotificationType.None;
var notificationType = ToastNotificationExtensions.NotificationType.None;
Brush brush;
string trayIconText;
switch (batteryChargeStatus)
Expand Down Expand Up @@ -280,7 +284,7 @@ private void UpdateBatteryStatus()
// Show fully charged notification.

_notificationTitle = "Fully charged" + powerLineText;
notificationType = NotificationType.Full;
notificationType = ToastNotificationExtensions.NotificationType.Full;
CheckAndSendNotification();

return;
Expand Down Expand Up @@ -322,13 +326,15 @@ private void UpdateBatteryStatus()
{
// When battery capacity is critical.
brush = GetBatteryCriticalBrush();
if (Default.BatteryCriticalNotification) notificationType = NotificationType.Critical;
if (Default.BatteryCriticalNotification)
notificationType = ToastNotificationExtensions.NotificationType.Critical;
}
else if (percent <= Default.BatteryLowNotificationValue)
{
// When battery capacity is low.
brush = GetBatteryLowBrush();
if (Default.BatteryLowNotification) notificationType = NotificationType.Low;
if (Default.BatteryLowNotification)
notificationType = ToastNotificationExtensions.NotificationType.Low;
}
else
{
Expand Down Expand Up @@ -361,9 +367,9 @@ private void UpdateBatteryStatus()
void SetHighOrFullNotification()
{
if (percent == Default.BatteryHighNotificationValue && Default.BatteryHighNotification)
notificationType = NotificationType.High;
notificationType = ToastNotificationExtensions.NotificationType.High;
else if (percent == 100 && Default.BatteryFullNotification)
notificationType = NotificationType.Full;
notificationType = ToastNotificationExtensions.NotificationType.Full;
}
}
}
Expand All @@ -381,7 +387,7 @@ void SetHighOrFullNotification()
// Check and send notification.
void CheckAndSendNotification()
{
if (notificationType == NotificationType.None)
if (notificationType == ToastNotificationExtensions.NotificationType.None)
// No notification required.
return;

Expand All @@ -390,18 +396,15 @@ void CheckAndSendNotification()
utcNow - _lastNotification.DateTime > TimeSpan.FromMinutes(5))
// Notification is required if the existing notification type is different from the previous one or
// battery status is the same, but it has been more than 5 minutes since the last notification was shown.
ToastNotificationExtensions.ShowToastNotification(_notificationTitle, _notificationText);
ToastNotificationExtensions.ShowToastNotification(_notificationTitle, _notificationText,
notificationType);

_lastNotification = (notificationType, utcNow);
}
}

private enum NotificationType : byte
public void RequestBatteryStatusUpdate()
{
None = 0,
Critical,
Low,
High,
Full
_batteryStatusUpdateSubject.OnNext(false);
}
}

0 comments on commit 758d376

Please sign in to comment.