From 5ac8e467c5dc0453cc959247dcd67714a075a755 Mon Sep 17 00:00:00 2001 From: Frank Mao <61649477+myfix16@users.noreply.github.com> Date: Tue, 11 Jul 2023 14:43:31 +0800 Subject: [PATCH 1/6] Feature: Add the prototype of a detailed battery info window --- src/App.xaml.cs | 8 + src/BatteryTracker.csproj | 6 +- src/MainWindow.xaml | 6 +- src/Models/BatteryInfo.cs | 11 + src/Resources/Commands.xaml | 20 +- src/Styles/Button.xaml | 859 +++++++++++-------------- src/Styles/FontSizes.xaml | 4 +- src/Styles/TextBlock.xaml | 14 +- src/Styles/Thickness.xaml | 6 +- src/ViewModels/BatteryInfoViewModel.cs | 41 ++ src/Views/BatteryIcon.cs | 5 + src/Views/BatteryInfoWindow.xaml | 42 ++ src/Views/BatteryInfoWindow.xaml.cs | 67 ++ 13 files changed, 582 insertions(+), 507 deletions(-) create mode 100644 src/Models/BatteryInfo.cs create mode 100644 src/ViewModels/BatteryInfoViewModel.cs create mode 100644 src/Views/BatteryInfoWindow.xaml create mode 100644 src/Views/BatteryInfoWindow.xaml.cs diff --git a/src/App.xaml.cs b/src/App.xaml.cs index e611f42..464142f 100644 --- a/src/App.xaml.cs +++ b/src/App.xaml.cs @@ -193,10 +193,18 @@ private async Task InitializeTrayIconAsync() var exitApplicationCommand = (XamlUICommand)Resources["ExitApplicationCommand"]; exitApplicationCommand.ExecuteRequested += ExitApplicationCommand_ExecuteRequested; + var displayBatteryInfoCommand = (XamlUICommand)Resources["DisplayBatteryInfoCommand"]; + displayBatteryInfoCommand.ExecuteRequested += DisplayBatteryInfoCommand_ExecuteRequested; + _batteryIcon = GetService(); await _batteryIcon.InitAsync(MainWindow.BatteryTrayIcon); } + void DisplayBatteryInfoCommand_ExecuteRequested(XamlUICommand sender, ExecuteRequestedEventArgs args) + { + _batteryIcon!.BatteryInfoWindow.Activate(); + } + private void ProcessUnhandledException(Exception? e, bool showNotification) { _logger.LogCritical(e, "Unhandled exception"); diff --git a/src/BatteryTracker.csproj b/src/BatteryTracker.csproj index 3d6298b..be35a9c 100644 --- a/src/BatteryTracker.csproj +++ b/src/BatteryTracker.csproj @@ -80,6 +80,7 @@ + @@ -107,7 +108,7 @@ - + @@ -145,6 +146,9 @@ $(DefaultXamlRuntime) + + MSBuild:Compile + MSBuild:Compile diff --git a/src/MainWindow.xaml b/src/MainWindow.xaml index e1e5e65..e5688a4 100644 --- a/src/MainWindow.xaml +++ b/src/MainWindow.xaml @@ -15,8 +15,10 @@ + ContextMenuMode="PopupMenu" + LeftClickCommand="{StaticResource DisplayBatteryInfoCommand}" + NoLeftClickDelay="True" ToolTipText="Battery Tracker" + Visibility="Visible"> + xmlns:tb="using:H.NotifyIcon"> - + x:Key="OpenSettingsCommand" x:Uid="OpenSettingsCommand" + Description="Open settings window" /> - + x:Key="ExitApplicationCommand" x:Uid="ExitApplicationCommand" + Description="Exit" /> + \ No newline at end of file diff --git a/src/Styles/Button.xaml b/src/Styles/Button.xaml index 2d736cc..e0e888b 100644 --- a/src/Styles/Button.xaml +++ b/src/Styles/Button.xaml @@ -1,6 +1,4 @@ - + @@ -119,6 +117,12 @@ Margin="{TemplateBinding Padding}" Background="{TemplateBinding Background}" CornerRadius="4"> + @@ -166,12 +170,6 @@ - @@ -179,9 +177,10 @@ - - diff --git a/src/Styles/Thickness.xaml b/src/Styles/Thickness.xaml index 5e46377..0564c19 100644 --- a/src/Styles/Thickness.xaml +++ b/src/Styles/Thickness.xaml @@ -1,6 +1,4 @@ - + 0,36,0,0 0,36,0,36 @@ -33,5 +31,5 @@ 36,24,36,0 -12,4,0,0 - + diff --git a/src/ViewModels/BatteryInfoViewModel.cs b/src/ViewModels/BatteryInfoViewModel.cs new file mode 100644 index 0000000..5fcc10a --- /dev/null +++ b/src/ViewModels/BatteryInfoViewModel.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BatteryTracker.Models; +using CommunityToolkit.Mvvm.ComponentModel; +using Microsoft.Windows.System.Power; + +namespace BatteryTracker.ViewModels +{ + public sealed class BatteryInfoViewModel : ObservableRecipient + { + private BatteryInfo _batteryInfo; + + public int ChargePercent + { + get => _batteryInfo.ChargePercent; + set => SetProperty(ref _batteryInfo.ChargePercent, value); + } + + public BatteryStatus BatteryStatus + { + get => _batteryInfo.BatteryStatus; + set => SetProperty(ref _batteryInfo.BatteryStatus, value); + } + + public PowerSupplyStatus PowerSupplyStatus + { + get => _batteryInfo.PowerSupplyStatus; + set => SetProperty(ref _batteryInfo.PowerSupplyStatus, value); + } + + public void UpdateStatus() + { + ChargePercent = PowerManager.RemainingChargePercent; + BatteryStatus = PowerManager.BatteryStatus; + PowerSupplyStatus = PowerManager.PowerSupplyStatus; + } + } +} diff --git a/src/Views/BatteryIcon.cs b/src/Views/BatteryIcon.cs index 3e43978..f75f20a 100644 --- a/src/Views/BatteryIcon.cs +++ b/src/Views/BatteryIcon.cs @@ -109,6 +109,8 @@ private set #endregion + internal readonly BatteryInfoWindow BatteryInfoWindow; + #region Private fields private static readonly Brush White = new SolidColorBrush(Color.FromArgb(255, 255, 255, 255)); @@ -133,6 +135,7 @@ public BatteryIcon(IAppNotificationService notificationService, ILogger + + + + + + + + + + + + + + + + + diff --git a/src/Views/BatteryInfoWindow.xaml.cs b/src/Views/BatteryInfoWindow.xaml.cs new file mode 100644 index 0000000..e4a3365 --- /dev/null +++ b/src/Views/BatteryInfoWindow.xaml.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices.WindowsRuntime; +using Windows.Foundation; +using Windows.Foundation.Collections; +using BatteryTracker.Models; +using BatteryTracker.ViewModels; +using CommunityToolkit.WinUI.UI; +using Microsoft.UI.Windowing; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Controls.Primitives; +using Microsoft.UI.Xaml.Data; +using Microsoft.UI.Xaml.Input; +using Microsoft.UI.Xaml.Media; +using Microsoft.UI.Xaml.Navigation; +using Microsoft.Windows.System.Power; +using WinUIEx; + +// To learn more about WinUI, the WinUI project structure, +// and more about our project templates, see: http://aka.ms/winui-project-info. + +namespace BatteryTracker.Views +{ + /// + /// An empty window that can be used on its own or navigated to within a Frame. + /// + public sealed partial class BatteryInfoWindow : WindowEx + { + public BatteryInfoViewModel ViewModel { get; } + + public BatteryInfoWindow() + { + ViewModel = new BatteryInfoViewModel(); + InitializeComponent(); + Activated += OnActivated; + Closed += BatteryInfoWindow_Closed; + } + + void BatteryInfoWindow_Closed(object sender, WindowEventArgs args) + { + args.Handled = true; + this.Hide(); + } + + void OnActivated(object sender, WindowActivatedEventArgs args) + { + switch (args.WindowActivationState) + { + // Hide the window on losing focus + case WindowActivationState.Deactivated: + // this.Hide(); + break; + // Fetch the latest battery info and display the window + case WindowActivationState.CodeActivated: + case WindowActivationState.PointerActivated: + ViewModel.UpdateStatus(); + BringToFront(); + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } +} From 11df249345479bd243f31203d4bf4cde9d7c3912 Mon Sep 17 00:00:00 2001 From: Frank Mao <61649477+myfix16@users.noreply.github.com> Date: Wed, 12 Jul 2023 01:49:08 +0800 Subject: [PATCH 2/6] Feature: Adjust battery info window's layout --- src/App.xaml | 3 +- src/App.xaml.cs | 1 + src/BatteryTracker.csproj | 8 +- src/Models/BatteryInfo.cs | 4 + src/Models/PowerPlan.cs | 44 ++++++++++ src/Styles/FontIcon.xaml | 4 + src/ViewModels/BatteryInfoViewModel.cs | 73 +++++++++++++++- src/Views/BatteryInfoWindow.xaml | 114 +++++++++++++++++++------ src/Views/BatteryInfoWindow.xaml.cs | 2 +- 9 files changed, 222 insertions(+), 31 deletions(-) create mode 100644 src/Models/PowerPlan.cs create mode 100644 src/Styles/FontIcon.xaml diff --git a/src/App.xaml b/src/App.xaml index e1c99f4..139fe57 100644 --- a/src/App.xaml +++ b/src/App.xaml @@ -12,9 +12,10 @@ - + + diff --git a/src/App.xaml.cs b/src/App.xaml.cs index 464142f..455cc3c 100644 --- a/src/App.xaml.cs +++ b/src/App.xaml.cs @@ -87,6 +87,7 @@ public App() services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); // Taskbar icon services.AddSingleton(); diff --git a/src/BatteryTracker.csproj b/src/BatteryTracker.csproj index be35a9c..c554feb 100644 --- a/src/BatteryTracker.csproj +++ b/src/BatteryTracker.csproj @@ -80,7 +80,6 @@ - @@ -99,10 +98,12 @@ + + @@ -145,10 +146,10 @@ $(DefaultXamlRuntime) - MSBuild:Compile + MSBuild:Compile @@ -180,6 +181,9 @@ $(DefaultXamlRuntime) + + $(DefaultXamlRuntime) + diff --git a/src/Models/BatteryInfo.cs b/src/Models/BatteryInfo.cs index 629077c..35674bd 100644 --- a/src/Models/BatteryInfo.cs +++ b/src/Models/BatteryInfo.cs @@ -8,4 +8,8 @@ public struct BatteryInfo public int ChargePercent; public BatteryStatus BatteryStatus; public PowerSupplyStatus PowerSupplyStatus; + public int DesignedCapacity; + public int MaxCapacity; + public int RemainingCapacity; + public int ChargingRate; } diff --git a/src/Models/PowerPlan.cs b/src/Models/PowerPlan.cs new file mode 100644 index 0000000..3b384a4 --- /dev/null +++ b/src/Models/PowerPlan.cs @@ -0,0 +1,44 @@ +/* + * Attribute should be given to the project FluentFlyouts3 + * https://github.com/FireCubeStudios/FluentFlyouts3 + */ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BatteryTracker.Models +{ + public record PowerPlan(PowerPlanEnum Plan, Guid Id); + + public enum PowerPlanEnum + { + PowerSaver, + Recommended, + BetterPerformance, + BestPerformance + } + + public struct PowerPlanIds + { + public const string PowerSaver = "a1841308-3541-4fab-bc81-f71556f20b4a"; + public const string Recommended = "381b4222-f694-41f0-9685-ff5bb260df2e"; + public const string BetterPerformance = "3af9B8d9-7c97-431d-ad78-34a8bfea439f"; + public const string BestPerformance = "8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c"; + public const string Recommended11 = "00000000-0000-0000-0000-000000000000"; + public const string BestPerformance11 = "ed574b5-45a0-4f42-8737-46345c09c238"; + } + + public class PowerMode + { + public static readonly PowerPlan PowerSaver = new(PowerPlanEnum.PowerSaver, new Guid(PowerPlanIds.PowerSaver)); + + public static readonly PowerPlan Recommended = new(PowerPlanEnum.Recommended, new Guid(PowerPlanIds.Recommended)); + + public static readonly PowerPlan BetterPerformance = new(PowerPlanEnum.BetterPerformance, new Guid(PowerPlanIds.BetterPerformance)); + + public static readonly PowerPlan BestPerformance = new(PowerPlanEnum.BestPerformance, new Guid(PowerPlanIds.BestPerformance)); + } +} diff --git a/src/Styles/FontIcon.xaml b/src/Styles/FontIcon.xaml new file mode 100644 index 0000000..1a5504d --- /dev/null +++ b/src/Styles/FontIcon.xaml @@ -0,0 +1,4 @@ + + + + diff --git a/src/ViewModels/BatteryInfoViewModel.cs b/src/ViewModels/BatteryInfoViewModel.cs index 5fcc10a..3fe6759 100644 --- a/src/ViewModels/BatteryInfoViewModel.cs +++ b/src/ViewModels/BatteryInfoViewModel.cs @@ -1,17 +1,28 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; using BatteryTracker.Models; using CommunityToolkit.Mvvm.ComponentModel; using Microsoft.Windows.System.Power; +using Windows.Devices.Power; +using Microsoft.Extensions.Logging; namespace BatteryTracker.ViewModels { public sealed class BatteryInfoViewModel : ObservableRecipient - { - private BatteryInfo _batteryInfo; + { + #region Private fields + + BatteryInfo _batteryInfo; + + readonly ILogger _logger; + + #endregion + + #region Properties public int ChargePercent { @@ -31,11 +42,69 @@ public PowerSupplyStatus PowerSupplyStatus set => SetProperty(ref _batteryInfo.PowerSupplyStatus, value); } + public int DesignedCapacity + { + get => _batteryInfo.DesignedCapacity; + set => SetProperty(ref _batteryInfo.DesignedCapacity, value); + } + + public int MaxCapacity + { + get => _batteryInfo.MaxCapacity; + set => SetProperty(ref _batteryInfo.MaxCapacity, value); + } + + public int RemainingCapacity + { + get => _batteryInfo.RemainingCapacity; + set => SetProperty(ref _batteryInfo.RemainingCapacity, value); + } + + public int ChargingRate + { + get => _batteryInfo.ChargingRate; + set => SetProperty(ref _batteryInfo.ChargingRate, value); + } + + #endregion + + public BatteryInfoViewModel(ILogger logger) + { + _logger = logger; + } + public void UpdateStatus() { ChargePercent = PowerManager.RemainingChargePercent; BatteryStatus = PowerManager.BatteryStatus; PowerSupplyStatus = PowerManager.PowerSupplyStatus; + + BatteryReport info = Battery.AggregateBattery.GetReport(); + if (info == null) + { + _logger.LogError("Failed to fetch battery report"); + } + else + { + DesignedCapacity = info.DesignCapacityInMilliwattHours!.Value; + MaxCapacity = info.FullChargeCapacityInMilliwattHours!.Value; + RemainingCapacity = info.RemainingCapacityInMilliwattHours!.Value; + ChargingRate = info.ChargeRateInMilliwatts!.Value; + } } + + #region Binding functions + + internal static string GetRemainingCapacityText(int capacity) + => $"{(double)capacity / 1000:0.##} Wh"; + + internal static string GetBatteryHealthText(int maxCapacity, int designedCapacity) + => $"{(double)maxCapacity * 100 / designedCapacity:0.#}% " + + $"({(double)maxCapacity / 1000:0.##} Wh / {(double)designedCapacity / 1000} Wh)"; + + internal static string GetChargingRateText(int rate, BatteryStatus status) + => $"{(double)rate / 1000:0.##} Wh ({status})"; + + #endregion } } diff --git a/src/Views/BatteryInfoWindow.xaml b/src/Views/BatteryInfoWindow.xaml index 1e7ec25..0c9707c 100644 --- a/src/Views/BatteryInfoWindow.xaml +++ b/src/Views/BatteryInfoWindow.xaml @@ -6,8 +6,9 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="using:BatteryTracker.Views" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:viewModels="using:BatteryTracker.ViewModels" xmlns:winUiEx="using:WinUIEx" - Width="500" Height="600" + Width="400" Height="300" IsTitleBarVisible="True" mc:Ignorable="d"> @@ -15,28 +16,91 @@ - - - - - - - - - - + + + + + 0.7 + 12 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/BatteryInfoWindow.xaml.cs b/src/Views/BatteryInfoWindow.xaml.cs index e4a3365..a2b6342 100644 --- a/src/Views/BatteryInfoWindow.xaml.cs +++ b/src/Views/BatteryInfoWindow.xaml.cs @@ -33,7 +33,7 @@ public sealed partial class BatteryInfoWindow : WindowEx public BatteryInfoWindow() { - ViewModel = new BatteryInfoViewModel(); + ViewModel = App.GetService(); InitializeComponent(); Activated += OnActivated; Closed += BatteryInfoWindow_Closed; From c013634903386a2b4adf9f94490518556326844e Mon Sep 17 00:00:00 2001 From: Frank Mao <61649477+myfix16@users.noreply.github.com> Date: Wed, 12 Jul 2023 19:06:30 +0800 Subject: [PATCH 3/6] Feature: Implement a slider to change power mode --- src/App.xaml.cs | 1 + src/BatteryTracker.csproj | 7 ++ src/Contracts/Services/IPowerService.cs | 10 ++ src/Converters/PowerModeToIndexConverter.cs | 19 ++++ src/Helpers/PowerHelper.cs | 21 ++++ src/Models/BatteryInfo.cs | 3 +- src/Models/Exceptions.cs | 6 ++ src/Models/PowerMode.cs | 55 ++++++++++ src/Models/PowerPlan.cs | 44 -------- src/Services/PowerService.cs | 37 +++++++ src/Services/SettingsService.cs | 1 + src/ViewModels/BatteryInfoViewModel.cs | 41 +++++--- src/ViewModels/SettingsViewModel.cs | 1 + src/Views/BatteryInfoPage.xaml | 108 ++++++++++++++++++++ src/Views/BatteryInfoPage.xaml.cs | 15 +++ src/Views/BatteryInfoWindow.xaml | 89 +--------------- src/Views/BatteryInfoWindow.xaml.cs | 26 +---- src/Views/SettingsPage.xaml.cs | 3 +- 18 files changed, 313 insertions(+), 174 deletions(-) create mode 100644 src/Contracts/Services/IPowerService.cs create mode 100644 src/Converters/PowerModeToIndexConverter.cs create mode 100644 src/Helpers/PowerHelper.cs create mode 100644 src/Models/Exceptions.cs create mode 100644 src/Models/PowerMode.cs delete mode 100644 src/Models/PowerPlan.cs create mode 100644 src/Services/PowerService.cs create mode 100644 src/Views/BatteryInfoPage.xaml create mode 100644 src/Views/BatteryInfoPage.xaml.cs diff --git a/src/App.xaml.cs b/src/App.xaml.cs index 455cc3c..e50b26c 100644 --- a/src/App.xaml.cs +++ b/src/App.xaml.cs @@ -79,6 +79,7 @@ public App() services.AddSingleton(); services.AddSingleton(); services.AddTransient(); + services.AddTransient(); // Views and ViewModels services.AddTransient(); diff --git a/src/BatteryTracker.csproj b/src/BatteryTracker.csproj index c554feb..2f67b24 100644 --- a/src/BatteryTracker.csproj +++ b/src/BatteryTracker.csproj @@ -104,6 +104,7 @@ + @@ -150,6 +151,9 @@ MSBuild:Compile + + MSBuild:Compile + MSBuild:Compile @@ -165,6 +169,9 @@ $(DefaultXamlRuntime) + + $(DefaultXamlRuntime) + diff --git a/src/Contracts/Services/IPowerService.cs b/src/Contracts/Services/IPowerService.cs new file mode 100644 index 0000000..9b56953 --- /dev/null +++ b/src/Contracts/Services/IPowerService.cs @@ -0,0 +1,10 @@ +using BatteryTracker.Models; + +namespace BatteryTracker.Contracts.Services; + +public interface IPowerService +{ + PowerMode GetPowerMode(); + + void SetPowerMode(PowerMode powerMode); +} \ No newline at end of file diff --git a/src/Converters/PowerModeToIndexConverter.cs b/src/Converters/PowerModeToIndexConverter.cs new file mode 100644 index 0000000..831e265 --- /dev/null +++ b/src/Converters/PowerModeToIndexConverter.cs @@ -0,0 +1,19 @@ +using BatteryTracker.Models; +using Microsoft.UI.Xaml.Data; + +namespace BatteryTracker.Converters; + +public class PowerModeToIndexConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, string language) + => (double)((PowerMode)value).Mode; + + public object ConvertBack(object value, Type targetType, object parameter, string language) + => (double)value switch + { + 0 => PowerMode.BetterBattery, + 1 => PowerMode.Balanced, + 2 => PowerMode.BestPerformance, + _ => PowerMode.Balanced + }; +} diff --git a/src/Helpers/PowerHelper.cs b/src/Helpers/PowerHelper.cs new file mode 100644 index 0000000..aa8b4b6 --- /dev/null +++ b/src/Helpers/PowerHelper.cs @@ -0,0 +1,21 @@ +/* + * Attribute should be given to this StackOverflow post: + * https://stackoverflow.com/questions/61869347/control-windows-10s-power-mode-programmatically + */ + +using System.Runtime.InteropServices; + +namespace BatteryTracker.Helpers +{ + internal static class PowerHelper + { + [DllImport("powrprof.dll", EntryPoint = "PowerSetActiveOverlayScheme")] + internal static extern uint PowerSetActiveOverlayScheme(Guid OverlaySchemeGuid); + + [DllImport("powrprof.dll", EntryPoint = "PowerGetActualOverlayScheme")] + internal static extern uint PowerGetActualOverlayScheme(out Guid ActualOverlayGuid); + + [DllImport("powrprof.dll", EntryPoint = "PowerGetEffectiveOverlayScheme")] + internal static extern uint PowerGetEffectiveOverlayScheme(out Guid EffectiveOverlayGuid); + } +} diff --git a/src/Models/BatteryInfo.cs b/src/Models/BatteryInfo.cs index 35674bd..f43abbc 100644 --- a/src/Models/BatteryInfo.cs +++ b/src/Models/BatteryInfo.cs @@ -1,5 +1,4 @@ -using CommunityToolkit.Mvvm.ComponentModel; -using Microsoft.Windows.System.Power; +using Microsoft.Windows.System.Power; namespace BatteryTracker.Models; diff --git a/src/Models/Exceptions.cs b/src/Models/Exceptions.cs new file mode 100644 index 0000000..ee3d5ca --- /dev/null +++ b/src/Models/Exceptions.cs @@ -0,0 +1,6 @@ +namespace BatteryTracker.Models; + +public sealed class RuntimeException : ApplicationException +{ + public RuntimeException(string message) : base(message) { } +} \ No newline at end of file diff --git a/src/Models/PowerMode.cs b/src/Models/PowerMode.cs new file mode 100644 index 0000000..8586bc2 --- /dev/null +++ b/src/Models/PowerMode.cs @@ -0,0 +1,55 @@ +/* + * Attribute should be given to FluentFlyouts3: + * https://github.com/FireCubeStudios/FluentFlyouts3 + */ + +namespace BatteryTracker.Models; + +public enum PowerModeEnum +{ + BetterBattery = 0, + Balanced = 1, + BestPerformance = 2 +} + +public static class PowerModeIds +{ + public static readonly Guid BetterBattery = new("961cc777-2547-4f9d-8174-7d86181b8a7a"); + public static readonly Guid Balanced = new("00000000-0000-0000-0000-000000000000"); + public static readonly Guid BestPerformance = new("ded574b5-45a0-4f42-8737-46345c09c238"); +} + +public sealed record PowerMode(PowerModeEnum Mode, Guid Guid) +{ + public static readonly PowerMode BetterBattery = + new(PowerModeEnum.BetterBattery, PowerModeIds.BetterBattery); + + public static readonly PowerMode Balanced = + new(PowerModeEnum.Balanced, PowerModeIds.Balanced); + + public static readonly PowerMode BestPerformance = + new(PowerModeEnum.BestPerformance, PowerModeIds.BestPerformance); + + public static PowerMode GuidToPowerMode(Guid guid) + { + if (guid == PowerModeIds.BetterBattery) + { + return BetterBattery; + } + if (guid == PowerModeIds.Balanced) + { + return Balanced; + } + if (guid == PowerModeIds.BestPerformance) + { + return BestPerformance; + } + + throw new ArgumentOutOfRangeException($"Power mode guid {guid} is invalid!"); + } + + public override string ToString() + { + return $"{Mode} {Guid}"; + } +} diff --git a/src/Models/PowerPlan.cs b/src/Models/PowerPlan.cs deleted file mode 100644 index 3b384a4..0000000 --- a/src/Models/PowerPlan.cs +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Attribute should be given to the project FluentFlyouts3 - * https://github.com/FireCubeStudios/FluentFlyouts3 - */ - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace BatteryTracker.Models -{ - public record PowerPlan(PowerPlanEnum Plan, Guid Id); - - public enum PowerPlanEnum - { - PowerSaver, - Recommended, - BetterPerformance, - BestPerformance - } - - public struct PowerPlanIds - { - public const string PowerSaver = "a1841308-3541-4fab-bc81-f71556f20b4a"; - public const string Recommended = "381b4222-f694-41f0-9685-ff5bb260df2e"; - public const string BetterPerformance = "3af9B8d9-7c97-431d-ad78-34a8bfea439f"; - public const string BestPerformance = "8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c"; - public const string Recommended11 = "00000000-0000-0000-0000-000000000000"; - public const string BestPerformance11 = "ed574b5-45a0-4f42-8737-46345c09c238"; - } - - public class PowerMode - { - public static readonly PowerPlan PowerSaver = new(PowerPlanEnum.PowerSaver, new Guid(PowerPlanIds.PowerSaver)); - - public static readonly PowerPlan Recommended = new(PowerPlanEnum.Recommended, new Guid(PowerPlanIds.Recommended)); - - public static readonly PowerPlan BetterPerformance = new(PowerPlanEnum.BetterPerformance, new Guid(PowerPlanIds.BetterPerformance)); - - public static readonly PowerPlan BestPerformance = new(PowerPlanEnum.BestPerformance, new Guid(PowerPlanIds.BestPerformance)); - } -} diff --git a/src/Services/PowerService.cs b/src/Services/PowerService.cs new file mode 100644 index 0000000..db06267 --- /dev/null +++ b/src/Services/PowerService.cs @@ -0,0 +1,37 @@ +using BatteryTracker.Contracts.Services; +using BatteryTracker.Helpers; +using BatteryTracker.Models; +using Microsoft.Extensions.Logging; + +namespace BatteryTracker.Services; + +public sealed class PowerService : IPowerService +{ + readonly ILogger _logger; + + public PowerService(ILogger logger) + { + _logger = logger; + } + + public PowerMode GetPowerMode() + { + uint result = PowerHelper.PowerGetActualOverlayScheme(out Guid powerModeId); + if (result != 0) + { + _logger.LogError("Failed to fetch current power mode"); + throw new RuntimeException("Failed to fetch current power mode"); + } + return PowerMode.GuidToPowerMode(powerModeId); + } + + public void SetPowerMode(PowerMode powerMode) + { + uint result = PowerHelper.PowerSetActiveOverlayScheme(powerMode.Guid); + if (result != 0) + { + _logger.LogError("Failed to set current power mode: {powerModeId}", powerMode); + throw new RuntimeException($"Failed to set current power mode: {powerMode}"); + } + } +} diff --git a/src/Services/SettingsService.cs b/src/Services/SettingsService.cs index b0fa8e4..60e2bdb 100644 --- a/src/Services/SettingsService.cs +++ b/src/Services/SettingsService.cs @@ -146,6 +146,7 @@ public SettingsService(ISettingsStorageService settingsStorageService, ILogger(SettingVersionSettingsKey, null); diff --git a/src/ViewModels/BatteryInfoViewModel.cs b/src/ViewModels/BatteryInfoViewModel.cs index 3fe6759..ba03b72 100644 --- a/src/ViewModels/BatteryInfoViewModel.cs +++ b/src/ViewModels/BatteryInfoViewModel.cs @@ -1,14 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Cryptography; -using System.Text; -using System.Threading.Tasks; +using BatteryTracker.Contracts.Services; using BatteryTracker.Models; using CommunityToolkit.Mvvm.ComponentModel; +using Microsoft.Extensions.Logging; using Microsoft.Windows.System.Power; using Windows.Devices.Power; -using Microsoft.Extensions.Logging; namespace BatteryTracker.ViewModels { @@ -17,8 +12,10 @@ public sealed class BatteryInfoViewModel : ObservableRecipient #region Private fields BatteryInfo _batteryInfo; + PowerMode _powerMode; readonly ILogger _logger; + readonly IPowerService _powerService; #endregion @@ -33,7 +30,7 @@ public int ChargePercent public BatteryStatus BatteryStatus { get => _batteryInfo.BatteryStatus; - set => SetProperty(ref _batteryInfo.BatteryStatus, value); + private set => SetProperty(ref _batteryInfo.BatteryStatus, value); } public PowerSupplyStatus PowerSupplyStatus @@ -45,36 +42,49 @@ public PowerSupplyStatus PowerSupplyStatus public int DesignedCapacity { get => _batteryInfo.DesignedCapacity; - set => SetProperty(ref _batteryInfo.DesignedCapacity, value); + private set => SetProperty(ref _batteryInfo.DesignedCapacity, value); } public int MaxCapacity { get => _batteryInfo.MaxCapacity; - set => SetProperty(ref _batteryInfo.MaxCapacity, value); + private set => SetProperty(ref _batteryInfo.MaxCapacity, value); } public int RemainingCapacity { get => _batteryInfo.RemainingCapacity; - set => SetProperty(ref _batteryInfo.RemainingCapacity, value); + private set => SetProperty(ref _batteryInfo.RemainingCapacity, value); } public int ChargingRate { get => _batteryInfo.ChargingRate; - set => SetProperty(ref _batteryInfo.ChargingRate, value); + private set => SetProperty(ref _batteryInfo.ChargingRate, value); + } + + public PowerMode PowerMode + { + get => _powerMode; + set + { + _powerService.SetPowerMode(value); + SetProperty(ref _powerMode, value); + } } #endregion - public BatteryInfoViewModel(ILogger logger) + public BatteryInfoViewModel(ILogger logger, IPowerService powerService) { _logger = logger; + _powerService = powerService; + _powerMode = PowerMode.Balanced; } public void UpdateStatus() { + // Update battery info ChargePercent = PowerManager.RemainingChargePercent; BatteryStatus = PowerManager.BatteryStatus; PowerSupplyStatus = PowerManager.PowerSupplyStatus; @@ -91,6 +101,9 @@ public void UpdateStatus() RemainingCapacity = info.RemainingCapacityInMilliwattHours!.Value; ChargingRate = info.ChargeRateInMilliwatts!.Value; } + + // Update power mode + PowerMode = _powerService.GetPowerMode(); } #region Binding functions @@ -102,7 +115,7 @@ internal static string GetBatteryHealthText(int maxCapacity, int designedCapacit => $"{(double)maxCapacity * 100 / designedCapacity:0.#}% " + $"({(double)maxCapacity / 1000:0.##} Wh / {(double)designedCapacity / 1000} Wh)"; - internal static string GetChargingRateText(int rate, BatteryStatus status) + internal static string GetChargingRateText(int rate, BatteryStatus status) => $"{(double)rate / 1000:0.##} Wh ({status})"; #endregion diff --git a/src/ViewModels/SettingsViewModel.cs b/src/ViewModels/SettingsViewModel.cs index c6d5a67..84417aa 100644 --- a/src/ViewModels/SettingsViewModel.cs +++ b/src/ViewModels/SettingsViewModel.cs @@ -228,6 +228,7 @@ public SettingsViewModel(BatteryIcon icon, IThemeSelectorService themeSelectorSe }); _appLanguageId = _settingsService.Language.LanguageId; + _language = Languages[0]; // dummy line to disable compiler warning ReadSettingValues(); LanguageChanged = false; diff --git a/src/Views/BatteryInfoPage.xaml b/src/Views/BatteryInfoPage.xaml new file mode 100644 index 0000000..aa36ef6 --- /dev/null +++ b/src/Views/BatteryInfoPage.xaml @@ -0,0 +1,108 @@ + + + + + + + + 0.7 + 12 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/BatteryInfoPage.xaml.cs b/src/Views/BatteryInfoPage.xaml.cs new file mode 100644 index 0000000..f1c5da3 --- /dev/null +++ b/src/Views/BatteryInfoPage.xaml.cs @@ -0,0 +1,15 @@ +using BatteryTracker.ViewModels; +using Microsoft.UI.Xaml.Controls; + +namespace BatteryTracker.Views; + +public sealed partial class BatteryInfoPage : Page +{ + public BatteryInfoViewModel ViewModel { get; } + + public BatteryInfoPage() + { + ViewModel = App.GetService(); + InitializeComponent(); + } +} diff --git a/src/Views/BatteryInfoWindow.xaml b/src/Views/BatteryInfoWindow.xaml index 0c9707c..6f9d9bc 100644 --- a/src/Views/BatteryInfoWindow.xaml +++ b/src/Views/BatteryInfoWindow.xaml @@ -6,7 +6,6 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="using:BatteryTracker.Views" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:viewModels="using:BatteryTracker.ViewModels" xmlns:winUiEx="using:WinUIEx" Width="400" Height="300" IsTitleBarVisible="True" @@ -16,91 +15,5 @@ - - - - - 0.7 - 12 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + diff --git a/src/Views/BatteryInfoWindow.xaml.cs b/src/Views/BatteryInfoWindow.xaml.cs index a2b6342..b4bb265 100644 --- a/src/Views/BatteryInfoWindow.xaml.cs +++ b/src/Views/BatteryInfoWindow.xaml.cs @@ -1,22 +1,3 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Runtime.InteropServices.WindowsRuntime; -using Windows.Foundation; -using Windows.Foundation.Collections; -using BatteryTracker.Models; -using BatteryTracker.ViewModels; -using CommunityToolkit.WinUI.UI; -using Microsoft.UI.Windowing; -using Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Controls; -using Microsoft.UI.Xaml.Controls.Primitives; -using Microsoft.UI.Xaml.Data; -using Microsoft.UI.Xaml.Input; -using Microsoft.UI.Xaml.Media; -using Microsoft.UI.Xaml.Navigation; -using Microsoft.Windows.System.Power; using WinUIEx; // To learn more about WinUI, the WinUI project structure, @@ -29,11 +10,8 @@ namespace BatteryTracker.Views /// public sealed partial class BatteryInfoWindow : WindowEx { - public BatteryInfoViewModel ViewModel { get; } - public BatteryInfoWindow() { - ViewModel = App.GetService(); InitializeComponent(); Activated += OnActivated; Closed += BatteryInfoWindow_Closed; @@ -56,11 +34,11 @@ void OnActivated(object sender, WindowActivatedEventArgs args) // Fetch the latest battery info and display the window case WindowActivationState.CodeActivated: case WindowActivationState.PointerActivated: - ViewModel.UpdateStatus(); + BatteryInfoPage.ViewModel.UpdateStatus(); BringToFront(); break; default: - throw new ArgumentOutOfRangeException(); + throw new ArgumentOutOfRangeException(nameof(args), "Invalid WindowActivationState"); } } } diff --git a/src/Views/SettingsPage.xaml.cs b/src/Views/SettingsPage.xaml.cs index 13b24b5..74c4e41 100644 --- a/src/Views/SettingsPage.xaml.cs +++ b/src/Views/SettingsPage.xaml.cs @@ -1,5 +1,4 @@ -using BatteryTracker.Helpers; -using BatteryTracker.ViewModels; +using BatteryTracker.ViewModels; using Microsoft.UI.Xaml.Controls; namespace BatteryTracker.Views; From 4ee1f90c77d098e340037e48cf0cb69e45d62443 Mon Sep 17 00:00:00 2001 From: Frank Mao <61649477+myfix16@users.noreply.github.com> Date: Wed, 12 Jul 2023 21:22:33 +0800 Subject: [PATCH 4/6] Feature: Enable content refreshing on the battery info window --- src/App.xaml.cs | 10 +++- src/Helpers/Timer.cs | 47 ++++++++++++++++++ src/Models/PowerMode.cs | 2 +- src/ViewModels/BatteryInfoViewModel.cs | 69 ++++++++++++++++++-------- src/Views/BatteryInfoPage.xaml | 52 +++++++++++-------- src/Views/BatteryInfoWindow.xaml.cs | 7 ++- 6 files changed, 141 insertions(+), 46 deletions(-) create mode 100644 src/Helpers/Timer.cs diff --git a/src/App.xaml.cs b/src/App.xaml.cs index e50b26c..f0aafdf 100644 --- a/src/App.xaml.cs +++ b/src/App.xaml.cs @@ -204,7 +204,15 @@ private async Task InitializeTrayIconAsync() void DisplayBatteryInfoCommand_ExecuteRequested(XamlUICommand sender, ExecuteRequestedEventArgs args) { - _batteryIcon!.BatteryInfoWindow.Activate(); + BatteryInfoWindow window = _batteryIcon!.BatteryInfoWindow; + if (window.Visible) + { + window.Hide(); + } + else + { + window.Activate(); + } } private void ProcessUnhandledException(Exception? e, bool showNotification) diff --git a/src/Helpers/Timer.cs b/src/Helpers/Timer.cs new file mode 100644 index 0000000..903929d --- /dev/null +++ b/src/Helpers/Timer.cs @@ -0,0 +1,47 @@ +using System.Threading; + +namespace BatteryTracker.Helpers; + +class Timer +{ + public Action? Action { get; set; } + + System.Threading.Timer? _timer; + + int _interval; // in milliseconds + bool _continue = true; + + public async void StartTimer(int interval) + { + _continue = true; + _interval = interval; + if (_timer != null) + { + await _timer.DisposeAsync(); + } + _timer = new System.Threading.Timer(Tick, null, interval, Timeout.Infinite); + } + + public void StopTimer() => _continue = false; + + void Tick(object state) + { + try + { + Action?.Invoke(); + } + finally + { + if (_continue) + { + _timer?.Change(_interval, Timeout.Infinite); + } + } + } + + ~Timer() + { + _timer?.Dispose(); + } +} + diff --git a/src/Models/PowerMode.cs b/src/Models/PowerMode.cs index 8586bc2..1d7e31a 100644 --- a/src/Models/PowerMode.cs +++ b/src/Models/PowerMode.cs @@ -50,6 +50,6 @@ public static PowerMode GuidToPowerMode(Guid guid) public override string ToString() { - return $"{Mode} {Guid}"; + return $"{Mode}"; } } diff --git a/src/ViewModels/BatteryInfoViewModel.cs b/src/ViewModels/BatteryInfoViewModel.cs index ba03b72..1ff4a16 100644 --- a/src/ViewModels/BatteryInfoViewModel.cs +++ b/src/ViewModels/BatteryInfoViewModel.cs @@ -4,6 +4,9 @@ using Microsoft.Extensions.Logging; using Microsoft.Windows.System.Power; using Windows.Devices.Power; +using BatteryTracker.Helpers; +using CommunityToolkit.WinUI; +using Microsoft.UI.Dispatching; namespace BatteryTracker.ViewModels { @@ -11,12 +14,17 @@ public sealed class BatteryInfoViewModel : ObservableRecipient { #region Private fields + readonly DispatcherQueue _dispatcher = DispatcherQueue.GetForCurrentThread(); + BatteryInfo _batteryInfo; PowerMode _powerMode; + int _refreshInterval = 5000; // todo: allow customizing refresh interval in settings readonly ILogger _logger; readonly IPowerService _powerService; + readonly Timer _timer; + #endregion #region Properties @@ -80,30 +88,47 @@ public BatteryInfoViewModel(ILogger logger, IPowerService _logger = logger; _powerService = powerService; _powerMode = PowerMode.Balanced; + PowerMode = _powerService.GetPowerMode(); + + _timer = new Timer(); + _timer.Action += UpdateStatus; } - public void UpdateStatus() + ~BatteryInfoViewModel() { - // Update battery info - ChargePercent = PowerManager.RemainingChargePercent; - BatteryStatus = PowerManager.BatteryStatus; - PowerSupplyStatus = PowerManager.PowerSupplyStatus; + _timer.Action -= UpdateStatus; + } - BatteryReport info = Battery.AggregateBattery.GetReport(); - if (info == null) - { - _logger.LogError("Failed to fetch battery report"); - } - else - { - DesignedCapacity = info.DesignCapacityInMilliwattHours!.Value; - MaxCapacity = info.FullChargeCapacityInMilliwattHours!.Value; - RemainingCapacity = info.RemainingCapacityInMilliwattHours!.Value; - ChargingRate = info.ChargeRateInMilliwatts!.Value; - } + public void StartUpdatingStatus() + { + UpdateStatus(); + _timer.StartTimer(_refreshInterval); + } - // Update power mode - PowerMode = _powerService.GetPowerMode(); + public void StopUpdatingStatus() => _timer.StopTimer(); + + void UpdateStatus() + { + _dispatcher.TryEnqueue(() => + { + // Update battery info + ChargePercent = PowerManager.RemainingChargePercent; + BatteryStatus = PowerManager.BatteryStatus; + PowerSupplyStatus = PowerManager.PowerSupplyStatus; + + BatteryReport info = Battery.AggregateBattery.GetReport(); + if (info == null) + { + _logger.LogError("Failed to fetch battery report"); + } + else + { + DesignedCapacity = info.DesignCapacityInMilliwattHours!.Value; + MaxCapacity = info.FullChargeCapacityInMilliwattHours!.Value; + RemainingCapacity = info.RemainingCapacityInMilliwattHours!.Value; + ChargingRate = info.ChargeRateInMilliwatts!.Value; + } + }); } #region Binding functions @@ -116,7 +141,11 @@ internal static string GetBatteryHealthText(int maxCapacity, int designedCapacit $"({(double)maxCapacity / 1000:0.##} Wh / {(double)designedCapacity / 1000} Wh)"; internal static string GetChargingRateText(int rate, BatteryStatus status) - => $"{(double)rate / 1000:0.##} Wh ({status})"; + => $"{(double)rate / 1000:0.##} W ({status})"; + + // todo: localize this text + internal static string GetPowerModeText(PowerMode powerMode) + => $"Power Mode: {powerMode}"; #endregion } diff --git a/src/Views/BatteryInfoPage.xaml b/src/Views/BatteryInfoPage.xaml index aa36ef6..d988bef 100644 --- a/src/Views/BatteryInfoPage.xaml +++ b/src/Views/BatteryInfoPage.xaml @@ -8,6 +8,7 @@ xmlns:local="using:BatteryTracker.Views" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:viewModels="using:BatteryTracker.ViewModels" + Padding="0" mc:Ignorable="d"> + ColumnDefinitions="150, *" RowDefinitions="30, 30, 30, 30, 30, 20, 26"> + FontWeight="SemiBold" Text="Current capacity:" /> @@ -50,7 +53,7 @@ + FontWeight="SemiBold" Text="Charging rate:" /> @@ -58,7 +61,7 @@ + FontWeight="SemiBold" Text="Battery health:" /> @@ -66,31 +69,36 @@ + FontWeight="SemiBold" Text="Power mode:" /> + - - - - - + + + - - - + Style="{StaticResource TipTextStyle}" + Text="Best performance" /> diff --git a/src/Views/BatteryInfoWindow.xaml.cs b/src/Views/BatteryInfoWindow.xaml.cs index b4bb265..e036f34 100644 --- a/src/Views/BatteryInfoWindow.xaml.cs +++ b/src/Views/BatteryInfoWindow.xaml.cs @@ -29,17 +29,20 @@ void OnActivated(object sender, WindowActivatedEventArgs args) { // Hide the window on losing focus case WindowActivationState.Deactivated: - // this.Hide(); + BatteryInfoPage.ViewModel.StopUpdatingStatus(); + this.Hide(); break; // Fetch the latest battery info and display the window case WindowActivationState.CodeActivated: case WindowActivationState.PointerActivated: - BatteryInfoPage.ViewModel.UpdateStatus(); + BatteryInfoPage.ViewModel.StartUpdatingStatus(); BringToFront(); break; default: throw new ArgumentOutOfRangeException(nameof(args), "Invalid WindowActivationState"); } } + + } } From 6c4c44bca81b6503749651064aaea9f78bfae80f Mon Sep 17 00:00:00 2001 From: Frank Mao Date: Thu, 14 Dec 2023 16:41:46 -0500 Subject: [PATCH 5/6] Dependency: Update Nuget packages --- src/BatteryTracker.csproj | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/BatteryTracker.csproj b/src/BatteryTracker.csproj index 2f67b24..a34e87d 100644 --- a/src/BatteryTracker.csproj +++ b/src/BatteryTracker.csproj @@ -110,17 +110,17 @@ - + - - - - - + + + + + - + From c46a069f2c0bf8951071c11ed07c124532be923f Mon Sep 17 00:00:00 2001 From: Frank Mao Date: Thu, 14 Dec 2023 16:59:06 -0500 Subject: [PATCH 6/6] Project: Migrate to .NET 8 --- .../BatteryTracker.Tests.UnitTests.csproj | 3 ++- src/BatteryTracker.csproj | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Tests/BatteryTracker.Tests.UnitTests/BatteryTracker.Tests.UnitTests.csproj b/Tests/BatteryTracker.Tests.UnitTests/BatteryTracker.Tests.UnitTests.csproj index 99e8e3d..9e591ef 100644 --- a/Tests/BatteryTracker.Tests.UnitTests/BatteryTracker.Tests.UnitTests.csproj +++ b/Tests/BatteryTracker.Tests.UnitTests/BatteryTracker.Tests.UnitTests.csproj @@ -1,9 +1,10 @@ - net7.0-windows10.0.22000.0 + net8.0-windows10.0.22000.0 BatteryTracker.Tests.UnitTests x86;x64;arm64 + win-x86;win-x64;win-arm64 false enable enable diff --git a/src/BatteryTracker.csproj b/src/BatteryTracker.csproj index a34e87d..a83ef00 100644 --- a/src/BatteryTracker.csproj +++ b/src/BatteryTracker.csproj @@ -1,12 +1,12 @@ WinExe - net7.0-windows10.0.22000.0 + net8.0-windows10.0.22000.0 10.0.17763.0 BatteryTracker app.manifest x86;x64;ARM64 - win10-x86;win10-x64;win10-arm64 + win-x86;win-x64;win-arm64 win10-$(Platform).pubxml true true