diff --git a/LibreHardwareMonitor.Windows.WinUI/App.xaml b/LibreHardwareMonitor.Windows.WinUI/App.xaml new file mode 100644 index 0000000..ea5ef48 --- /dev/null +++ b/LibreHardwareMonitor.Windows.WinUI/App.xaml @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/LibreHardwareMonitor.Windows.WinUI/App.xaml.cs b/LibreHardwareMonitor.Windows.WinUI/App.xaml.cs new file mode 100644 index 0000000..a22a639 --- /dev/null +++ b/LibreHardwareMonitor.Windows.WinUI/App.xaml.cs @@ -0,0 +1,79 @@ +// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +// Copyright (C) LibreHardwareMonitor and Contributors. + +using System; +using System.IO; +using System.Threading.Tasks; +using Microsoft.UI.Xaml; +using IOPath = System.IO.Path; + +namespace LibreHardwareMonitor.Windows.WinUI; + +public partial class App : Application +{ + private bool _launchStarted; + private Window? _window; + + public App() + { + UnhandledException += App_UnhandledException; + AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; + TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException; + InitializeComponent(); + } + + protected override void OnLaunched(LaunchActivatedEventArgs args) + { + LaunchMainWindow(); + } + + private void LaunchMainWindow() + { + if (_launchStarted) + return; + + _launchStarted = true; + try + { + MainWindow mainWindow = new(); + _window = mainWindow; + _window.Activate(); + mainWindow.StartMonitoringAfterActivation(); + } + catch (Exception ex) + { + WriteExceptionLog("Window launch failed", ex); + throw; + } + } + + private static void App_UnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs args) + { + WriteExceptionLog("WinUI unhandled exception", args.Exception); + } + + private static void CurrentDomain_UnhandledException(object sender, System.UnhandledExceptionEventArgs args) + { + if (args.ExceptionObject is Exception exception) + WriteExceptionLog("AppDomain unhandled exception", exception); + else + WriteMessageLog($"AppDomain unhandled exception: {args.ExceptionObject}"); + } + + private static void TaskScheduler_UnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs args) + { + WriteExceptionLog("Unobserved task exception", args.Exception); + } + + private static void WriteExceptionLog(string context, Exception exception) + { + WriteMessageLog($"{context}{Environment.NewLine}{exception}"); + } + + private static void WriteMessageLog(string message) + { + string logPath = IOPath.Combine(AppContext.BaseDirectory, "LibreHardwareMonitor.Windows.WinUI.startup.log"); + File.AppendAllText(logPath, $"{DateTimeOffset.Now:u} {message}{Environment.NewLine}{Environment.NewLine}"); + } +} diff --git a/LibreHardwareMonitor.Windows.WinUI/LibreHardwareMonitor.Windows.WinUI.csproj b/LibreHardwareMonitor.Windows.WinUI/LibreHardwareMonitor.Windows.WinUI.csproj new file mode 100644 index 0000000..67bdc87 --- /dev/null +++ b/LibreHardwareMonitor.Windows.WinUI/LibreHardwareMonitor.Windows.WinUI.csproj @@ -0,0 +1,35 @@ + + + WinExe + net10.0-windows10.0.19041.0 + 10.0.19041.0 + x64;x86;ARM64 + win-x64;win-x86;win-arm64 + LibreHardwareMonitor.Windows.WinUI + Libre Hardware Monitor + LibreHardwareMonitor.Windows.WinUI + ..\LibreHardwareMonitor.Windows.Forms\Resources\icon.ico + app.manifest + true + None + true + ..\bin\$(Configuration)\ + latest + enable + + + + + + + + + + + + + + + + + diff --git a/LibreHardwareMonitor.Windows.WinUI/MainWindow.cs b/LibreHardwareMonitor.Windows.WinUI/MainWindow.cs new file mode 100644 index 0000000..84da256 --- /dev/null +++ b/LibreHardwareMonitor.Windows.WinUI/MainWindow.cs @@ -0,0 +1,1239 @@ +// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +// Copyright (C) LibreHardwareMonitor and Contributors. + +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Threading.Tasks; +using LibreHardwareMonitor.Hardware; +using LibreHardwareMonitor.Windows.WinUI.Services; +using LibreHardwareMonitor.Windows.WinUI.ViewModels; +using Microsoft.UI; +using Microsoft.UI.Dispatching; +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.Markup; +using Microsoft.UI.Xaml.Media; +using Microsoft.UI.Xaml.Shapes; +using Windows.Graphics; +using Windows.Storage; +using Windows.Storage.Pickers; +using WinRT.Interop; +using IOPath = System.IO.Path; + +namespace LibreHardwareMonitor.Windows.WinUI; + +public sealed class MainWindow : Window +{ + private const double SensorColumnPadding = 72; + private const double SensorTreeIndentWidth = 20; + private const double ValueColumnPadding = 18; + + private readonly AppWindow _appWindow; + private readonly Grid _contentGrid; + private readonly DispatcherQueueTimer _timer; + private readonly Canvas _plotCanvas; + private readonly Grid _plotPane; + private readonly Grid _rootGrid; + private readonly TreeView _sensorTree; + private readonly Grid _sensorPane; + private readonly List _sensorRowGrids = []; + private readonly double[] _sensorColumnWidths = [320, 120, 120, 120]; + private Grid? _sensorHeader; + private bool _isUpdating; + private bool _isMonitoringStarted; + + public MainWindow() + { + AppSettings settings = AppSettings.LoadDefault(); + ViewModel = new MainWindowViewModel(settings); + + IntPtr hwnd = WindowNative.GetWindowHandle(this); + WindowId windowId = Win32Interop.GetWindowIdFromWindow(hwnd); + _appWindow = AppWindow.GetFromWindowId(windowId); + _appWindow.Title = "Libre Hardware Monitor"; + + Grid root = BuildRoot(); + Content = root; + _rootGrid = root; + + _contentGrid = (Grid)root.Children[1]; + _sensorPane = (Grid)_contentGrid.Children[0]; + _plotPane = (Grid)_contentGrid.Children[1]; + _sensorTree = (TreeView)((Grid)_sensorPane.Children[1]).Children[0]; + _plotCanvas = (Canvas)_plotPane.Children[1]; + + RestoreWindowBounds(); + MaximizeWindow(); + ApplyTheme(); + UpdatePlotLayout(); + + _timer = DispatcherQueue.CreateTimer(); + _timer.Interval = ViewModel.UpdateInterval; + _timer.Tick += UpdateTimer_Tick; + + ViewModel.PropertyChanged += (_, args) => + { + if (args.PropertyName is nameof(ViewModel.UpdateInterval) or nameof(ViewModel.UpdateIntervalIndex)) + _timer.Interval = ViewModel.UpdateInterval; + + if (args.PropertyName is nameof(ViewModel.ShowPlot) + or nameof(ViewModel.PlotLocation) + or nameof(ViewModel.PlotGridColumn) + or nameof(ViewModel.PlotGridRow)) + UpdatePlotLayout(); + + if (args.PropertyName == nameof(ViewModel.ShowHiddenSensors)) + RebuildSensorTree(); + + if (args.PropertyName is nameof(ViewModel.ShowValueColumn) + or nameof(ViewModel.ShowMinColumn) + or nameof(ViewModel.ShowMaxColumn)) + UpdateSensorColumnWidths(); + + if (args.PropertyName == nameof(ViewModel.ThemeMode)) + ApplyTheme(); + }; + ViewModel.RootItems.CollectionChanged += RootItems_CollectionChanged; + ViewModel.PlotInvalidated += (_, _) => DrawPlot(); + + Closed += MainWindow_Closed; + + } + + public MainWindowViewModel ViewModel { get; } + + public void StartMonitoringAfterActivation() + { + DispatcherQueue.TryEnqueue(async () => + { + if (_isMonitoringStarted) + return; + + _isMonitoringStarted = true; + try + { + await Task.Delay(250); + await ViewModel.StartAsync(); + RebuildSensorTree(); + _timer.Start(); + } + catch (Exception ex) + { + RecordRuntimeError("Hardware initialization failed", ex); + } + }); + } + + private Grid BuildRoot() + { + Grid root = new() + { + Background = new SolidColorBrush(Colors.White), + RowDefinitions = + { + new RowDefinition { Height = GridLength.Auto }, + new RowDefinition { Height = new GridLength(1, GridUnitType.Star) }, + new RowDefinition { Height = GridLength.Auto } + } + }; + + root.Children.Add(BuildMenuBar()); + + Grid contentGrid = new() + { + ColumnDefinitions = + { + new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }, + new ColumnDefinition { Width = new GridLength(320) } + }, + RowDefinitions = + { + new RowDefinition { Height = new GridLength(1, GridUnitType.Star) }, + new RowDefinition { Height = new GridLength(220) } + } + }; + Grid.SetRow(contentGrid, 1); + + contentGrid.Children.Add(BuildSensorPane()); + contentGrid.Children.Add(BuildPlotPane()); + root.Children.Add(contentGrid); + + TextBlock statusText = new() + { + Padding = new Thickness(8, 4, 8, 4), + TextTrimming = TextTrimming.CharacterEllipsis + }; + Bind(statusText, TextBlock.TextProperty, ViewModel, nameof(ViewModel.StatusText)); + Grid.SetRow(statusText, 2); + root.Children.Add(statusText); + + return root; + } + + private MenuBar BuildMenuBar() + { + MenuBar menuBar = new(); + + MenuBarItem file = new() { Title = "File" }; + file.Items.Add(CreateMenuItem("Save Report...", async (_, _) => await SaveReportAsync())); + file.Items.Add(new MenuFlyoutSeparator()); + file.Items.Add(CreateMenuItem("Reset", (_, _) => ViewModel.ResetHardware())); + MenuFlyoutSubItem hardware = new() { Text = "Hardware" }; + hardware.Items.Add(CreateToggleItem("Motherboard", nameof(ViewModel.IsMotherboardEnabled))); + hardware.Items.Add(CreateToggleItem("CPU", nameof(ViewModel.IsCpuEnabled))); + hardware.Items.Add(CreateToggleItem("Memory", nameof(ViewModel.IsMemoryEnabled))); + hardware.Items.Add(CreateToggleItem("GPU", nameof(ViewModel.IsGpuEnabled))); + hardware.Items.Add(CreateToggleItem("Power Monitors", nameof(ViewModel.IsPowerMonitorEnabled))); + hardware.Items.Add(CreateToggleItem("Fan Controllers", nameof(ViewModel.IsControllerEnabled))); + hardware.Items.Add(CreateToggleItem("Storage Devices", nameof(ViewModel.IsStorageEnabled))); + hardware.Items.Add(CreateToggleItem("Network", nameof(ViewModel.IsNetworkEnabled))); + hardware.Items.Add(CreateToggleItem("Power Supplies", nameof(ViewModel.IsPsuEnabled))); + hardware.Items.Add(CreateToggleItem("Battery", nameof(ViewModel.IsBatteryEnabled))); + file.Items.Add(hardware); + file.Items.Add(new MenuFlyoutSeparator()); + file.Items.Add(CreateMenuItem("Exit", (_, _) => Close())); + menuBar.Items.Add(file); + + MenuBarItem view = new() { Title = "View" }; + view.Items.Add(CreateMenuItem("Reset Min/Max", (_, _) => ViewModel.ResetMinMax())); + view.Items.Add(CreateMenuItem("Expand All Nodes", (_, _) => + { + ViewModel.SetAllExpanded(true); + RebuildSensorTree(); + })); + view.Items.Add(CreateMenuItem("Collapse All Nodes", (_, _) => + { + ViewModel.SetAllExpanded(false); + RebuildSensorTree(); + })); + view.Items.Add(CreateMenuItem("Reset Plot", (_, _) => ResetPlot())); + view.Items.Add(new MenuFlyoutSeparator()); + view.Items.Add(CreateToggleItem("Show Hidden Sensors", nameof(ViewModel.ShowHiddenSensors))); + view.Items.Add(CreateToggleItem("Show Plot", nameof(ViewModel.ShowPlot))); + MenuFlyoutSubItem columns = new() { Text = "Columns" }; + columns.Items.Add(CreateToggleItem("Value", nameof(ViewModel.ShowValueColumn))); + columns.Items.Add(CreateToggleItem("Min", nameof(ViewModel.ShowMinColumn))); + columns.Items.Add(CreateToggleItem("Max", nameof(ViewModel.ShowMaxColumn))); + view.Items.Add(columns); + menuBar.Items.Add(view); + + MenuBarItem options = new() { Title = "Options" }; + options.Items.Add(CreateToggleItem("Start Minimized", nameof(ViewModel.StartMinimized))); + options.Items.Add(CreateToggleItem("Minimize To Tray", nameof(ViewModel.MinimizeToTray))); + options.Items.Add(CreateToggleItem("Minimize On Close", nameof(ViewModel.MinimizeOnClose))); + options.Items.Add(CreateToggleItem("Run On Windows Startup", nameof(ViewModel.RunOnStartup))); + options.Items.Add(new MenuFlyoutSeparator()); + options.Items.Add(BuildRadioSubMenu("Temperature Unit", [ + ("Celsius", TemperatureUnit.Celsius), + ("Fahrenheit", TemperatureUnit.Fahrenheit) + ], () => ViewModel.TemperatureUnit, value => ViewModel.TemperatureUnit = value)); + options.Items.Add(BuildRadioSubMenu("Plot Location", [ + ("Bottom", PlotLocation.Bottom), + ("Right", PlotLocation.Right) + ], () => ViewModel.PlotLocation, value => ViewModel.PlotLocation = value)); + options.Items.Add(BuildRadioSubMenu("Theme", [ + ("Auto", AppThemeMode.Auto), + ("Light", AppThemeMode.Light), + ("Dark", AppThemeMode.Dark), + ("Black", AppThemeMode.Black) + ], () => ViewModel.ThemeMode, value => ViewModel.ThemeMode = value)); + options.Items.Add(BuildIndexedSubMenu("Stroke Thickness", [ + ("1pt", 1), + ("2pt", 2), + ("3pt", 3), + ("4pt", 4) + ], () => (int)ViewModel.PlotStrokeThickness, value => ViewModel.PlotStrokeThickness = value)); + options.Items.Add(new MenuFlyoutSeparator()); + options.Items.Add(CreateToggleItem("Log Sensors", nameof(ViewModel.LogSensors))); + options.Items.Add(CreateToggleItem("Force Drive Wakeup", nameof(ViewModel.ForceDriveWakeup))); + options.Items.Add(CreateToggleItem("Throttle ATA Storage", nameof(ViewModel.ThrottleAtaUpdate))); + options.Items.Add(BuildRadioSubMenu("File Rotation Method", [ + ("Per Session", LoggerFileRotation.PerSession), + ("Daily", LoggerFileRotation.Daily) + ], () => ViewModel.FileRotationMethod, value => ViewModel.FileRotationMethod = value)); + options.Items.Add(BuildIndexedSubMenu("Update Interval", [ + ("250ms", 0), + ("500ms", 1), + ("1s", 2), + ("2s", 3), + ("5s", 4), + ("10s", 5) + ], () => ViewModel.UpdateIntervalIndex, value => ViewModel.UpdateIntervalIndex = value)); + options.Items.Add(BuildIndexedSubMenu("Logging Interval", [ + ("1s", 0), + ("2s", 1), + ("5s", 2), + ("10s", 3), + ("30s", 4), + ("1min", 5), + ("2min", 6), + ("5min", 7), + ("10min", 8), + ("30min", 9), + ("1h", 10), + ("2h", 11), + ("6h", 12) + ], () => ViewModel.LoggingIntervalIndex, value => ViewModel.LoggingIntervalIndex = value)); + options.Items.Add(BuildIndexedSubMenu("Sensor Values Time Window", [ + ("30s", 0), + ("1min", 1), + ("2min", 2), + ("5min", 3), + ("10min", 4), + ("30min", 5), + ("1h", 6), + ("2h", 7), + ("6h", 8), + ("12h", 9), + ("24h", 10) + ], () => ViewModel.SensorValuesTimeWindowIndex, value => ViewModel.SensorValuesTimeWindowIndex = value)); + options.Items.Add(new MenuFlyoutSeparator()); + MenuFlyoutSubItem webServer = new() { Text = "Remote Web Server" }; + ToggleMenuFlyoutItem runWebServer = CreateToggleItem("Run", nameof(ViewModel.RunWebServer)); + runWebServer.IsEnabled = !ViewModel.IsWebServerUnavailable; + webServer.Items.Add(runWebServer); + MenuFlyoutItem interfacePort = CreateMenuItem("Interface / Port", async (_, _) => await ShowWebServerSettingsAsync()); + interfacePort.IsEnabled = !ViewModel.IsWebServerUnavailable; + webServer.Items.Add(interfacePort); + MenuFlyoutItem authentication = CreateMenuItem("Authentication", async (_, _) => await ShowWebServerAuthenticationAsync()); + authentication.IsEnabled = !ViewModel.IsWebServerUnavailable; + webServer.Items.Add(authentication); + options.Items.Add(webServer); + menuBar.Items.Add(options); + + MenuBarItem help = new() { Title = "Help" }; + help.Items.Add(CreateMenuItem("About", async (_, _) => await ShowAboutAsync())); + menuBar.Items.Add(help); + + return menuBar; + } + + private Grid BuildSensorPane() + { + Grid pane = new() + { + RowDefinitions = + { + new RowDefinition { Height = GridLength.Auto }, + new RowDefinition { Height = new GridLength(1, GridUnitType.Star) } + } + }; + + Grid header = CreateSensorRowGrid(); + _sensorHeader = header; + header.Padding = new Thickness(8, 5, 8, 5); + header.Background = (Brush)Application.Current.Resources["SystemControlBackgroundChromeMediumLowBrush"]; + header.Children.Add(CreateHeaderText("Sensor", 0, Visibility.Visible)); + header.Children.Add(CreateHeaderText("Value", 1, ViewModel.ValueColumnVisibility, nameof(ViewModel.ValueColumnVisibility))); + header.Children.Add(CreateHeaderText("Min", 2, ViewModel.MinColumnVisibility, nameof(ViewModel.MinColumnVisibility))); + header.Children.Add(CreateHeaderText("Max", 3, ViewModel.MaxColumnVisibility, nameof(ViewModel.MaxColumnVisibility))); + pane.Children.Add(header); + + Grid treeHost = new(); + Grid.SetRow(treeHost, 1); + TreeView tree = new() + { + ItemTemplate = CreateTreeViewNodeContentTemplate(), + SelectionMode = TreeViewSelectionMode.Single + }; + tree.SelectionChanged += SensorTree_SelectionChanged; + treeHost.Children.Add(tree); + pane.Children.Add(treeHost); + + return pane; + } + + private Grid BuildPlotPane() + { + Grid pane = new() + { + Padding = new Thickness(8), + BorderBrush = (Brush)Application.Current.Resources["SystemControlForegroundBaseLowBrush"], + BorderThickness = new Thickness(1, 0, 0, 0), + RowDefinitions = + { + new RowDefinition { Height = GridLength.Auto }, + new RowDefinition { Height = new GridLength(1, GridUnitType.Star) } + } + }; + + Grid toolbar = new() + { + ColumnDefinitions = + { + new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }, + new ColumnDefinition { Width = GridLength.Auto } + } + }; + toolbar.Children.Add(new TextBlock { Text = "Plot", FontWeight = new global::Windows.UI.Text.FontWeight { Weight = 600 }, VerticalAlignment = VerticalAlignment.Center }); + Button reset = new() { Content = "Reset" }; + reset.Click += (_, _) => ResetPlot(); + Grid.SetColumn(reset, 1); + toolbar.Children.Add(reset); + pane.Children.Add(toolbar); + + Canvas canvas = new() + { + MinHeight = 160, + Background = (Brush)Application.Current.Resources["SystemControlBackgroundAltHighBrush"] + }; + canvas.SizeChanged += (_, _) => DrawPlot(); + Grid.SetRow(canvas, 1); + pane.Children.Add(canvas); + + return pane; + } + + private void RootItems_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + RebuildSensorTree(); + } + + private async void UpdateTimer_Tick(DispatcherQueueTimer sender, object args) + { + if (_isUpdating) + return; + + _isUpdating = true; + try + { + await ViewModel.UpdateAsync(); + UpdateSensorColumnWidths(); + DrawPlot(); + } + catch (Exception ex) + { + _timer.Stop(); + RecordRuntimeError("Sensor update failed", ex); + } + finally + { + _isUpdating = false; + } + } + + private void RecordRuntimeError(string message, Exception exception) + { + string logPath = IOPath.Combine(AppContext.BaseDirectory, "LibreHardwareMonitor.Windows.WinUI.startup.log"); + File.WriteAllText(logPath, exception.ToString()); + ViewModel.SetStatusText($"{message}. See {IOPath.GetFileName(logPath)}."); + } + + private void MainWindow_Closed(object sender, WindowEventArgs args) + { + _timer.Stop(); + SaveWindowBounds(); + ViewModel.Dispose(); + } + + private void RebuildSensorTree() + { + if (_sensorTree == null) + return; + + _sensorRowGrids.Clear(); + _sensorTree.RootNodes.Clear(); + foreach (SensorTreeItemViewModel item in ViewModel.RootItems) + { + TreeViewNode? node = CreateTreeNode(item); + if (node != null) + _sensorTree.RootNodes.Add(node); + } + + UpdateSensorColumnWidths(); + } + + private static DataTemplate CreateTreeViewNodeContentTemplate() + { + const string xaml = """ + + + + """; + + return (DataTemplate)XamlReader.Load(xaml); + } + + private TreeViewNode? CreateTreeNode(SensorTreeItemViewModel item) + { + if (item.RowVisibility == Visibility.Collapsed) + return null; + + TreeViewNode node = new() + { + Content = CreateSensorRow(item), + IsExpanded = item.IsExpanded + }; + + foreach (SensorTreeItemViewModel child in item.Children) + { + TreeViewNode? childNode = CreateTreeNode(child); + if (childNode != null) + node.Children.Add(childNode); + } + + return node; + } + + private FrameworkElement CreateSensorRow(SensorTreeItemViewModel item) + { + Grid row = CreateSensorRowGrid(); + _sensorRowGrids.Add(row); + row.DataContext = item; + row.Padding = new Thickness(0, 3, 0, 3); + + StackPanel sensorCell = new() { Orientation = Orientation.Horizontal, Spacing = 6 }; + CheckBox plotCheck = new() { MinWidth = 22 }; + Bind(plotCheck, ToggleButton.IsCheckedProperty, item, nameof(SensorTreeItemViewModel.Plot), BindingMode.TwoWay); + Bind(plotCheck, UIElement.VisibilityProperty, item, nameof(SensorTreeItemViewModel.PlotCheckVisibility)); + sensorCell.Children.Add(plotCheck); + + FontIcon icon = new() { FontSize = 14 }; + Bind(icon, FontIcon.GlyphProperty, item, nameof(SensorTreeItemViewModel.IconGlyph)); + sensorCell.Children.Add(icon); + + TextBlock name = new() { TextTrimming = TextTrimming.CharacterEllipsis }; + Bind(name, TextBlock.TextProperty, item, nameof(SensorTreeItemViewModel.Text)); + ToolTip toolTip = new(); + Bind(toolTip, ContentControl.ContentProperty, item, nameof(SensorTreeItemViewModel.ToolTip)); + ToolTipService.SetToolTip(name, toolTip); + sensorCell.Children.Add(name); + + row.Children.Add(sensorCell); + row.Children.Add(CreateValueTextBlock(item, nameof(SensorTreeItemViewModel.Value), nameof(SensorTreeItemViewModel.ValueColumnVisibility), 1)); + row.Children.Add(CreateValueTextBlock(item, nameof(SensorTreeItemViewModel.Min), nameof(SensorTreeItemViewModel.MinColumnVisibility), 2)); + row.Children.Add(CreateValueTextBlock(item, nameof(SensorTreeItemViewModel.Max), nameof(SensorTreeItemViewModel.MaxColumnVisibility), 3)); + + row.ContextFlyout = BuildSensorContextMenu(item); + return row; + } + + private MenuFlyout BuildSensorContextMenu(SensorTreeItemViewModel item) + { + MenuFlyout flyout = new(); + if (item.Sensor?.Parameters.Count > 0) + flyout.Items.Add(CreateMenuItem("Parameters...", async (_, _) => await ShowParametersAsync(item))); + + MenuFlyoutItem rename = CreateMenuItem("Rename", async (_, _) => await RenameNodeAsync(item)); + rename.IsEnabled = item.CanRename; + flyout.Items.Add(rename); + + ToggleMenuFlyoutItem plot = new() { Text = "Show in Plot", IsEnabled = item.Sensor != null }; + Bind(plot, ToggleMenuFlyoutItem.IsCheckedProperty, item, nameof(SensorTreeItemViewModel.Plot), BindingMode.TwoWay); + flyout.Items.Add(plot); + + MenuFlyoutItem hide = CreateMenuItem(item.IsVisible ? "Hide Sensor" : "Unhide Sensor", (_, _) => + { + if (item.CanHide) + { + item.IsVisible = !item.IsVisible; + RebuildSensorTree(); + } + }); + hide.IsEnabled = item.CanHide; + flyout.Items.Add(hide); + flyout.Items.Add(new MenuFlyoutSeparator()); + MenuFlyoutItem penColor = CreateMenuItem("Pen Color...", async (_, _) => await ShowPenColorAsync(item)); + penColor.IsEnabled = item.Sensor != null; + flyout.Items.Add(penColor); + MenuFlyoutItem resetPenColor = CreateMenuItem("Reset Pen Color", (_, _) => ViewModel.SetSensorPenColor(item, null)); + resetPenColor.IsEnabled = item.PenColor != null; + flyout.Items.Add(resetPenColor); + + if (item.Sensor?.Control != null) + { + flyout.Items.Add(new MenuFlyoutSeparator()); + flyout.Items.Add(BuildControlMenu(item.Sensor.Control)); + } + + flyout.Items.Add(new MenuFlyoutSeparator()); + flyout.Items.Add(CreateMenuItem("Reset Min/Max", (_, _) => ViewModel.ResetMinMax())); + return flyout; + } + + private static MenuFlyoutSubItem BuildControlMenu(IControl control) + { + MenuFlyoutSubItem controlItem = new() { Text = "Control" }; + ToggleMenuFlyoutItem defaultItem = new() { Text = "Default", IsChecked = control.ControlMode == ControlMode.Default }; + defaultItem.Click += (_, _) => control.SetDefault(); + controlItem.Items.Add(defaultItem); + + MenuFlyoutSubItem manual = new() { Text = "Manual" }; + for (int value = 0; value <= 100; value += 5) + { + if (value > control.MaxSoftwareValue || value < control.MinSoftwareValue) + continue; + + int controlValue = value; + ToggleMenuFlyoutItem item = new() + { + Text = $"{controlValue} %", + IsChecked = control.ControlMode == ControlMode.Software && Math.Abs(control.SoftwareValue - controlValue) < 0.1f + }; + item.Click += (_, _) => control.SetSoftware(controlValue); + manual.Items.Add(item); + } + + controlItem.Items.Add(manual); + return controlItem; + } + + private async Task RenameNodeAsync(SensorTreeItemViewModel item) + { + if (!item.CanRename) + return; + + TextBox nameTextBox = new() { Text = item.Text, SelectionStart = 0, SelectionLength = item.Text.Length }; + ContentDialog dialog = new() + { + XamlRoot = Content.XamlRoot, + Title = "Rename Sensor", + Content = nameTextBox, + PrimaryButtonText = "Save", + CloseButtonText = "Cancel", + DefaultButton = ContentDialogButton.Primary + }; + + if (await dialog.ShowAsync() == ContentDialogResult.Primary) + item.Text = nameTextBox.Text.Trim(); + } + + private async Task ShowPenColorAsync(SensorTreeItemViewModel item) + { + if (item.Sensor == null) + return; + + ColorPicker picker = new() + { + Color = item.PenColor ?? global::Windows.UI.Color.FromArgb(255, 0, 120, 212), + IsAlphaEnabled = false, + IsColorSliderVisible = true, + IsColorChannelTextInputVisible = true, + IsHexInputVisible = true + }; + + ContentDialog dialog = new() + { + XamlRoot = Content.XamlRoot, + Title = "Pen Color", + Content = picker, + PrimaryButtonText = "Save", + CloseButtonText = "Cancel", + DefaultButton = ContentDialogButton.Primary + }; + + if (await dialog.ShowAsync() == ContentDialogResult.Primary) + ViewModel.SetSensorPenColor(item, picker.Color); + } + + private async Task ShowParametersAsync(SensorTreeItemViewModel item) + { + if (item.Sensor == null || item.Sensor.Parameters.Count == 0) + return; + + StackPanel panel = new() { Spacing = 10 }; + TextBlock caption = new() + { + Text = item.Sensor.Name, + FontWeight = new global::Windows.UI.Text.FontWeight { Weight = 600 }, + TextWrapping = TextWrapping.Wrap + }; + panel.Children.Add(caption); + + ParameterEditorRow[] rows = item.Sensor.Parameters.Select(CreateParameterEditorRow).ToArray(); + foreach (ParameterEditorRow row in rows) + panel.Children.Add(row.Container); + + ContentDialog dialog = new() + { + XamlRoot = Content.XamlRoot, + Title = "Parameters", + Content = new ScrollViewer { Content = panel, MaxHeight = 520 }, + PrimaryButtonText = "OK", + CloseButtonText = "Cancel", + DefaultButton = ContentDialogButton.Primary + }; + dialog.PrimaryButtonClick += (sender, args) => + { + foreach (ParameterEditorRow row in rows) + { + if (row.UseDefault.IsChecked == true) + continue; + + if (!float.TryParse(row.Value.Text, NumberStyles.Float, CultureInfo.CurrentCulture, out float _) + && !float.TryParse(row.Value.Text, NumberStyles.Float, CultureInfo.InvariantCulture, out float _)) + { + row.Value.Header = "Invalid value"; + args.Cancel = true; + return; + } + } + + foreach (ParameterEditorRow row in rows) + { + if (row.UseDefault.IsChecked == true) + row.Parameter.IsDefault = true; + else if (float.TryParse(row.Value.Text, NumberStyles.Float, CultureInfo.CurrentCulture, out float currentCultureValue) + || float.TryParse(row.Value.Text, NumberStyles.Float, CultureInfo.InvariantCulture, out currentCultureValue)) + row.Parameter.Value = currentCultureValue; + } + }; + + await dialog.ShowAsync(); + } + + private static ParameterEditorRow CreateParameterEditorRow(IParameter parameter) + { + Grid grid = new() + { + ColumnDefinitions = + { + new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }, + new ColumnDefinition { Width = GridLength.Auto }, + new ColumnDefinition { Width = new GridLength(120) } + }, + RowDefinitions = + { + new RowDefinition { Height = GridLength.Auto }, + new RowDefinition { Height = GridLength.Auto } + }, + ColumnSpacing = 8 + }; + + TextBlock name = new() + { + Text = parameter.Name, + VerticalAlignment = VerticalAlignment.Center, + TextWrapping = TextWrapping.Wrap + }; + grid.Children.Add(name); + + CheckBox useDefault = new() + { + Content = "Default", + IsChecked = parameter.IsDefault, + VerticalAlignment = VerticalAlignment.Center + }; + Grid.SetColumn(useDefault, 1); + grid.Children.Add(useDefault); + + TextBox value = new() + { + Text = parameter.Value.ToString(CultureInfo.CurrentCulture), + IsEnabled = !parameter.IsDefault + }; + Grid.SetColumn(value, 2); + grid.Children.Add(value); + + TextBlock description = new() + { + Text = parameter.Description, + Opacity = 0.72, + TextWrapping = TextWrapping.Wrap + }; + Grid.SetRow(description, 1); + Grid.SetColumnSpan(description, 3); + grid.Children.Add(description); + + useDefault.Checked += (_, _) => value.IsEnabled = false; + useDefault.Unchecked += (_, _) => value.IsEnabled = true; + + return new ParameterEditorRow(parameter, grid, useDefault, value); + } + + private async Task ShowWebServerSettingsAsync() + { + ComboBox interfaces = new() { MinWidth = 260 }; + foreach (string address in GetLocalIPv4Addresses()) + interfaces.Items.Add(address); + interfaces.Items.Add("0.0.0.0"); + interfaces.SelectedItem = interfaces.Items.Contains(ViewModel.ListenerIp) ? ViewModel.ListenerIp : "0.0.0.0"; + + TextBox port = new() + { + Header = "Port", + Text = ViewModel.ListenerPort.ToString(CultureInfo.InvariantCulture), + InputScope = new InputScope { Names = { new InputScopeName(InputScopeNameValue.Number) } } + }; + + HyperlinkButton link = new() + { + Content = ViewModel.WebServerUrl, + NavigateUri = new Uri(ViewModel.WebServerUrl) + }; + + StackPanel content = new() { Spacing = 12 }; + content.Children.Add(new TextBlock { Text = "Interface", FontWeight = new global::Windows.UI.Text.FontWeight { Weight = 600 } }); + content.Children.Add(interfaces); + content.Children.Add(port); + content.Children.Add(link); + + ContentDialog dialog = new() + { + XamlRoot = Content.XamlRoot, + Title = "Remote Web Server", + Content = content, + PrimaryButtonText = "Save", + CloseButtonText = "Cancel", + DefaultButton = ContentDialogButton.Primary + }; + + dialog.PrimaryButtonClick += (_, args) => + { + if (!int.TryParse(port.Text, NumberStyles.Integer, CultureInfo.InvariantCulture, out int parsedPort) || parsedPort < 1 || parsedPort > 65535) + { + port.Header = "Port must be 1-65535"; + args.Cancel = true; + return; + } + + ViewModel.ListenerIp = interfaces.SelectedItem as string ?? "0.0.0.0"; + ViewModel.ListenerPort = parsedPort; + }; + + await dialog.ShowAsync(); + } + + private async Task ShowWebServerAuthenticationAsync() + { + CheckBox enabled = new() { Content = "Enable HTTP authentication", IsChecked = ViewModel.AuthWebServer }; + TextBox userName = new() + { + Header = "Username", + Text = ViewModel.AuthWebServerUserName, + IsEnabled = enabled.IsChecked == true + }; + PasswordBox password = new() + { + Header = "New password", + IsEnabled = enabled.IsChecked == true + }; + + enabled.Checked += (_, _) => userName.IsEnabled = password.IsEnabled = true; + enabled.Unchecked += (_, _) => userName.IsEnabled = password.IsEnabled = false; + + StackPanel content = new() { Spacing = 12 }; + content.Children.Add(enabled); + content.Children.Add(userName); + content.Children.Add(password); + + ContentDialog dialog = new() + { + XamlRoot = Content.XamlRoot, + Title = "Remote Web Server Authentication", + Content = content, + PrimaryButtonText = "Save", + CloseButtonText = "Cancel", + DefaultButton = ContentDialogButton.Primary + }; + dialog.PrimaryButtonClick += (_, _) => + { + ViewModel.AuthWebServer = enabled.IsChecked == true; + ViewModel.AuthWebServerUserName = userName.Text; + ViewModel.SetAuthPassword(password.Password); + }; + + await dialog.ShowAsync(); + } + + private void SensorTree_SelectionChanged(TreeView sender, TreeViewSelectionChangedEventArgs args) + { + if (args.AddedItems.Count == 0) + { + ViewModel.SelectedItem = null; + return; + } + + object selected = args.AddedItems[0]; + if (selected is TreeViewNode node && node.Content is FrameworkElement element) + ViewModel.SelectedItem = element.DataContext as SensorTreeItemViewModel; + } + + private async Task SaveReportAsync() + { + FileSavePicker picker = new() + { + SuggestedFileName = "LibreHardwareMonitor.Report", + SuggestedStartLocation = PickerLocationId.DocumentsLibrary + }; + picker.FileTypeChoices.Add("Text", [".txt"]); + InitializeWithWindow.Initialize(picker, WindowNative.GetWindowHandle(this)); + + StorageFile? file = await picker.PickSaveFileAsync(); + if (file == null) + return; + + CachedFileManager.DeferUpdates(file); + await FileIO.WriteTextAsync(file, ViewModel.GetReport()); + await CachedFileManager.CompleteUpdatesAsync(file); + } + + private async Task ShowAboutAsync() + { + StackPanel content = new() { Spacing = 8 }; + content.Children.Add(new TextBlock + { + Text = "Libre Hardware Monitor", + FontWeight = new global::Windows.UI.Text.FontWeight { Weight = 600 }, + FontSize = 18 + }); + content.Children.Add(new TextBlock + { + Text = $"Version {typeof(MainWindow).Assembly.GetName().Version}", + TextWrapping = TextWrapping.Wrap + }); + content.Children.Add(new TextBlock + { + Text = "A free, open source hardware monitoring application.", + TextWrapping = TextWrapping.Wrap + }); + content.Children.Add(new HyperlinkButton + { + Content = "Project website", + NavigateUri = new Uri("https://github.com/LibreHardwareMonitor/LibreHardwareMonitor") + }); + content.Children.Add(new HyperlinkButton + { + Content = "Mozilla Public License 2.0", + NavigateUri = new Uri("https://www.mozilla.org/MPL/2.0/") + }); + + ContentDialog dialog = new() + { + XamlRoot = Content.XamlRoot, + Title = "Libre Hardware Monitor", + Content = content, + CloseButtonText = "OK" + }; + await dialog.ShowAsync(); + } + + private void ResetPlot() + { + ViewModel.ResetPlot(); + DrawPlot(); + } + + private void DrawPlot() + { + if (_plotCanvas == null) + return; + + _plotCanvas.Children.Clear(); + if (!ViewModel.ShowPlot || ViewModel.PlotSeries.Count == 0 || _plotCanvas.ActualWidth <= 4 || _plotCanvas.ActualHeight <= 4) + return; + + double minTime = double.MaxValue; + double maxTime = double.MinValue; + double minValue = double.MaxValue; + double maxValue = double.MinValue; + + foreach (PlotSeriesViewModel plotSeries in ViewModel.PlotSeries) + { + foreach (PlotPointViewModel point in plotSeries.Points) + { + double time = point.Timestamp.ToOADate(); + minTime = Math.Min(minTime, time); + maxTime = Math.Max(maxTime, time); + minValue = Math.Min(minValue, point.Value); + maxValue = Math.Max(maxValue, point.Value); + } + } + + if (minTime == double.MaxValue || minValue == double.MaxValue) + return; + + if (Math.Abs(maxTime - minTime) < double.Epsilon) + maxTime = minTime + TimeSpan.FromSeconds(1).TotalDays; + if (Math.Abs(maxValue - minValue) < double.Epsilon) + { + maxValue += 1; + minValue -= 1; + } + + double width = _plotCanvas.ActualWidth; + double height = _plotCanvas.ActualHeight; + DrawAxis(width, height); + + foreach (PlotSeriesViewModel plotSeries in ViewModel.PlotSeries) + { + Polyline line = new() + { + Stroke = new SolidColorBrush(plotSeries.Color), + StrokeThickness = ViewModel.PlotStrokeThickness, + StrokeLineJoin = PenLineJoin.Round + }; + + foreach (PlotPointViewModel point in plotSeries.Points) + { + double x = (point.Timestamp.ToOADate() - minTime) / (maxTime - minTime) * width; + double y = height - ((point.Value - minValue) / (maxValue - minValue) * height); + line.Points.Add(new global::Windows.Foundation.Point(x, y)); + } + + _plotCanvas.Children.Add(line); + } + } + + private void DrawAxis(double width, double height) + { + SolidColorBrush brush = new(Colors.Gray); + for (int i = 1; i < 4; i++) + { + double y = height * i / 4; + _plotCanvas.Children.Add(new Line + { + X1 = 0, + X2 = width, + Y1 = y, + Y2 = y, + Stroke = brush, + StrokeThickness = 0.5, + Opacity = 0.5 + }); + } + } + + private void UpdatePlotLayout() + { + _contentGrid.ColumnDefinitions[1].Width = ViewModel.ShowPlot && ViewModel.PlotLocation == PlotLocation.Right ? new GridLength(320) : new GridLength(0); + _contentGrid.RowDefinitions[1].Height = ViewModel.ShowPlot && ViewModel.PlotLocation == PlotLocation.Bottom ? new GridLength(220) : new GridLength(0); + + _plotPane.Visibility = ViewModel.ShowPlot ? Visibility.Visible : Visibility.Collapsed; + Grid.SetRow(_plotPane, ViewModel.PlotGridRow); + Grid.SetColumn(_plotPane, ViewModel.PlotGridColumn); + Grid.SetRowSpan(_plotPane, ViewModel.PlotGridRowSpan); + Grid.SetColumnSpan(_plotPane, ViewModel.PlotGridColumnSpan); + Grid.SetRowSpan(_sensorPane, ViewModel.SensorGridRowSpan); + Grid.SetColumnSpan(_sensorPane, ViewModel.SensorGridColumnSpan); + DrawPlot(); + } + + private void ApplyTheme() + { + _rootGrid.RequestedTheme = ViewModel.ThemeMode switch + { + AppThemeMode.Light => ElementTheme.Light, + AppThemeMode.Dark or AppThemeMode.Black => ElementTheme.Dark, + _ => ElementTheme.Default + }; + + _rootGrid.Background = ViewModel.ThemeMode switch + { + AppThemeMode.Black => new SolidColorBrush(Colors.Black), + AppThemeMode.Dark => new SolidColorBrush(global::Windows.UI.Color.FromArgb(255, 30, 30, 30)), + _ => (Brush)Application.Current.Resources["ApplicationPageBackgroundThemeBrush"] + }; + } + + private void RestoreWindowBounds() + { + int width = Math.Max(470, ViewModel.Settings.GetValue("mainForm.Width", 760)); + int height = Math.Max(640, ViewModel.Settings.GetValue("mainForm.Height", 680)); + _appWindow.Resize(new SizeInt32(width, height)); + + int x = ViewModel.Settings.GetValue("mainForm.Location.X", int.MinValue); + int y = ViewModel.Settings.GetValue("mainForm.Location.Y", int.MinValue); + if (x != int.MinValue && y != int.MinValue) + _appWindow.Move(new PointInt32(x, y)); + } + + private void MaximizeWindow() + { + if (_appWindow.Presenter is OverlappedPresenter presenter) + presenter.Maximize(); + } + + private void SaveWindowBounds() + { + ViewModel.Settings.SetValue("mainForm.Location.X", _appWindow.Position.X); + ViewModel.Settings.SetValue("mainForm.Location.Y", _appWindow.Position.Y); + ViewModel.Settings.SetValue("mainForm.Width", _appWindow.Size.Width); + ViewModel.Settings.SetValue("mainForm.Height", _appWindow.Size.Height); + ViewModel.Save(); + } + + private void UpdateSensorColumnWidths() + { + double sensorWidth = MeasureText("Sensor", true) + SensorColumnPadding; + double valueWidth = ViewModel.ShowValueColumn ? MeasureText("Value", true) + ValueColumnPadding : 0; + double minWidth = ViewModel.ShowMinColumn ? MeasureText("Min", true) + ValueColumnPadding : 0; + double maxWidth = ViewModel.ShowMaxColumn ? MeasureText("Max", true) + ValueColumnPadding : 0; + + foreach (SensorTreeItemViewModel root in ViewModel.RootItems) + MeasureSensorColumnWidths(root, 0, ref sensorWidth, ref valueWidth, ref minWidth, ref maxWidth); + + _sensorColumnWidths[0] = Math.Ceiling(sensorWidth); + _sensorColumnWidths[1] = Math.Ceiling(valueWidth); + _sensorColumnWidths[2] = Math.Ceiling(minWidth); + _sensorColumnWidths[3] = Math.Ceiling(maxWidth); + + if (_sensorHeader != null) + ApplySensorColumnWidths(_sensorHeader); + foreach (Grid row in _sensorRowGrids) + ApplySensorColumnWidths(row); + } + + private void MeasureSensorColumnWidths( + SensorTreeItemViewModel item, + int depth, + ref double sensorWidth, + ref double valueWidth, + ref double minWidth, + ref double maxWidth) + { + if (item.RowVisibility == Visibility.Visible) + { + sensorWidth = Math.Max(sensorWidth, MeasureText(item.Text) + SensorColumnPadding + depth * SensorTreeIndentWidth); + if (ViewModel.ShowValueColumn) + valueWidth = Math.Max(valueWidth, MeasureText(item.Value) + ValueColumnPadding); + if (ViewModel.ShowMinColumn) + minWidth = Math.Max(minWidth, MeasureText(item.Min) + ValueColumnPadding); + if (ViewModel.ShowMaxColumn) + maxWidth = Math.Max(maxWidth, MeasureText(item.Max) + ValueColumnPadding); + } + + foreach (SensorTreeItemViewModel child in item.Children) + MeasureSensorColumnWidths(child, depth + 1, ref sensorWidth, ref valueWidth, ref minWidth, ref maxWidth); + } + + private static double MeasureText(string text, bool bold = false) + { + TextBlock textBlock = new() + { + Text = text, + FontWeight = new global::Windows.UI.Text.FontWeight { Weight = bold ? (ushort)600 : (ushort)400 } + }; + textBlock.Measure(new global::Windows.Foundation.Size(double.PositiveInfinity, double.PositiveInfinity)); + return textBlock.DesiredSize.Width; + } + + private void ApplySensorColumnWidths(Grid grid) + { + for (int i = 0; i < grid.ColumnDefinitions.Count && i < _sensorColumnWidths.Length; i++) + grid.ColumnDefinitions[i].Width = new GridLength(_sensorColumnWidths[i]); + } + + private Grid CreateSensorRowGrid() + { + Grid grid = new() + { + ColumnDefinitions = + { + new ColumnDefinition(), + new ColumnDefinition(), + new ColumnDefinition(), + new ColumnDefinition() + } + }; + + ApplySensorColumnWidths(grid); + return grid; + } + + private TextBlock CreateHeaderText(string text, int column, Visibility visibility, string? bindingPath = null) + { + TextBlock textBlock = new() + { + Text = text, + FontWeight = new global::Windows.UI.Text.FontWeight { Weight = 600 }, + Visibility = visibility + }; + if (bindingPath != null) + Bind(textBlock, UIElement.VisibilityProperty, ViewModel, bindingPath); + Grid.SetColumn(textBlock, column); + return textBlock; + } + + private static TextBlock CreateValueTextBlock(SensorTreeItemViewModel item, string textPath, string visibilityPath, int column) + { + TextBlock textBlock = new() { TextTrimming = TextTrimming.CharacterEllipsis }; + Bind(textBlock, TextBlock.TextProperty, item, textPath); + Bind(textBlock, UIElement.VisibilityProperty, item, visibilityPath); + Grid.SetColumn(textBlock, column); + return textBlock; + } + + private static MenuFlyoutSubItem BuildIndexedSubMenu(string text, (string Label, int Value)[] items, Func getter, Action setter) + { + return BuildRadioSubMenu(text, items, getter, setter); + } + + private static MenuFlyoutSubItem BuildRadioSubMenu(string text, (string Label, T Value)[] items, Func getter, Action setter) + { + MenuFlyoutSubItem subItem = new() { Text = text }; + foreach ((string label, T value) in items) + { + ToggleMenuFlyoutItem item = new() + { + Text = label, + IsChecked = Equals(getter(), value) + }; + item.Click += (_, _) => + { + setter(value); + foreach (ToggleMenuFlyoutItem sibling in subItem.Items.OfType()) + sibling.IsChecked = Equals(sibling.Tag, value); + }; + item.Tag = value; + subItem.Items.Add(item); + } + + return subItem; + } + + private static MenuFlyoutItem CreateMenuItem(string text, RoutedEventHandler handler) + { + MenuFlyoutItem item = new() { Text = text }; + item.Click += handler; + return item; + } + + private ToggleMenuFlyoutItem CreateToggleItem(string text, string bindingPath) + { + ToggleMenuFlyoutItem item = new() { Text = text }; + Bind(item, ToggleMenuFlyoutItem.IsCheckedProperty, ViewModel, bindingPath, BindingMode.TwoWay); + return item; + } + + private static string[] GetLocalIPv4Addresses() + { + try + { + return Dns.GetHostEntry(Dns.GetHostName()) + .AddressList + .Where(address => address.AddressFamily == AddressFamily.InterNetwork) + .Select(address => address.ToString()) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray(); + } + catch + { + return []; + } + } + + private sealed record ParameterEditorRow(IParameter Parameter, Grid Container, CheckBox UseDefault, TextBox Value); + + private static void Bind(DependencyObject target, DependencyProperty property, object source, string path, BindingMode mode = BindingMode.OneWay) + { + BindingOperations.SetBinding(target, property, new Binding + { + Source = source, + Path = new PropertyPath(path), + Mode = mode, + UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged + }); + } +} diff --git a/LibreHardwareMonitor.Windows.WinUI/Services/AppSettings.cs b/LibreHardwareMonitor.Windows.WinUI/Services/AppSettings.cs new file mode 100644 index 0000000..dd00102 --- /dev/null +++ b/LibreHardwareMonitor.Windows.WinUI/Services/AppSettings.cs @@ -0,0 +1,234 @@ +// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +// Copyright (C) LibreHardwareMonitor and Contributors. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Text; +using System.Xml; +using LibreHardwareMonitor.Hardware; +using Windows.UI; + +namespace LibreHardwareMonitor.Windows.WinUI.Services; + +public sealed class AppSettings : ISettings +{ + private readonly IDictionary _settings = new Dictionary(); + + private AppSettings(string fileName) + { + FileName = fileName; + } + + public string FileName { get; } + + public static AppSettings LoadDefault() + { + string fileName = Path.ChangeExtension(Environment.ProcessPath, ".config") + ?? Path.Combine(AppContext.BaseDirectory, "LibreHardwareMonitor.Windows.WinUI.config"); + AppSettings settings = new(fileName); + settings.Load(); + return settings; + } + + public bool Contains(string name) + { + return _settings.ContainsKey(name); + } + + public string GetValue(string name, string value) + { + return _settings.TryGetValue(name, out string? result) ? result : value; + } + + public int GetValue(string name, int value) + { + return _settings.TryGetValue(name, out string? result) && int.TryParse(result, out int parsedValue) + ? parsedValue + : value; + } + + public float GetValue(string name, float value) + { + return _settings.TryGetValue(name, out string? result) + && float.TryParse(result, NumberStyles.Float, CultureInfo.InvariantCulture, out float parsedValue) + ? parsedValue + : value; + } + + public double GetValue(string name, double value) + { + return _settings.TryGetValue(name, out string? result) + && double.TryParse(result, NumberStyles.Float, CultureInfo.InvariantCulture, out double parsedValue) + ? parsedValue + : value; + } + + public bool GetValue(string name, bool value) + { + return _settings.TryGetValue(name, out string? result) ? result == "true" : value; + } + + public Color GetValue(string name, Color value) + { + if (_settings.TryGetValue(name, out string? result) + && uint.TryParse(result, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out uint parsedValue)) + { + return Color.FromArgb( + (byte)((parsedValue >> 24) & 0xff), + (byte)((parsedValue >> 16) & 0xff), + (byte)((parsedValue >> 8) & 0xff), + (byte)(parsedValue & 0xff)); + } + + return value; + } + + public void SetValue(string name, string value) + { + _settings[name] = value; + } + + public void SetValue(string name, int value) + { + _settings[name] = value.ToString(CultureInfo.InvariantCulture); + } + + public void SetValue(string name, float value) + { + _settings[name] = value.ToString(CultureInfo.InvariantCulture); + } + + public void SetValue(string name, double value) + { + _settings[name] = value.ToString(CultureInfo.InvariantCulture); + } + + public void SetValue(string name, bool value) + { + _settings[name] = value ? "true" : "false"; + } + + public void SetValue(string name, Color value) + { + uint argb = (uint)((value.A << 24) | (value.R << 16) | (value.G << 8) | value.B); + _settings[name] = argb.ToString("X8", CultureInfo.InvariantCulture); + } + + public void Remove(string name) + { + _settings.Remove(name); + } + + public void Load() + { + XmlDocument doc = new(); + try + { + doc.Load(FileName); + } + catch + { + if (!TryLoadBackup(doc)) + return; + } + + XmlNodeList list = doc.GetElementsByTagName("appSettings"); + foreach (XmlNode node in list) + { + if (node.ParentNode?.Name != "configuration" || node.ParentNode.ParentNode is not XmlDocument) + continue; + + foreach (XmlNode child in node.ChildNodes) + { + if (child.Name != "add") + continue; + + XmlAttribute? keyAttribute = child.Attributes?["key"]; + XmlAttribute? valueAttribute = child.Attributes?["value"]; + if (keyAttribute?.Value != null && valueAttribute?.Value != null) + _settings[keyAttribute.Value] = valueAttribute.Value; + } + } + } + + public void Save() + { + Directory.CreateDirectory(Path.GetDirectoryName(FileName) ?? AppContext.BaseDirectory); + + XmlDocument doc = new(); + doc.AppendChild(doc.CreateXmlDeclaration("1.0", "utf-8", null)); + XmlElement configuration = doc.CreateElement("configuration"); + doc.AppendChild(configuration); + XmlElement appSettings = doc.CreateElement("appSettings"); + configuration.AppendChild(appSettings); + + foreach (KeyValuePair setting in _settings) + { + XmlElement add = doc.CreateElement("add"); + add.SetAttribute("key", setting.Key); + add.SetAttribute("value", setting.Value); + appSettings.AppendChild(add); + } + + byte[] file; + using (MemoryStream memory = new()) + { + using (StreamWriter writer = new(memory, Encoding.UTF8, leaveOpen: true)) + doc.Save(writer); + file = memory.ToArray(); + } + + string backupFileName = FileName + ".backup"; + if (File.Exists(FileName)) + { + TryDelete(backupFileName); + TryMove(FileName, backupFileName); + } + + using (FileStream stream = new(FileName, FileMode.Create, FileAccess.Write)) + stream.Write(file, 0, file.Length); + + TryDelete(backupFileName); + } + + private bool TryLoadBackup(XmlDocument doc) + { + TryDelete(FileName); + string backupFileName = FileName + ".backup"; + try + { + doc.Load(backupFileName); + return true; + } + catch + { + TryDelete(backupFileName); + return false; + } + } + + private static void TryDelete(string fileName) + { + try + { + File.Delete(fileName); + } + catch + { + } + } + + private static void TryMove(string sourceFileName, string destinationFileName) + { + try + { + File.Move(sourceFileName, destinationFileName); + } + catch + { + } + } +} diff --git a/LibreHardwareMonitor.Windows.WinUI/Services/HardwareMonitorService.cs b/LibreHardwareMonitor.Windows.WinUI/Services/HardwareMonitorService.cs new file mode 100644 index 0000000..2f8fdd9 --- /dev/null +++ b/LibreHardwareMonitor.Windows.WinUI/Services/HardwareMonitorService.cs @@ -0,0 +1,197 @@ +// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +// Copyright (C) LibreHardwareMonitor and Contributors. + +using System; +using System.Linq; +using System.Threading.Tasks; +using LibreHardwareMonitor.Hardware; +using LibreHardwareMonitor.Hardware.Storage; +using LibreHardwareMonitor.Windows.WinUI.ViewModels; + +namespace LibreHardwareMonitor.Windows.WinUI.Services; + +public sealed class HardwareMonitorService : IDisposable +{ + private readonly object _updateLock = new(); + private readonly UpdateVisitor _updateVisitor = new(); + private bool _isOpen; + + public HardwareMonitorService(AppSettings settings) + { + Settings = settings; + Computer = new Computer(settings); + Computer.HardwareAdded += HardwareChanged; + Computer.HardwareRemoved += HardwareChanged; + ApplyHardwareFlagsFromSettings(); + } + + public event EventHandler? TreeRebuilt; + + public Computer Computer { get; } + + public AppSettings Settings { get; } + + public SensorTreeItemViewModel Root { get; private set; } = SensorTreeItemViewModel.CreateRoot(Environment.MachineName); + + public bool IsMotherboardEnabled + { + get => Computer.IsMotherboardEnabled; + set => SetHardwareEnabled("mainboardMenuItem", value, v => Computer.IsMotherboardEnabled = v); + } + + public bool IsCpuEnabled + { + get => Computer.IsCpuEnabled; + set => SetHardwareEnabled("cpuMenuItem", value, v => Computer.IsCpuEnabled = v); + } + + public bool IsMemoryEnabled + { + get => Computer.IsMemoryEnabled; + set => SetHardwareEnabled("ramMenuItem", value, v => Computer.IsMemoryEnabled = v); + } + + public bool IsGpuEnabled + { + get => Computer.IsGpuEnabled; + set => SetHardwareEnabled("gpuMenuItem", value, v => Computer.IsGpuEnabled = v); + } + + public bool IsPowerMonitorEnabled + { + get => Computer.IsPowerMonitorEnabled; + set => SetHardwareEnabled("powerMonitorMenuItem", value, v => Computer.IsPowerMonitorEnabled = v); + } + + public bool IsControllerEnabled + { + get => Computer.IsControllerEnabled; + set => SetHardwareEnabled("fanControllerMenuItem", value, v => Computer.IsControllerEnabled = v); + } + + public bool IsStorageEnabled + { + get => Computer.IsStorageEnabled; + set => SetHardwareEnabled("hddMenuItem", value, v => Computer.IsStorageEnabled = v); + } + + public bool IsNetworkEnabled + { + get => Computer.IsNetworkEnabled; + set => SetHardwareEnabled("nicMenuItem", value, v => Computer.IsNetworkEnabled = v); + } + + public bool IsPsuEnabled + { + get => Computer.IsPsuEnabled; + set => SetHardwareEnabled("psuMenuItem", value, v => Computer.IsPsuEnabled = v); + } + + public bool IsBatteryEnabled + { + get => Computer.IsBatteryEnabled; + set => SetHardwareEnabled("batteryMenuItem", value, v => Computer.IsBatteryEnabled = v); + } + + public bool ForceDriveWakeup + { + get => Settings.GetValue("forceDriveWakeupItem", false); + set + { + Settings.SetValue("forceDriveWakeupItem", value); + foreach (StorageDevice storageDevice in Computer.Hardware.OfType()) + storageDevice.ForceWakeup = value; + } + } + + public void Open(bool raiseTreeRebuilt = true) + { + if (_isOpen) + return; + + Computer.Open(); + _isOpen = true; + RebuildTree(raiseTreeRebuilt); + } + + public void Reset() + { + if (_isOpen) + Computer.Close(); + + Root = SensorTreeItemViewModel.CreateRoot(Environment.MachineName); + Computer.Open(); + RebuildTree(); + } + + public Task UpdateAsync() + { + return Task.Run(() => + { + lock (_updateLock) + Computer.Accept(_updateVisitor); + }); + } + + public void ResetMinMax() + { + SensorVisitor visitor = new(sensor => sensor.ResetMin()); + visitor.VisitComputer(Computer); + visitor = new SensorVisitor(sensor => sensor.ResetMax()); + visitor.VisitComputer(Computer); + } + + public void ClearSensorValues() + { + SensorVisitor visitor = new(sensor => sensor.ClearValues()); + visitor.VisitComputer(Computer); + } + + public void RebuildTree(bool raiseTreeRebuilt = true) + { + SensorTreeItemViewModel root = SensorTreeItemViewModel.CreateRoot(Environment.MachineName); + foreach (IHardware hardware in Computer.Hardware.OrderBy(hardware => hardware.HardwareType).ThenBy(hardware => hardware.Name, StringComparer.OrdinalIgnoreCase)) + root.Children.Add(SensorTreeItemViewModel.FromHardware(hardware, Settings)); + + Root = root; + if (raiseTreeRebuilt) + TreeRebuilt?.Invoke(this, EventArgs.Empty); + } + + public void Dispose() + { + Computer.HardwareAdded -= HardwareChanged; + Computer.HardwareRemoved -= HardwareChanged; + Computer.Close(); + } + + private void ApplyHardwareFlagsFromSettings() + { + Computer.IsMotherboardEnabled = Settings.GetValue("mainboardMenuItem", true); + Computer.IsCpuEnabled = Settings.GetValue("cpuMenuItem", true); + Computer.IsMemoryEnabled = Settings.GetValue("ramMenuItem", true); + Computer.IsGpuEnabled = Settings.GetValue("gpuMenuItem", true); + Computer.IsPowerMonitorEnabled = Settings.GetValue("powerMonitorMenuItem", true); + Computer.IsControllerEnabled = Settings.GetValue("fanControllerMenuItem", true); + Computer.IsStorageEnabled = Settings.GetValue("hddMenuItem", true); + Computer.IsNetworkEnabled = Settings.GetValue("nicMenuItem", true); + Computer.IsPsuEnabled = Settings.GetValue("psuMenuItem", true); + Computer.IsBatteryEnabled = Settings.GetValue("batteryMenuItem", true); + ForceDriveWakeup = Settings.GetValue("forceDriveWakeupItem", false); + } + + private void SetHardwareEnabled(string settingName, bool value, Action setter) + { + setter(value); + Settings.SetValue(settingName, value); + if (_isOpen) + RebuildTree(); + } + + private void HardwareChanged(IHardware hardware) + { + if (_isOpen) + RebuildTree(); + } +} diff --git a/LibreHardwareMonitor.Windows.WinUI/Services/Logger.cs b/LibreHardwareMonitor.Windows.WinUI/Services/Logger.cs new file mode 100644 index 0000000..a4b2ff8 --- /dev/null +++ b/LibreHardwareMonitor.Windows.WinUI/Services/Logger.cs @@ -0,0 +1,201 @@ +// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +// Copyright (C) LibreHardwareMonitor and Contributors. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using LibreHardwareMonitor.Hardware; + +namespace LibreHardwareMonitor.Windows.WinUI.Services; + +public sealed class Logger +{ + private const string FileNameFormat = "LibreHardwareMonitorLog-{0:yyyy-MM-dd}{1}.csv"; + + private readonly IComputer _computer; + private DateTime _day = DateTime.MinValue; + private string _fileName = ""; + private string[]? _identifiers; + private DateTime _lastLoggedTime = DateTime.MinValue; + private ISensor?[]? _sensors; + + public Logger(IComputer computer) + { + _computer = computer; + _computer.HardwareAdded += HardwareAdded; + _computer.HardwareRemoved += HardwareRemoved; + } + + public LoggerFileRotation FileRotationMethod { get; set; } = LoggerFileRotation.PerSession; + + public TimeSpan LoggingInterval { get; set; } = TimeSpan.FromSeconds(1); + + public void Log() + { + DateTime now = DateTime.Now; + if (_lastLoggedTime + LoggingInterval - TimeSpan.FromMilliseconds(500) > now) + return; + + switch (FileRotationMethod) + { + case LoggerFileRotation.PerSession: + if (!File.Exists(_fileName) || now - _lastLoggedTime > LoggingInterval + TimeSpan.FromMilliseconds(100)) + { + uint sessionNumber = 1; + do + { + _fileName = GetFileName(DateTime.Now, sessionNumber); + sessionNumber++; + } while (File.Exists(_fileName)); + + CreateNewLogFile(); + } + break; + + case LoggerFileRotation.Daily: + if (_day != now.Date || !File.Exists(_fileName)) + { + _day = now.Date; + _fileName = GetFileName(_day); + if (!OpenExistingLogFile()) + CreateNewLogFile(); + } + break; + } + + try + { + using StreamWriter writer = new(new FileStream(_fileName, FileMode.Append, FileAccess.Write, FileShare.ReadWrite)); + writer.Write(now.ToString("G", CultureInfo.InvariantCulture)); + writer.Write(","); + + if (_sensors != null) + { + for (int i = 0; i < _sensors.Length; i++) + { + float? value = _sensors[i]?.Value; + if (value.HasValue) + writer.Write(value.Value.ToString("R", CultureInfo.InvariantCulture)); + + writer.Write(i < _sensors.Length - 1 ? "," : Environment.NewLine); + } + } + } + catch (IOException) + { + } + + _lastLoggedTime = now; + } + + private static string GetFileName(DateTime date, uint sessionNumber = 0) + { + return Path.Combine(AppContext.BaseDirectory, string.Format(FileNameFormat, date, sessionNumber == 0 ? "" : "-" + sessionNumber)); + } + + private void CreateNewLogFile() + { + IList sensors = new List(); + SensorVisitor visitor = new(sensor => sensors.Add(sensor)); + visitor.VisitComputer(_computer); + + _sensors = sensors.Cast().ToArray(); + _identifiers = sensors.Select(sensor => sensor.Identifier.ToString()).ToArray(); + + using StreamWriter writer = new(_fileName, false); + writer.Write(","); + writer.WriteLine(string.Join(",", _identifiers)); + writer.Write("Time,"); + writer.WriteLine(string.Join(",", sensors.Select(sensor => $"\"{sensor.Name}\""))); + } + + private bool OpenExistingLogFile() + { + if (!File.Exists(_fileName)) + return false; + + try + { + using StreamReader reader = new(_fileName); + string? line = reader.ReadLine(); + if (string.IsNullOrEmpty(line)) + return false; + + _identifiers = line.Split(',').Skip(1).ToArray(); + } + catch + { + _identifiers = null; + return false; + } + + if (_identifiers.Length == 0) + { + _identifiers = null; + return false; + } + + _sensors = new ISensor?[_identifiers.Length]; + SensorVisitor visitor = new(sensor => + { + for (int i = 0; i < _identifiers.Length; i++) + { + if (sensor.Identifier.ToString() == _identifiers[i]) + _sensors[i] = sensor; + } + }); + visitor.VisitComputer(_computer); + return true; + } + + private void HardwareAdded(IHardware hardware) + { + foreach (ISensor sensor in hardware.Sensors) + SensorAdded(sensor); + + hardware.SensorAdded += SensorAdded; + hardware.SensorRemoved += SensorRemoved; + + foreach (IHardware subHardware in hardware.SubHardware) + HardwareAdded(subHardware); + } + + private void HardwareRemoved(IHardware hardware) + { + hardware.SensorAdded -= SensorAdded; + hardware.SensorRemoved -= SensorRemoved; + + foreach (ISensor sensor in hardware.Sensors) + SensorRemoved(sensor); + + foreach (IHardware subHardware in hardware.SubHardware) + HardwareRemoved(subHardware); + } + + private void SensorAdded(ISensor sensor) + { + if (_sensors == null || _identifiers == null) + return; + + for (int i = 0; i < _sensors.Length; i++) + { + if (sensor.Identifier.ToString() == _identifiers[i]) + _sensors[i] = sensor; + } + } + + private void SensorRemoved(ISensor sensor) + { + if (_sensors == null) + return; + + for (int i = 0; i < _sensors.Length; i++) + { + if (sensor == _sensors[i]) + _sensors[i] = null; + } + } +} diff --git a/LibreHardwareMonitor.Windows.WinUI/Services/LoggerFileRotation.cs b/LibreHardwareMonitor.Windows.WinUI/Services/LoggerFileRotation.cs new file mode 100644 index 0000000..52f618a --- /dev/null +++ b/LibreHardwareMonitor.Windows.WinUI/Services/LoggerFileRotation.cs @@ -0,0 +1,11 @@ +// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +// Copyright (C) LibreHardwareMonitor and Contributors. + +namespace LibreHardwareMonitor.Windows.WinUI.Services; + +public enum LoggerFileRotation +{ + PerSession = 0, + Daily = 1 +} diff --git a/LibreHardwareMonitor.Windows.WinUI/Services/RemoteWebServer.cs b/LibreHardwareMonitor.Windows.WinUI/Services/RemoteWebServer.cs new file mode 100644 index 0000000..0dce8af --- /dev/null +++ b/LibreHardwareMonitor.Windows.WinUI/Services/RemoteWebServer.cs @@ -0,0 +1,719 @@ +// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +// Copyright (C) LibreHardwareMonitor and Contributors. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Net; +using System.Reflection; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading; +using System.Threading.Tasks; +using LibreHardwareMonitor.Hardware; +using LibreHardwareMonitor.Windows.WinUI.Utilities; +using LibreHardwareMonitor.Windows.WinUI.ViewModels; + +namespace LibreHardwareMonitor.Windows.WinUI.Services; + +public sealed class RemoteWebServer : IDisposable +{ + private readonly IComputer _computer; + private readonly Func _rootProvider; + private readonly Version _version = typeof(RemoteWebServer).Assembly.GetName().Version ?? new Version(0, 0); + private CancellationTokenSource? _cts; + private HttpListener? _listener; + private Task? _listenerTask; + + public RemoteWebServer( + Func rootProvider, + IComputer computer, + string listenerIp, + int listenerPort, + bool authEnabled, + string userName, + string passwordSha256) + { + _rootProvider = rootProvider; + _computer = computer; + ListenerIp = listenerIp; + ListenerPort = listenerPort; + AuthEnabled = authEnabled; + UserName = userName; + PasswordSHA256 = passwordSha256; + + try + { + _listener = new HttpListener { IgnoreWriteExceptions = true }; + } + catch (PlatformNotSupportedException) + { + _listener = null; + } + } + + public bool AuthEnabled { get; set; } + + public bool IsRunning => _listener?.IsListening == true; + + public string ListenerIp { get; set; } + + public int ListenerPort { get; set; } + + public string PasswordSHA256 { get; private set; } + + public bool PlatformNotSupported => _listener == null; + + public string UserName { get; set; } + + public void Dispose() + { + Quit(); + } + + public void SetPassword(string plainPassword) + { + PasswordSHA256 = ComputeSHA256(plainPassword); + } + + public bool Start() + { + if (PlatformNotSupported || _listener == null) + return false; + + try + { + if (_listener.IsListening) + return true; + + string listenerIp = ResolveListenerIp(); + string prefix = $"http://{listenerIp}:{ListenerPort}/"; + + _listener.Prefixes.Clear(); + _listener.Prefixes.Add(prefix); + _listener.Realm = "Libre Hardware Monitor"; + _listener.AuthenticationSchemes = AuthEnabled ? AuthenticationSchemes.Basic : AuthenticationSchemes.Anonymous; + _listener.Start(); + + _cts = new CancellationTokenSource(); + _listenerTask = Task.Run(() => ProcessRequestsAsync(_cts.Token)); + return true; + } + catch + { + return false; + } + } + + public bool Stop() + { + if (PlatformNotSupported || _listener == null) + return false; + + try + { + _cts?.Cancel(); + _listenerTask?.Wait(TimeSpan.FromSeconds(5)); + _listener.Stop(); + _cts?.Dispose(); + _cts = null; + _listenerTask = null; + return true; + } + catch + { + return false; + } + } + + public void Quit() + { + if (PlatformNotSupported) + return; + + Stop(); + try + { + _listener?.Abort(); + } + catch + { + } + } + + private async Task ProcessRequestsAsync(CancellationToken cancellationToken) + { + if (_listener == null) + return; + + while (_listener.IsListening && !cancellationToken.IsCancellationRequested) + { + try + { + HttpListenerContext context = await _listener.GetContextAsync(); + _ = Task.Run(() => HandleContextAsync(context), cancellationToken); + } + catch (ObjectDisposedException) + { + break; + } + catch (OperationCanceledException) + { + break; + } + catch (HttpListenerException) + { + if (!_listener.IsListening) + break; + } + catch + { + } + } + } + + private async Task HandleContextAsync(HttpListenerContext context) + { + try + { + if (!IsAuthenticated(context)) + { + context.Response.StatusCode = 401; + await SendResponseAsync(context.Response, "

401 Unauthorized

Authorization required.", "text/html"); + return; + } + + HttpListenerRequest request = context.Request; + if (request.HttpMethod == "POST") + { + await HandlePostRequestAsync(context.Response, request); + return; + } + + string requestedFile = request.RawUrl?.TrimStart('/') ?? ""; + if (requestedFile.Length == 0) + requestedFile = "index.html"; + + if (requestedFile.Equals("data.json", StringComparison.OrdinalIgnoreCase)) + { + await SendJsonAsync(context.Response, request); + return; + } + + if (requestedFile.StartsWith("metrics", StringComparison.OrdinalIgnoreCase)) + { + await SendPrometheusAsync(context.Response, request); + return; + } + + if (requestedFile.StartsWith("Sensor", StringComparison.OrdinalIgnoreCase)) + { + Dictionary result = []; + HandleSensorRequest(request, result); + await SendJsonSensorAsync(context.Response, result); + return; + } + + if (requestedFile.StartsWith("ResetAllMinMax", StringComparison.OrdinalIgnoreCase)) + { + _computer.Accept(new SensorVisitor(sensor => + { + sensor.ResetMin(); + sensor.ResetMax(); + })); + await SendJsonAsync(context.Response, request); + return; + } + + if (requestedFile.StartsWith("images_icon/", StringComparison.OrdinalIgnoreCase)) + { + await ServeResourceFileAsync(context.Response, requestedFile["images_icon/".Length..]); + return; + } + + await ServeResourceFileAsync(context.Response, PathForWebResource(requestedFile)); + } + catch + { + try + { + context.Response.StatusCode = 500; + context.Response.Close(); + } + catch + { + } + } + } + + private bool IsAuthenticated(HttpListenerContext context) + { + if (!AuthEnabled) + return true; + + try + { + if (context.User?.Identity is HttpListenerBasicIdentity identity) + return identity.Name == UserName && ComputeSHA256(identity.Password) == PasswordSHA256; + } + catch + { + } + + return false; + } + + private async Task HandlePostRequestAsync(HttpListenerResponse response, HttpListenerRequest request) + { + Dictionary result = new() { ["result"] = "ok" }; + try + { + if (request.Url?.Segments.Length == 2 && request.Url.Segments[1].TrimEnd('/').Equals("Sensor", StringComparison.OrdinalIgnoreCase)) + HandleSensorRequest(request, result); + else + throw new ArgumentException("Empty URL, possible values: ['Sensor']"); + } + catch (Exception ex) + { + result["result"] = "fail"; + result["message"] = ex.ToString(); + } + + await SendJsonSensorAsync(response, result); + } + + private void HandleSensorRequest(HttpListenerRequest request, Dictionary result) + { + IDictionary query = ParseQuery(request.Url?.Query); + if (!query.TryGetValue("action", out string? action)) + throw new ArgumentNullException(nameof(action), "No action provided"); + if (!query.TryGetValue("id", out string? id)) + throw new ArgumentNullException(nameof(id), "No id provided"); + + SensorTreeItemViewModel sensorNode = FindSensor(id) + ?? throw new ArgumentException("Unknown id " + id + " specified"); + ISensor sensor = sensorNode.Sensor!; + + if (action == "ResetMinMax") + { + sensor.ResetMin(); + sensor.ResetMax(); + action = "Get"; + } + + switch (action) + { + case "Set" when query.TryGetValue("value", out string? value): + SetSensorControlValue(sensor, value); + result["result"] = "ok"; + break; + case "Set": + throw new ArgumentNullException("value", "No value provided"); + case "Get": + result["result"] = "ok"; + result["value"] = sensor.Value; + result["min"] = sensor.Min; + result["max"] = sensor.Max; + result["format"] = SensorFormatter.GetFormatString(sensor); + break; + default: + throw new ArgumentException("Unknown action type " + action); + } + } + + private SensorTreeItemViewModel? FindSensor(string id) + { + return _rootProvider()?.EnumerateSensors() + .FirstOrDefault(sensorItem => sensorItem.Sensor?.Identifier.ToString() == id); + } + + private static void SetSensorControlValue(ISensor sensor, string value) + { + if (sensor.Control == null) + throw new ArgumentException("Specified sensor '" + sensor.Identifier + "' can not be set"); + + if (value == "null") + sensor.Control.SetDefault(); + else + sensor.Control.SetSoftware(float.Parse(value, CultureInfo.InvariantCulture)); + } + + private async Task SendJsonAsync(HttpListenerResponse response, HttpListenerRequest request) + { + Dictionary json = new() + { + ["id"] = 0, + ["Version"] = $"{_version.Major}.{_version.Minor}.{_version.Build}", + ["Text"] = "Sensor", + ["Min"] = "Min", + ["Value"] = "Value", + ["Max"] = "Max", + ["ImageURL"] = "" + }; + + int nodeIndex = 1; + SensorTreeItemViewModel? root = _rootProvider(); + json["Children"] = root == null ? Array.Empty() : new List { GenerateJsonForNode(root, ref nodeIndex) }; + + JsonSerializerOptions options = new() + { + NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals + }; + + byte[] buffer = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(json, options)); + bool acceptGzip = request.Headers["Accept-Encoding"]?.IndexOf("gzip", StringComparison.OrdinalIgnoreCase) >= 0; + + response.AddHeader("Cache-Control", "no-cache"); + response.AddHeader("Access-Control-Allow-Origin", "*"); + response.ContentType = "application/json"; + + if (acceptGzip) + { + response.AddHeader("Content-Encoding", "gzip"); + using MemoryStream memory = new(); + using (GZipStream zip = new(memory, CompressionMode.Compress, true)) + await zip.WriteAsync(buffer); + buffer = memory.ToArray(); + } + + response.ContentLength64 = buffer.Length; + await response.OutputStream.WriteAsync(buffer); + response.Close(); + } + + private Dictionary GenerateJsonForNode(SensorTreeItemViewModel node, ref int nodeIndex) + { + Dictionary jsonNode = new() + { + ["id"] = nodeIndex++, + ["Text"] = node.Text, + ["Min"] = "", + ["Value"] = "", + ["Max"] = "", + ["ImageURL"] = GetImageUrl(node) + }; + + if (node.Sensor != null) + { + jsonNode["SensorId"] = node.Sensor.Identifier.ToString(); + jsonNode["Type"] = node.Sensor.SensorType.ToString(); + jsonNode["Min"] = node.Min; + jsonNode["Value"] = node.Value; + jsonNode["Max"] = node.Max; + jsonNode["RawMin"] = node.Sensor.Min; + jsonNode["RawValue"] = node.Sensor.Value; + jsonNode["RawMax"] = node.Sensor.Max; + jsonNode["ImageURL"] = "images/transparent.png"; + } + else if (node.Hardware != null) + { + jsonNode["HardwareId"] = node.Hardware.Identifier.ToString(); + } + + List children = []; + foreach (SensorTreeItemViewModel child in node.Children) + children.Add(GenerateJsonForNode(child, ref nodeIndex)); + jsonNode["Children"] = children; + + return jsonNode; + } + + private async Task SendPrometheusAsync(HttpListenerResponse response, HttpListenerRequest request) + { + Dictionary settings = ParsePrometheusSettings(request); + string content = GeneratePrometheusResponse(_rootProvider(), settings); + + response.AddHeader("Cache-Control", "no-cache"); + response.AddHeader("Access-Control-Allow-Origin", "*"); + response.AddHeader("X-archivelength", settings["archivelength"].ToString(CultureInfo.InvariantCulture)); + response.AddHeader("X-timestamps", settings["timestamps"].ToString(CultureInfo.InvariantCulture)); + response.AddHeader("X-lastvalue", settings["lastvalue"].ToString(CultureInfo.InvariantCulture)); + await SendResponseAsync(response, content, "text/plain"); + } + + private static Dictionary ParsePrometheusSettings(HttpListenerRequest request) + { + Dictionary query = new(ParseQuery(request.Url?.Query), StringComparer.OrdinalIgnoreCase); + int archive = ClampQueryValue(query, "archivelength", 0, 0, 10); + int timestamps = ClampQueryValue(query, "timestamps", 0, 0, 1); + int lastValue = ClampQueryValue(query, "lastvalue", 1, 0, 1); + + if (archive == 0 && lastValue == 0) + { + archive = 1; + timestamps = 1; + } + + if (archive > 0) + timestamps = 1; + + return new Dictionary + { + ["archivelength"] = archive, + ["timestamps"] = timestamps, + ["lastvalue"] = lastValue + }; + } + + private static int ClampQueryValue(IDictionary query, string key, int fallback, int min, int max) + { + if (!query.TryGetValue(key, out string? rawValue) || !int.TryParse(rawValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out int value)) + return fallback; + + return Math.Clamp(value, min, max); + } + + private static string GeneratePrometheusResponse(SensorTreeItemViewModel? root, Dictionary settings) + { + if (root == null) + return ""; + + StringBuilder builder = new(); + string host = root.Text; + string lastTagName = ""; + + foreach (SensorTreeItemViewModel hardware in root.Enumerate().Where(item => item.Hardware != null)) + { + string hardwareType = hardware.Hardware!.HardwareType.ToString(); + string hardwareName = hardware.Hardware.Name; + string hardwareId = hardware.Hardware.Identifier.ToString(); + string hardwareAlias = $"{hardwareName} ({hardwareId})"; + + foreach (SensorTreeItemViewModel sensorItem in hardware.EnumerateSensors()) + { + ISensor sensor = sensorItem.Sensor!; + (string suffix, double factor) = GetPrometheusUnit(sensor.SensorType); + string tagName = $"lhm_{hardwareType}_{sensor.SensorType}{suffix}".ToLowerInvariant(); + string sensorName = sensor.Name.Replace("#", ""); + string sensorId = sensor.Identifier.ToString().StartsWith(hardwareId, StringComparison.Ordinal) + ? sensor.Identifier.ToString()[hardwareId.Length..] + : sensor.Identifier.ToString(); + string sensorAlias = $"{sensorName} ({sensorId})"; + string tagLine = $$"""{{tagName}} {"sensorName"="{{sensorName}}", "sensorAlias"="{{sensorAlias}}", "hardwareName"="{{hardwareName}}", "hardwareAlias"="{{hardwareAlias}}", "sensorId"="{{sensorId}}", "hardwareId"="{{hardwareId}}", "host"="{{host}}"}"""; + + if (lastTagName != tagName) + { + builder.Append("# TYPE ").Append(tagName).AppendLine(" gauge"); + lastTagName = tagName; + } + + int counter = 0; + foreach (SensorValue value in sensor.Values.Reverse()) + { + if (counter++ > settings["archivelength"]) + break; + if (float.IsNaN(value.Value)) + { + builder.Append("# HELP ").Append(tagLine).AppendLine(" has an invalid value and was skipped."); + continue; + } + if (counter == 1 && settings["lastvalue"] == 0) + continue; + + builder.Append(tagLine).Append(' ').Append((value.Value * factor).ToString(CultureInfo.InvariantCulture)); + if (settings["timestamps"] == 1) + builder.Append(' ').Append(((DateTimeOffset)value.Time).ToUnixTimeMilliseconds()); + builder.AppendLine(); + } + } + } + + return builder.ToString(); + } + + private static (string Suffix, double Factor) GetPrometheusUnit(SensorType sensorType) + { + return sensorType switch + { + SensorType.Clock => ("_hertz", 1000000), + SensorType.Conductivity => ("_seconds_per_centimeter", 0.000001), + SensorType.Control => ("_percent", 1), + SensorType.Current => ("_amperes", 1), + SensorType.Data => ("_bytes", 1000000000), + SensorType.Energy => ("_watthour", 0.001), + SensorType.Fan => ("_rpm", 1), + SensorType.Flow => ("_liters_per_hour", 1), + SensorType.Frequency => ("_hertz", 1), + SensorType.Humidity => ("_percent", 1), + SensorType.Level => ("_percent", 1), + SensorType.Load => ("_percent", 1), + SensorType.Noise => ("_decibels", 1), + SensorType.Power => ("_watts", 1), + SensorType.SmallData => ("_bytes", 1024 * 1024), + SensorType.Temperature => ("_celsius", 1), + SensorType.Throughput => ("_bytes_per_second", 1), + SensorType.TimeSpan => ("_seconds", 1), + SensorType.Timing => ("_seconds", 0.000000001), + SensorType.Voltage => ("_volts", 1), + _ => ("", 1) + }; + } + + private async Task SendJsonSensorAsync(HttpListenerResponse response, Dictionary sensorData) + { + response.AddHeader("Cache-Control", "no-cache"); + response.AddHeader("Access-Control-Allow-Origin", "*"); + await SendResponseAsync(response, JsonSerializer.Serialize(sensorData), "application/json"); + } + + private static async Task SendResponseAsync(HttpListenerResponse response, string content, string contentType) + { + byte[] buffer = Encoding.UTF8.GetBytes(content); + response.ContentType = contentType; + response.ContentLength64 = buffer.Length; + await response.OutputStream.WriteAsync(buffer); + response.Close(); + } + + private async Task ServeResourceFileAsync(HttpListenerResponse response, string relativePath) + { + string normalizedPath = relativePath.Replace('/', '.').Replace('\\', '.').Replace("custom-theme", "custom_theme"); + Assembly assembly = Assembly.GetExecutingAssembly(); + string? resourceName = assembly.GetManifestResourceNames() + .FirstOrDefault(name => name.EndsWith(".Resources." + normalizedPath, StringComparison.OrdinalIgnoreCase) + || name.EndsWith(".Resources.Web." + normalizedPath, StringComparison.OrdinalIgnoreCase)); + + if (resourceName == null) + { + response.StatusCode = 404; + response.Close(); + return; + } + + await using Stream? stream = assembly.GetManifestResourceStream(resourceName); + if (stream == null) + { + response.StatusCode = 404; + response.Close(); + return; + } + + response.ContentType = GetContentType(Path.GetExtension(relativePath)); + response.ContentLength64 = stream.Length; + await stream.CopyToAsync(response.OutputStream); + response.Close(); + } + + private string ResolveListenerIp() + { + if (ListenerIp is "+" or "*" or "0.0.0.0") + return ListenerIp; + + try + { + IPHostEntry host = Dns.GetHostEntry(Dns.GetHostName()); + if (host.AddressList.Any(ip => ip.ToString() == ListenerIp)) + return ListenerIp; + } + catch + { + } + + ListenerIp = "+"; + return ListenerIp; + } + + private static string PathForWebResource(string requestedFile) + { + return "Web." + requestedFile.TrimStart('/').Replace('/', '.'); + } + + private static string GetImageUrl(SensorTreeItemViewModel node) + { + if (node.Hardware != null) + return "images_icon/" + GetHardwareImageFile(node.Hardware.HardwareType); + if (node.Kind == SensorTreeItemKind.SensorType) + return "images_icon/" + GetTypeImageFile(node.Text); + if (node.Kind == SensorTreeItemKind.Root) + return "images_icon/computer.png"; + + return "images/transparent.png"; + } + + private static string GetHardwareImageFile(HardwareType hardwareType) + { + return hardwareType switch + { + HardwareType.Cpu => "cpu.png", + HardwareType.GpuNvidia => "nvidia.png", + HardwareType.GpuAmd => "ati.png", + HardwareType.GpuIntel => "intel.png", + HardwareType.Storage => "hdd.png", + HardwareType.Motherboard => "mainboard.png", + HardwareType.SuperIO => "chip.png", + HardwareType.Memory => "ram.png", + HardwareType.Cooler => "fan.png", + HardwareType.Network => "nic.png", + HardwareType.Psu => "power-supply.png", + HardwareType.Battery => "battery.png", + HardwareType.PowerMonitor => "powermonitor.png", + _ => "cpu.png" + }; + } + + private static string GetTypeImageFile(string sensorTypeText) + { + return sensorTypeText switch + { + "Voltages" or "Currents" or "Conductivity" => "voltage.png", + "Clocks" or "Timings" or "Frequencies" => "clock.png", + "Load" => "load.png", + "Temperatures" => "temperature.png", + "Fans" => "fan.png", + "Flows" or "Humidity" => "flow.png", + "Controls" => "control.png", + "Levels" => "level.png", + "Powers" => "power.png", + "Noise" => "loudspeaker.png", + "Throughput" => "throughput.png", + "Data" or "Small Data" => "data.png", + "Energy" or "Time Spans" => "time.png", + _ => "power.png" + }; + } + + private static string GetContentType(string extension) + { + return extension.ToLowerInvariant() switch + { + ".css" => "text/css", + ".gif" => "image/gif", + ".htm" or ".html" => "text/html", + ".ico" => "image/x-icon", + ".jpg" or ".jpeg" => "image/jpeg", + ".js" => "application/javascript", + ".png" => "image/png", + ".txt" => "text/plain", + _ => "application/octet-stream" + }; + } + + private static IDictionary ParseQuery(string? query) + { + Dictionary result = new(StringComparer.OrdinalIgnoreCase); + if (string.IsNullOrWhiteSpace(query)) + return result; + + foreach (string part in query.TrimStart('?').Split('&', StringSplitOptions.RemoveEmptyEntries)) + { + string[] split = part.Split('=', 2); + string key = WebUtility.UrlDecode(split[0]); + string value = split.Length > 1 ? WebUtility.UrlDecode(split[1]) : ""; + result[key] = value; + } + + return result; + } + + public static string ComputeSHA256(string text) + { + using SHA256 hash = SHA256.Create(); + return string.Concat(hash.ComputeHash(Encoding.UTF8.GetBytes(text)).Select(item => item.ToString("x2", CultureInfo.InvariantCulture))); + } +} diff --git a/LibreHardwareMonitor.Windows.WinUI/Services/StartupService.cs b/LibreHardwareMonitor.Windows.WinUI/Services/StartupService.cs new file mode 100644 index 0000000..ed499c5 --- /dev/null +++ b/LibreHardwareMonitor.Windows.WinUI/Services/StartupService.cs @@ -0,0 +1,38 @@ +// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +// Copyright (C) LibreHardwareMonitor and Contributors. + +using System; +using System.IO; +using Microsoft.Win32; + +namespace LibreHardwareMonitor.Windows.WinUI.Services; + +public sealed class StartupService +{ + private const string RegistryPath = @"Software\Microsoft\Windows\CurrentVersion\Run"; + private const string RegistryValueName = nameof(LibreHardwareMonitor); + + public bool IsAvailable => OperatingSystem.IsWindows(); + + public bool Startup + { + get + { + using RegistryKey? registryKey = Registry.CurrentUser.OpenSubKey(RegistryPath); + string? value = registryKey?.GetValue(RegistryValueName) as string; + return string.Equals(value, Environment.ProcessPath, StringComparison.OrdinalIgnoreCase); + } + set + { + if (!IsAvailable) + throw new InvalidOperationException("Windows startup registration is not available on this platform."); + + using RegistryKey? registryKey = Registry.CurrentUser.CreateSubKey(RegistryPath); + if (value) + registryKey?.SetValue(RegistryValueName, Environment.ProcessPath ?? Path.Combine(AppContext.BaseDirectory, "LibreHardwareMonitor.Windows.WinUI.exe")); + else + registryKey?.DeleteValue(RegistryValueName, false); + } + } +} diff --git a/LibreHardwareMonitor.Windows.WinUI/Services/UpdateVisitor.cs b/LibreHardwareMonitor.Windows.WinUI/Services/UpdateVisitor.cs new file mode 100644 index 0000000..4fce4ea --- /dev/null +++ b/LibreHardwareMonitor.Windows.WinUI/Services/UpdateVisitor.cs @@ -0,0 +1,30 @@ +// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +// Copyright (C) LibreHardwareMonitor and Contributors. + +using LibreHardwareMonitor.Hardware; + +namespace LibreHardwareMonitor.Windows.WinUI.Services; + +public sealed class UpdateVisitor : IVisitor +{ + public void VisitComputer(IComputer computer) + { + computer.Traverse(this); + } + + public void VisitHardware(IHardware hardware) + { + hardware.Update(); + foreach (IHardware subHardware in hardware.SubHardware) + subHardware.Accept(this); + } + + public void VisitSensor(ISensor sensor) + { + } + + public void VisitParameter(IParameter parameter) + { + } +} diff --git a/LibreHardwareMonitor.Windows.WinUI/Utilities/SensorFormatter.cs b/LibreHardwareMonitor.Windows.WinUI/Utilities/SensorFormatter.cs new file mode 100644 index 0000000..3086091 --- /dev/null +++ b/LibreHardwareMonitor.Windows.WinUI/Utilities/SensorFormatter.cs @@ -0,0 +1,144 @@ +// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +// Copyright (C) LibreHardwareMonitor and Contributors. + +using System; +using System.Globalization; +using System.Text; +using LibreHardwareMonitor.Hardware; +using LibreHardwareMonitor.Windows.WinUI.ViewModels; + +namespace LibreHardwareMonitor.Windows.WinUI.Utilities; + +public static class SensorFormatter +{ + public static string GetFormatString(ISensor sensor) + { + return sensor.SensorType switch + { + SensorType.Voltage => "{0:F3} V", + SensorType.Current => "{0:F3} A", + SensorType.Clock => "{0:F1} MHz", + SensorType.Load => "{0:F1} %", + SensorType.Temperature => "{0:F1} \u00B0C", + SensorType.Fan => "{0:F0} RPM", + SensorType.Flow => "{0:F1} L/h", + SensorType.Control => "{0:F1} %", + SensorType.Level => "{0:F1} %", + SensorType.Power => "{0:F1} W", + SensorType.Data => "{0:F1} GB", + SensorType.SmallData => "{0:F1} MB", + SensorType.Factor => "{0:F3}", + SensorType.Frequency => "{0:F1} Hz", + SensorType.Throughput => "{0:F1} B/s", + SensorType.TimeSpan => "{0:g}", + SensorType.Timing => "{0:F3} ns", + SensorType.Energy => "{0:F0} mWh", + SensorType.Noise => "{0:F0} dBA", + SensorType.Conductivity => "{0:F1} \u00B5S/cm", + SensorType.Humidity => "{0:F0} %", + _ => "{0:F1}" + }; + } + + public static string FormatValue(ISensor sensor, float? value, TemperatureUnit temperatureUnit) + { + if (!value.HasValue) + return "-"; + + return sensor.SensorType switch + { + SensorType.Voltage => $"{value:F3} V", + SensorType.Current => $"{value:F3} A", + SensorType.Clock => $"{value:F1} MHz", + SensorType.Load => $"{value:F1} %", + SensorType.Temperature when temperatureUnit == TemperatureUnit.Fahrenheit => $"{CelsiusToFahrenheit(value):F1} \u00B0F", + SensorType.Temperature => $"{value:F1} \u00B0C", + SensorType.Fan => $"{value:F0} RPM", + SensorType.Flow => $"{value:F1} L/h", + SensorType.Control => $"{value:F1} %", + SensorType.Level => $"{value:F1} %", + SensorType.Power => $"{value:F1} W", + SensorType.Data => $"{value:F1} GB", + SensorType.SmallData => $"{value:F1} MB", + SensorType.Factor => $"{value:F3}", + SensorType.Frequency => $"{value:F1} Hz", + SensorType.Throughput => FormatThroughput(sensor, value.Value), + SensorType.TimeSpan => string.Format(CultureInfo.CurrentCulture, "{0:g}", TimeSpan.FromSeconds(value.Value)), + SensorType.Timing => $"{value:F3} ns", + SensorType.Energy => $"{value:F0} mWh", + SensorType.Noise => $"{value:F0} dBA", + SensorType.Conductivity => $"{value:F1} \u00B5S/cm", + SensorType.Humidity => $"{value:F0} %", + _ => value.Value.ToString("F1", CultureInfo.CurrentCulture) + }; + } + + public static double? GetPlotValue(ISensor sensor, TemperatureUnit temperatureUnit) + { + float? value = sensor.Value; + if (!value.HasValue) + return null; + + if (sensor.SensorType == SensorType.Temperature && temperatureUnit == TemperatureUnit.Fahrenheit) + return CelsiusToFahrenheit(value.Value); + + return value.Value; + } + + public static string GetToolTip(ISensor sensor, TemperatureUnit temperatureUnit) + { + StringBuilder builder = new(); + + if (sensor is ICriticalSensorLimits criticalSensorLimits) + OptionallyAppendRange(builder, sensor, temperatureUnit, criticalSensorLimits.CriticalLowLimit, criticalSensorLimits.CriticalHighLimit, "critical"); + + if (sensor is ISensorLimits sensorLimits) + OptionallyAppendRange(builder, sensor, temperatureUnit, sensorLimits.LowLimit, sensorLimits.HighLimit, "normal"); + + return builder.ToString(); + } + + private static void OptionallyAppendRange(StringBuilder builder, ISensor sensor, TemperatureUnit temperatureUnit, float? min, float? max, string kind) + { + if (min.HasValue) + { + builder.AppendLine(max.HasValue + ? $"{CultureInfo.CurrentUICulture.TextInfo.ToTitleCase(kind)} range: {FormatValue(sensor, min, temperatureUnit)} to {FormatValue(sensor, max, temperatureUnit)}." + : $"Minimal {kind} value: {FormatValue(sensor, min, temperatureUnit)}."); + } + else if (max.HasValue) + { + builder.AppendLine($"Maximal {kind} value: {FormatValue(sensor, max, temperatureUnit)}."); + } + } + + private static string FormatThroughput(ISensor sensor, float value) + { + if (sensor.Name == "Connection Speed") + { + return value switch + { + 100000000 => "100Mbps", + 1000000000 => "1Gbps", + _ when value < 1024 => $"{value:F0} bps", + _ when value < 1048576 => $"{value / 1024:F1} Kbps", + _ when value < 1073741824 => $"{value / 1048576:F1} Mbps", + _ => $"{value / 1073741824:F1} Gbps" + }; + } + + const int oneMegabyte = 1048576; + return value < oneMegabyte ? $"{value / 1024:F1} KB/s" : $"{value / oneMegabyte:F1} MB/s"; + } + + private static float? CelsiusToFahrenheit(float? valueInCelsius) + { + return valueInCelsius * 1.8f + 32; + } + + private static float CelsiusToFahrenheit(float valueInCelsius) + { + return valueInCelsius * 1.8f + 32; + } +} diff --git a/LibreHardwareMonitor.Windows.WinUI/Utilities/SensorTypeDisplay.cs b/LibreHardwareMonitor.Windows.WinUI/Utilities/SensorTypeDisplay.cs new file mode 100644 index 0000000..e76b682 --- /dev/null +++ b/LibreHardwareMonitor.Windows.WinUI/Utilities/SensorTypeDisplay.cs @@ -0,0 +1,89 @@ +// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +// Copyright (C) LibreHardwareMonitor and Contributors. + +using LibreHardwareMonitor.Hardware; + +namespace LibreHardwareMonitor.Windows.WinUI.Utilities; + +public static class SensorTypeDisplay +{ + public static string GetText(SensorType sensorType) + { + return sensorType switch + { + SensorType.Voltage => "Voltages", + SensorType.Current => "Currents", + SensorType.Clock => "Clocks", + SensorType.Load => "Load", + SensorType.Temperature => "Temperatures", + SensorType.Fan => "Fans", + SensorType.Flow => "Flows", + SensorType.Control => "Controls", + SensorType.Level => "Levels", + SensorType.Power => "Powers", + SensorType.Data => "Data", + SensorType.SmallData => "Data", + SensorType.Factor => "Factors", + SensorType.Frequency => "Frequencies", + SensorType.Throughput => "Throughput", + SensorType.TimeSpan => "Times", + SensorType.Timing => "Timings", + SensorType.Energy => "Capacities", + SensorType.Noise => "Noise Levels", + SensorType.Conductivity => "Conductivities", + SensorType.Humidity => "Humidity Levels", + _ => sensorType.ToString() + }; + } + + public static string GetGlyph(SensorType sensorType) + { + return sensorType switch + { + SensorType.Voltage => "\uE945", + SensorType.Current => "\uE945", + SensorType.Clock => "\uE916", + SensorType.Load => "\uE9D9", + SensorType.Temperature => "\uE9CA", + SensorType.Fan => "\uE9F3", + SensorType.Flow => "\uE9D5", + SensorType.Control => "\uE713", + SensorType.Level => "\uE9D2", + SensorType.Power => "\uE7E8", + SensorType.Data => "\uE8AB", + SensorType.SmallData => "\uE8AB", + SensorType.Factor => "\uE9D2", + SensorType.Frequency => "\uE916", + SensorType.Throughput => "\uE9D5", + SensorType.TimeSpan => "\uE121", + SensorType.Timing => "\uE121", + SensorType.Energy => "\uEBAA", + SensorType.Noise => "\uE767", + SensorType.Conductivity => "\uE945", + SensorType.Humidity => "\uE9CA", + _ => "\uE9D9" + }; + } + + public static string GetHardwareGlyph(HardwareType hardwareType) + { + return hardwareType switch + { + HardwareType.Motherboard => "\uE950", + HardwareType.SuperIO => "\uE950", + HardwareType.Cpu => "\uE950", + HardwareType.Memory => "\uE8AB", + HardwareType.GpuNvidia => "\uE7F4", + HardwareType.GpuAmd => "\uE7F4", + HardwareType.GpuIntel => "\uE7F4", + HardwareType.Storage => "\uEDA2", + HardwareType.Network => "\uE968", + HardwareType.Cooler => "\uE9F3", + HardwareType.EmbeddedController => "\uE950", + HardwareType.Psu => "\uE7E8", + HardwareType.Battery => "\uEBAA", + _ => "\uE950" + }; + } +} diff --git a/LibreHardwareMonitor.Windows.WinUI/ViewModels/AppThemeMode.cs b/LibreHardwareMonitor.Windows.WinUI/ViewModels/AppThemeMode.cs new file mode 100644 index 0000000..992bf50 --- /dev/null +++ b/LibreHardwareMonitor.Windows.WinUI/ViewModels/AppThemeMode.cs @@ -0,0 +1,13 @@ +// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +// Copyright (C) LibreHardwareMonitor and Contributors. + +namespace LibreHardwareMonitor.Windows.WinUI.ViewModels; + +public enum AppThemeMode +{ + Auto, + Light, + Dark, + Black +} diff --git a/LibreHardwareMonitor.Windows.WinUI/ViewModels/MainWindowViewModel.cs b/LibreHardwareMonitor.Windows.WinUI/ViewModels/MainWindowViewModel.cs new file mode 100644 index 0000000..1acd452 --- /dev/null +++ b/LibreHardwareMonitor.Windows.WinUI/ViewModels/MainWindowViewModel.cs @@ -0,0 +1,878 @@ +// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +// Copyright (C) LibreHardwareMonitor and Contributors. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Threading.Tasks; +using LibreHardwareMonitor.Hardware; +using LibreHardwareMonitor.Hardware.Storage; +using LibreHardwareMonitor.Windows.WinUI.Services; +using LibreHardwareMonitor.Windows.WinUI.Utilities; +using Microsoft.UI.Xaml; +using Windows.UI; + +namespace LibreHardwareMonitor.Windows.WinUI.ViewModels; + +public sealed class MainWindowViewModel : ViewModelBase, IDisposable +{ + private static readonly TimeSpan[] LoggingIntervals = + [ + TimeSpan.FromSeconds(1), + TimeSpan.FromSeconds(2), + TimeSpan.FromSeconds(5), + TimeSpan.FromSeconds(10), + TimeSpan.FromSeconds(30), + TimeSpan.FromMinutes(1), + TimeSpan.FromMinutes(2), + TimeSpan.FromMinutes(5), + TimeSpan.FromMinutes(10), + TimeSpan.FromMinutes(30), + TimeSpan.FromHours(1), + TimeSpan.FromHours(2), + TimeSpan.FromHours(6) + ]; + + private static readonly TimeSpan[] UpdateIntervals = + [ + TimeSpan.FromMilliseconds(250), + TimeSpan.FromMilliseconds(500), + TimeSpan.FromSeconds(1), + TimeSpan.FromSeconds(2), + TimeSpan.FromSeconds(5), + TimeSpan.FromSeconds(10) + ]; + + private static readonly TimeSpan[] SensorValuesTimeWindows = + [ + TimeSpan.FromSeconds(30), + TimeSpan.FromMinutes(1), + TimeSpan.FromMinutes(2), + TimeSpan.FromMinutes(5), + TimeSpan.FromMinutes(10), + TimeSpan.FromMinutes(30), + TimeSpan.FromHours(1), + TimeSpan.FromHours(2), + TimeSpan.FromHours(6), + TimeSpan.FromHours(12), + TimeSpan.FromHours(24) + ]; + + private static readonly Color[] PlotColors = + [ + Color.FromArgb(255, 0x00, 0x78, 0xD4), + Color.FromArgb(255, 0xE8, 0x11, 0x23), + Color.FromArgb(255, 0x10, 0x7C, 0x10), + Color.FromArgb(255, 0xF7, 0x63, 0x0C), + Color.FromArgb(255, 0x88, 0x17, 0x98), + Color.FromArgb(255, 0x00, 0xB7, 0xC3), + Color.FromArgb(255, 0x49, 0x8B, 0x00), + Color.FromArgb(255, 0xA8, 0x00, 0x00) + ]; + + private readonly HardwareMonitorService _hardwareMonitor; + private readonly Logger _logger; + private readonly Dictionary _plotSeriesByIdentifier = new(); + private readonly RemoteWebServer _remoteWebServer; + private readonly StartupService _startupService = new(); + private AppThemeMode _themeMode; + private int _loggingIntervalIndex; + private PlotLocation _plotLocation; + private double _plotStrokeThickness; + private SensorTreeItemViewModel? _selectedItem; + private int _sensorValuesTimeWindowIndex; + private bool _showHiddenSensors; + private bool _showMaxColumn; + private bool _showMinColumn; + private bool _showPlot; + private bool _showValueColumn; + private bool _throttleAtaUpdate; + private bool _isStarted; + private bool _isStarting; + private string _statusText = ""; + private TemperatureUnit _temperatureUnit; + private int _updateIntervalIndex; + + public MainWindowViewModel(AppSettings settings) + { + Settings = settings; + _hardwareMonitor = new HardwareMonitorService(settings); + _logger = new Logger(_hardwareMonitor.Computer); + _remoteWebServer = new RemoteWebServer( + () => RootItems.FirstOrDefault(), + _hardwareMonitor.Computer, + settings.GetValue("listenerIp", "?"), + settings.GetValue("listenerPort", 8085), + settings.GetValue("authenticationEnabled", false), + settings.GetValue("authenticationUserName", ""), + settings.GetValue("authenticationPassword", "")); + _hardwareMonitor.TreeRebuilt += HardwareMonitor_TreeRebuilt; + + _themeMode = ParseThemeMode(settings.GetValue("theme", "auto")); + _showHiddenSensors = settings.GetValue("hiddenMenuItem", false); + _showValueColumn = settings.GetValue("valueMenuItem", true); + _showMinColumn = settings.GetValue("minMenuItem", false); + _showMaxColumn = settings.GetValue("maxMenuItem", true); + _showPlot = settings.GetValue("plotMenuItem", false); + _plotLocation = (PlotLocation)Math.Clamp(settings.GetValue("plotLocation", 0), 0, 1); + _plotStrokeThickness = Math.Clamp(settings.GetValue("plotStroke", 1) + 1, 1, 4); + _temperatureUnit = (TemperatureUnit)Math.Clamp(settings.GetValue("TemperatureUnit", 0), 0, 1); + _updateIntervalIndex = Math.Clamp(settings.GetValue("updateIntervalMenuItem", 2), 0, UpdateIntervals.Length - 1); + _loggingIntervalIndex = Math.Clamp(settings.GetValue("loggingInterval", 0), 0, LoggingIntervals.Length - 1); + _sensorValuesTimeWindowIndex = Math.Clamp(settings.GetValue("sensorValuesTimeWindow", 10), 0, SensorValuesTimeWindows.Length - 1); + _throttleAtaUpdate = settings.GetValue("throttleAtaUpdateMenuItem", false); + StorageDevice.ThrottleInterval = _throttleAtaUpdate ? TimeSpan.FromSeconds(30) : TimeSpan.Zero; + _logger.LoggingInterval = LoggingIntervals[_loggingIntervalIndex]; + _logger.FileRotationMethod = (LoggerFileRotation)Math.Clamp(settings.GetValue("logger.fileRotation", 0), 0, 1); + } + + public event EventHandler? PlotInvalidated; + + public Visibility MaxColumnVisibility => ShowMaxColumn ? Visibility.Visible : Visibility.Collapsed; + + public Visibility MinColumnVisibility => ShowMinColumn ? Visibility.Visible : Visibility.Collapsed; + + public ObservableCollection PlotSeries { get; } = []; + + public Visibility PlotVisibility => ShowPlot ? Visibility.Visible : Visibility.Collapsed; + + public ObservableCollection RootItems { get; } = []; + + public SensorTreeItemViewModel? SelectedItem + { + get => _selectedItem; + set => SetProperty(ref _selectedItem, value); + } + + public AppSettings Settings { get; } + + public string StatusText + { + get => _statusText; + private set => SetProperty(ref _statusText, value); + } + + public AppThemeMode ThemeMode + { + get => _themeMode; + set + { + if (!SetProperty(ref _themeMode, value)) + return; + + Settings.SetValue("theme", FormatThemeMode(value)); + } + } + + public TimeSpan UpdateInterval => UpdateIntervals[UpdateIntervalIndex]; + + public Visibility ValueColumnVisibility => ShowValueColumn ? Visibility.Visible : Visibility.Collapsed; + + public bool AuthWebServer + { + get => _remoteWebServer.AuthEnabled; + set + { + if (_remoteWebServer.AuthEnabled == value) + return; + + _remoteWebServer.AuthEnabled = value; + Settings.SetValue("authenticationEnabled", value); + RestartWebServerIfRunning(); + OnPropertyChanged(); + } + } + + public string AuthWebServerUserName + { + get => _remoteWebServer.UserName; + set + { + if (_remoteWebServer.UserName == value) + return; + + _remoteWebServer.UserName = value; + Settings.SetValue("authenticationUserName", value); + RestartWebServerIfRunning(); + OnPropertyChanged(); + } + } + + public LoggerFileRotation FileRotationMethod + { + get => _logger.FileRotationMethod; + set + { + if (_logger.FileRotationMethod == value) + return; + + _logger.FileRotationMethod = value; + Settings.SetValue("logger.fileRotation", (int)value); + OnPropertyChanged(); + } + } + + public bool ForceDriveWakeup + { + get => _hardwareMonitor.ForceDriveWakeup; + set + { + if (_hardwareMonitor.ForceDriveWakeup == value) + return; + + _hardwareMonitor.ForceDriveWakeup = value; + OnPropertyChanged(); + } + } + + public bool IsBatteryEnabled + { + get => _hardwareMonitor.IsBatteryEnabled; + set => SetHardwareFlag(value, _hardwareMonitor.IsBatteryEnabled, v => _hardwareMonitor.IsBatteryEnabled = v); + } + + public bool IsControllerEnabled + { + get => _hardwareMonitor.IsControllerEnabled; + set => SetHardwareFlag(value, _hardwareMonitor.IsControllerEnabled, v => _hardwareMonitor.IsControllerEnabled = v); + } + + public bool IsCpuEnabled + { + get => _hardwareMonitor.IsCpuEnabled; + set => SetHardwareFlag(value, _hardwareMonitor.IsCpuEnabled, v => _hardwareMonitor.IsCpuEnabled = v); + } + + public bool IsGpuEnabled + { + get => _hardwareMonitor.IsGpuEnabled; + set => SetHardwareFlag(value, _hardwareMonitor.IsGpuEnabled, v => _hardwareMonitor.IsGpuEnabled = v); + } + + public bool IsMemoryEnabled + { + get => _hardwareMonitor.IsMemoryEnabled; + set => SetHardwareFlag(value, _hardwareMonitor.IsMemoryEnabled, v => _hardwareMonitor.IsMemoryEnabled = v); + } + + public bool IsMotherboardEnabled + { + get => _hardwareMonitor.IsMotherboardEnabled; + set => SetHardwareFlag(value, _hardwareMonitor.IsMotherboardEnabled, v => _hardwareMonitor.IsMotherboardEnabled = v); + } + + public bool IsNetworkEnabled + { + get => _hardwareMonitor.IsNetworkEnabled; + set => SetHardwareFlag(value, _hardwareMonitor.IsNetworkEnabled, v => _hardwareMonitor.IsNetworkEnabled = v); + } + + public bool IsPowerMonitorEnabled + { + get => _hardwareMonitor.IsPowerMonitorEnabled; + set => SetHardwareFlag(value, _hardwareMonitor.IsPowerMonitorEnabled, v => _hardwareMonitor.IsPowerMonitorEnabled = v); + } + + public bool IsPsuEnabled + { + get => _hardwareMonitor.IsPsuEnabled; + set => SetHardwareFlag(value, _hardwareMonitor.IsPsuEnabled, v => _hardwareMonitor.IsPsuEnabled = v); + } + + public bool IsStorageEnabled + { + get => _hardwareMonitor.IsStorageEnabled; + set => SetHardwareFlag(value, _hardwareMonitor.IsStorageEnabled, v => _hardwareMonitor.IsStorageEnabled = v); + } + + public int LoggingIntervalIndex + { + get => _loggingIntervalIndex; + set + { + value = Math.Clamp(value, 0, LoggingIntervals.Length - 1); + if (!SetProperty(ref _loggingIntervalIndex, value)) + return; + + Settings.SetValue("loggingInterval", value); + _logger.LoggingInterval = LoggingIntervals[value]; + } + } + + public string ListenerIp + { + get => _remoteWebServer.ListenerIp; + set + { + if (_remoteWebServer.ListenerIp == value) + return; + + _remoteWebServer.ListenerIp = value; + Settings.SetValue("listenerIp", value); + RestartWebServerIfRunning(); + OnPropertyChanged(); + OnPropertyChanged(nameof(WebServerUrl)); + } + } + + public int ListenerPort + { + get => _remoteWebServer.ListenerPort; + set + { + value = Math.Clamp(value, 1, 65535); + if (_remoteWebServer.ListenerPort == value) + return; + + _remoteWebServer.ListenerPort = value; + Settings.SetValue("listenerPort", value); + RestartWebServerIfRunning(); + OnPropertyChanged(); + OnPropertyChanged(nameof(WebServerUrl)); + } + } + + public bool LogSensors + { + get => Settings.GetValue("logSensorsMenuItem", false); + set + { + if (LogSensors == value) + return; + + Settings.SetValue("logSensorsMenuItem", value); + OnPropertyChanged(); + } + } + + public bool RunWebServer + { + get => !IsWebServerUnavailable && Settings.GetValue("runWebServerMenuItem", false); + set + { + if (IsWebServerUnavailable || RunWebServer == value) + return; + + if (value) + { + if (!_remoteWebServer.Start()) + { + Settings.SetValue("runWebServerMenuItem", false); + StatusText = "Remote web server failed to start."; + OnPropertyChanged(); + return; + } + } + else + { + _remoteWebServer.Stop(); + } + + Settings.SetValue("runWebServerMenuItem", value); + OnPropertyChanged(); + UpdateStatus(); + } + } + + public int SensorValuesTimeWindowIndex + { + get => _sensorValuesTimeWindowIndex; + set + { + value = Math.Clamp(value, 0, SensorValuesTimeWindows.Length - 1); + if (!SetProperty(ref _sensorValuesTimeWindowIndex, value)) + return; + + Settings.SetValue("sensorValuesTimeWindow", value); + ApplySensorValuesTimeWindow(); + } + } + + public bool MinimizeOnClose + { + get => Settings.GetValue("minCloseMenuItem", false); + set => SetBooleanSetting("minCloseMenuItem", value); + } + + public bool MinimizeToTray + { + get => Settings.GetValue("minTrayMenuItem", true); + set => SetBooleanSetting("minTrayMenuItem", value); + } + + public PlotLocation PlotLocation + { + get => _plotLocation; + set + { + if (!SetProperty(ref _plotLocation, value)) + return; + + Settings.SetValue("plotLocation", (int)value); + NotifyPlotLayoutChanged(); + } + } + + public int PlotGridColumn => PlotLocation == PlotLocation.Right ? 1 : 0; + + public int PlotGridColumnSpan => PlotLocation == PlotLocation.Bottom ? 2 : 1; + + public int PlotGridRow => PlotLocation == PlotLocation.Bottom ? 1 : 0; + + public int PlotGridRowSpan => PlotLocation == PlotLocation.Right ? 2 : 1; + + public double PlotStrokeThickness + { + get => _plotStrokeThickness; + set + { + value = Math.Clamp(value, 1, 4); + if (!SetProperty(ref _plotStrokeThickness, value)) + return; + + Settings.SetValue("plotStroke", (int)value - 1); + PlotInvalidated?.Invoke(this, EventArgs.Empty); + } + } + + public bool ShowHiddenSensors + { + get => _showHiddenSensors; + set + { + if (!SetProperty(ref _showHiddenSensors, value)) + return; + + Settings.SetValue("hiddenMenuItem", value); + RootItems.FirstOrDefault()?.SetShowHiddenSensors(value); + } + } + + public bool ShowMaxColumn + { + get => _showMaxColumn; + set + { + if (!SetProperty(ref _showMaxColumn, value)) + return; + + Settings.SetValue("maxMenuItem", value); + NotifyColumnVisibilityChanged(); + } + } + + public bool ShowMinColumn + { + get => _showMinColumn; + set + { + if (!SetProperty(ref _showMinColumn, value)) + return; + + Settings.SetValue("minMenuItem", value); + NotifyColumnVisibilityChanged(); + } + } + + public bool ShowPlot + { + get => _showPlot; + set + { + if (!SetProperty(ref _showPlot, value)) + return; + + Settings.SetValue("plotMenuItem", value); + OnPropertyChanged(nameof(PlotVisibility)); + NotifyPlotLayoutChanged(); + PlotInvalidated?.Invoke(this, EventArgs.Empty); + } + } + + public bool ShowValueColumn + { + get => _showValueColumn; + set + { + if (!SetProperty(ref _showValueColumn, value)) + return; + + Settings.SetValue("valueMenuItem", value); + NotifyColumnVisibilityChanged(); + } + } + + public bool StartMinimized + { + get => Settings.GetValue("startMinMenuItem", false); + set => SetBooleanSetting("startMinMenuItem", value); + } + + public bool ThrottleAtaUpdate + { + get => _throttleAtaUpdate; + set + { + if (!SetProperty(ref _throttleAtaUpdate, value)) + return; + + Settings.SetValue("throttleAtaUpdateMenuItem", value); + StorageDevice.ThrottleInterval = value ? TimeSpan.FromSeconds(30) : TimeSpan.Zero; + } + } + + public bool IsWebServerUnavailable => _remoteWebServer.PlatformNotSupported; + + public string WebServerUrl + { + get + { + string host = ListenerIp is "?" or "+" or "*" or "0.0.0.0" ? "localhost" : ListenerIp; + return $"http://{host}:{ListenerPort}/"; + } + } + + public bool RunOnStartup + { + get => _startupService.IsAvailable && _startupService.Startup; + set + { + if (!_startupService.IsAvailable || RunOnStartup == value) + return; + + try + { + _startupService.Startup = value; + OnPropertyChanged(); + } + catch (InvalidOperationException) + { + OnPropertyChanged(); + } + } + } + + public int SensorGridColumnSpan => ShowPlot && PlotLocation == PlotLocation.Right ? 1 : 2; + + public int SensorGridRowSpan => ShowPlot && PlotLocation == PlotLocation.Bottom ? 1 : 2; + + public TemperatureUnit TemperatureUnit + { + get => _temperatureUnit; + set + { + if (!SetProperty(ref _temperatureUnit, value)) + return; + + Settings.SetValue("TemperatureUnit", (int)value); + RootItems.FirstOrDefault()?.SetTemperatureUnit(value); + PlotInvalidated?.Invoke(this, EventArgs.Empty); + } + } + + public int UpdateIntervalIndex + { + get => _updateIntervalIndex; + set + { + value = Math.Clamp(value, 0, UpdateIntervals.Length - 1); + if (!SetProperty(ref _updateIntervalIndex, value)) + return; + + Settings.SetValue("updateIntervalMenuItem", value); + OnPropertyChanged(nameof(UpdateInterval)); + } + } + + public void Dispose() + { + _hardwareMonitor.TreeRebuilt -= HardwareMonitor_TreeRebuilt; + Settings.SetValue("listenerIp", ListenerIp); + Settings.SetValue("listenerPort", ListenerPort); + Settings.SetValue("authenticationEnabled", AuthWebServer); + Settings.SetValue("authenticationUserName", AuthWebServerUserName); + Settings.SetValue("authenticationPassword", _remoteWebServer.PasswordSHA256); + _remoteWebServer.Quit(); + _hardwareMonitor.Dispose(); + Save(); + } + + public string GetReport() + { + return _hardwareMonitor.Computer.GetReport(); + } + + public void ResetHardware() + { + _hardwareMonitor.Reset(); + UpdateStatus(); + } + + public void ResetMinMax() + { + _hardwareMonitor.ResetMinMax(); + RootItems.FirstOrDefault()?.RefreshValues(); + } + + public void ResetPlot() + { + _hardwareMonitor.ClearSensorValues(); + _plotSeriesByIdentifier.Clear(); + PlotSeries.Clear(); + PlotInvalidated?.Invoke(this, EventArgs.Empty); + } + + public void Save() + { + Settings.Save(); + } + + public void SetStatusText(string statusText) + { + StatusText = statusText; + } + + public void SetAllExpanded(bool isExpanded) + { + RootItems.FirstOrDefault()?.SetAllExpanded(isExpanded); + } + + public void SetAuthPassword(string plainPassword) + { + if (string.IsNullOrWhiteSpace(plainPassword)) + return; + + _remoteWebServer.SetPassword(plainPassword); + Settings.SetValue("authenticationPassword", _remoteWebServer.PasswordSHA256); + RestartWebServerIfRunning(); + } + + public void SetSensorPenColor(SensorTreeItemViewModel item, Color? color) + { + if (item.Sensor == null) + return; + + item.PenColor = color; + string identifier = item.Sensor.Identifier.ToString(); + if (_plotSeriesByIdentifier.TryGetValue(identifier, out PlotSeriesViewModel? series)) + series.Color = GetPlotColor(item); + + PlotInvalidated?.Invoke(this, EventArgs.Empty); + } + + public async Task StartAsync() + { + if (_isStarted || _isStarting) + return; + + _isStarting = true; + try + { + StatusText = "Initializing hardware sensors..."; + await Task.Run(() => _hardwareMonitor.Open(raiseTreeRebuilt: false)); + UpdateRoot(); + ApplySensorValuesTimeWindow(); + StartWebServerFromSettings(); + UpdateStatus(); + _isStarted = true; + } + finally + { + _isStarting = false; + } + } + + public async Task UpdateAsync() + { + if (!_isStarted) + return; + + await _hardwareMonitor.UpdateAsync(); + + SensorTreeItemViewModel? root = RootItems.FirstOrDefault(); + root?.RefreshValues(); + TrackPlotPoints(); + + if (LogSensors) + _logger.Log(); + + UpdateStatus(); + } + + private void HardwareMonitor_TreeRebuilt(object? sender, EventArgs e) + { + UpdateRoot(); + } + + private void ApplySensorValuesTimeWindow() + { + TimeSpan timeWindow = SensorValuesTimeWindows[SensorValuesTimeWindowIndex]; + SensorVisitor visitor = new(sensor => sensor.ValuesTimeWindow = timeWindow); + visitor.VisitComputer(_hardwareMonitor.Computer); + } + + private void NotifyColumnVisibilityChanged() + { + OnPropertyChanged(nameof(ValueColumnVisibility)); + OnPropertyChanged(nameof(MinColumnVisibility)); + OnPropertyChanged(nameof(MaxColumnVisibility)); + RootItems.FirstOrDefault()?.SetColumnVisibility(ShowValueColumn, ShowMinColumn, ShowMaxColumn); + } + + private void NotifyPlotLayoutChanged() + { + OnPropertyChanged(nameof(PlotGridRow)); + OnPropertyChanged(nameof(PlotGridColumn)); + OnPropertyChanged(nameof(PlotGridRowSpan)); + OnPropertyChanged(nameof(PlotGridColumnSpan)); + OnPropertyChanged(nameof(SensorGridRowSpan)); + OnPropertyChanged(nameof(SensorGridColumnSpan)); + } + + private void SetBooleanSetting(string settingName, bool value) + { + if (Settings.GetValue(settingName, false) == value) + return; + + Settings.SetValue(settingName, value); + OnPropertyChanged(); + } + + private void SetHardwareFlag(bool value, bool currentValue, Action setValue) + { + if (currentValue == value) + return; + + setValue(value); + UpdateRoot(); + OnPropertyChanged(); + } + + private void TrackPlotPoints() + { + SensorTreeItemViewModel? root = RootItems.FirstOrDefault(); + if (root == null) + return; + + DateTime now = DateTime.Now; + HashSet selectedIdentifiers = new(); + foreach (SensorTreeItemViewModel sensorItem in root.EnumerateSensors().Where(sensorItem => sensorItem.Plot && sensorItem.Sensor != null)) + { + ISensor sensor = sensorItem.Sensor!; + string identifier = sensor.Identifier.ToString(); + selectedIdentifiers.Add(identifier); + double? value = SensorFormatter.GetPlotValue(sensor, TemperatureUnit); + if (!value.HasValue) + continue; + + if (!_plotSeriesByIdentifier.TryGetValue(identifier, out PlotSeriesViewModel? series)) + { + series = new PlotSeriesViewModel(identifier, sensor.Name, GetPlotColor(sensorItem)); + _plotSeriesByIdentifier[identifier] = series; + PlotSeries.Add(series); + } + else + { + series.Color = GetPlotColor(sensorItem); + } + + series.Points.Add(new PlotPointViewModel(now, value.Value)); + while (series.Points.Count > 0 && now - series.Points[0].Timestamp > TimeSpan.FromHours(24)) + series.Points.RemoveAt(0); + } + + foreach (string identifier in _plotSeriesByIdentifier.Keys.Where(identifier => !selectedIdentifiers.Contains(identifier)).ToArray()) + { + PlotSeriesViewModel series = _plotSeriesByIdentifier[identifier]; + _plotSeriesByIdentifier.Remove(identifier); + PlotSeries.Remove(series); + } + } + + private void UpdateRoot() + { + RootItems.Clear(); + SensorTreeItemViewModel root = _hardwareMonitor.Root; + root.Configure(TemperatureUnit, ShowHiddenSensors, ShowValueColumn, ShowMinColumn, ShowMaxColumn); + RootItems.Add(root); + ApplySensorValuesTimeWindow(); + } + + private void UpdateStatus() + { + int hardwareCount = _hardwareMonitor.Computer.Hardware.Count; + int sensorCount = RootItems.FirstOrDefault()?.EnumerateSensors().Count() ?? 0; + string webServerStatus = RunWebServer ? $", web server {WebServerUrl}" : ""; + StatusText = $"{hardwareCount} hardware devices, {sensorCount} sensors, update every {FormatInterval(UpdateInterval)}{webServerStatus}"; + } + + private Color GetPlotColor(SensorTreeItemViewModel sensorItem) + { + return sensorItem.PenColor ?? PlotColors[_plotSeriesByIdentifier.Count % PlotColors.Length]; + } + + private void RestartWebServerIfRunning() + { + if (!RunWebServer) + return; + + _remoteWebServer.Stop(); + if (!_remoteWebServer.Start()) + { + Settings.SetValue("runWebServerMenuItem", false); + StatusText = "Remote web server failed to restart."; + } + + OnPropertyChanged(nameof(RunWebServer)); + UpdateStatus(); + } + + private void StartWebServerFromSettings() + { + if (IsWebServerUnavailable || !Settings.GetValue("runWebServerMenuItem", false)) + return; + + if (!_remoteWebServer.Start()) + { + Settings.SetValue("runWebServerMenuItem", false); + StatusText = "Remote web server failed to start."; + } + + OnPropertyChanged(nameof(RunWebServer)); + } + + private static AppThemeMode ParseThemeMode(string value) + { + return value.ToLowerInvariant() switch + { + "light" => AppThemeMode.Light, + "dark" => AppThemeMode.Dark, + "black" => AppThemeMode.Black, + _ => AppThemeMode.Auto + }; + } + + private static string FormatThemeMode(AppThemeMode themeMode) + { + return themeMode switch + { + AppThemeMode.Light => "light", + AppThemeMode.Dark => "dark", + AppThemeMode.Black => "black", + _ => "auto" + }; + } + + private static string FormatInterval(TimeSpan interval) + { + if (interval.TotalMilliseconds < 1000) + return $"{interval.TotalMilliseconds:F0}ms"; + if (interval.TotalSeconds < 60) + return $"{interval.TotalSeconds:F0}s"; + if (interval.TotalMinutes < 60) + return $"{interval.TotalMinutes:F0}min"; + return $"{interval.TotalHours:F0}h"; + } +} diff --git a/LibreHardwareMonitor.Windows.WinUI/ViewModels/PlotLocation.cs b/LibreHardwareMonitor.Windows.WinUI/ViewModels/PlotLocation.cs new file mode 100644 index 0000000..b8d5a86 --- /dev/null +++ b/LibreHardwareMonitor.Windows.WinUI/ViewModels/PlotLocation.cs @@ -0,0 +1,11 @@ +// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +// Copyright (C) LibreHardwareMonitor and Contributors. + +namespace LibreHardwareMonitor.Windows.WinUI.ViewModels; + +public enum PlotLocation +{ + Bottom = 0, + Right = 1 +} diff --git a/LibreHardwareMonitor.Windows.WinUI/ViewModels/PlotPointViewModel.cs b/LibreHardwareMonitor.Windows.WinUI/ViewModels/PlotPointViewModel.cs new file mode 100644 index 0000000..e9bc473 --- /dev/null +++ b/LibreHardwareMonitor.Windows.WinUI/ViewModels/PlotPointViewModel.cs @@ -0,0 +1,20 @@ +// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +// Copyright (C) LibreHardwareMonitor and Contributors. + +using System; + +namespace LibreHardwareMonitor.Windows.WinUI.ViewModels; + +public sealed class PlotPointViewModel +{ + public PlotPointViewModel(DateTime timestamp, double value) + { + Timestamp = timestamp; + Value = value; + } + + public DateTime Timestamp { get; } + + public double Value { get; } +} diff --git a/LibreHardwareMonitor.Windows.WinUI/ViewModels/PlotSeriesViewModel.cs b/LibreHardwareMonitor.Windows.WinUI/ViewModels/PlotSeriesViewModel.cs new file mode 100644 index 0000000..e7123ae --- /dev/null +++ b/LibreHardwareMonitor.Windows.WinUI/ViewModels/PlotSeriesViewModel.cs @@ -0,0 +1,26 @@ +// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +// Copyright (C) LibreHardwareMonitor and Contributors. + +using System.Collections.ObjectModel; +using Windows.UI; + +namespace LibreHardwareMonitor.Windows.WinUI.ViewModels; + +public sealed class PlotSeriesViewModel +{ + public PlotSeriesViewModel(string sensorIdentifier, string name, Color color) + { + SensorIdentifier = sensorIdentifier; + Name = name; + Color = color; + } + + public Color Color { get; set; } + + public string Name { get; } + + public ObservableCollection Points { get; } = []; + + public string SensorIdentifier { get; } +} diff --git a/LibreHardwareMonitor.Windows.WinUI/ViewModels/SensorTreeItemKind.cs b/LibreHardwareMonitor.Windows.WinUI/ViewModels/SensorTreeItemKind.cs new file mode 100644 index 0000000..c1b557d --- /dev/null +++ b/LibreHardwareMonitor.Windows.WinUI/ViewModels/SensorTreeItemKind.cs @@ -0,0 +1,13 @@ +// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +// Copyright (C) LibreHardwareMonitor and Contributors. + +namespace LibreHardwareMonitor.Windows.WinUI.ViewModels; + +public enum SensorTreeItemKind +{ + Root, + Hardware, + SensorType, + Sensor +} diff --git a/LibreHardwareMonitor.Windows.WinUI/ViewModels/SensorTreeItemViewModel.cs b/LibreHardwareMonitor.Windows.WinUI/ViewModels/SensorTreeItemViewModel.cs new file mode 100644 index 0000000..8d8d91e --- /dev/null +++ b/LibreHardwareMonitor.Windows.WinUI/ViewModels/SensorTreeItemViewModel.cs @@ -0,0 +1,347 @@ +// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +// Copyright (C) LibreHardwareMonitor and Contributors. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Text; +using LibreHardwareMonitor.Hardware; +using LibreHardwareMonitor.Windows.WinUI.Services; +using LibreHardwareMonitor.Windows.WinUI.Utilities; +using Microsoft.UI.Xaml; +using Windows.UI; + +namespace LibreHardwareMonitor.Windows.WinUI.ViewModels; + +public sealed class SensorTreeItemViewModel : ViewModelBase +{ + private readonly AppSettings? _settings; + private string? _expandedSettingName; + private bool _isExpanded = true; + private bool _isVisible = true; + private bool _plot; + private bool _showHiddenSensors; + private Visibility _maxColumnVisibility = Visibility.Visible; + private Visibility _minColumnVisibility = Visibility.Collapsed; + private Visibility _rowVisibility = Visibility.Visible; + private Visibility _valueColumnVisibility = Visibility.Visible; + private Color? _penColor; + private TemperatureUnit _temperatureUnit; + + private SensorTreeItemViewModel(SensorTreeItemKind kind, AppSettings? settings) + { + Kind = kind; + _settings = settings; + } + + public ObservableCollection Children { get; } = []; + + public bool CanHide => Sensor != null; + + public bool CanRename => Sensor != null || Hardware != null; + + public IHardware? Hardware { get; private init; } + + public string IconGlyph { get; private init; } = "\uE950"; + + public bool IsExpanded + { + get => _isExpanded; + set + { + if (SetProperty(ref _isExpanded, value) && _expandedSettingName != null) + _settings?.SetValue(_expandedSettingName, value); + } + } + + public bool IsVisible + { + get => _isVisible; + set + { + if (!SetProperty(ref _isVisible, value)) + return; + + if (Sensor != null) + _settings?.SetValue(new Identifier(Sensor.Identifier, "hidden").ToString(), !value); + + UpdateVisibilityState(); + } + } + + public SensorTreeItemKind Kind { get; } + + public string Max => Sensor == null ? "" : SensorFormatter.FormatValue(Sensor, Sensor.Max, TemperatureUnit); + + public Visibility MaxColumnVisibility + { + get => _maxColumnVisibility; + private set => SetProperty(ref _maxColumnVisibility, value); + } + + public string Min => Sensor == null ? "" : SensorFormatter.FormatValue(Sensor, Sensor.Min, TemperatureUnit); + + public Visibility MinColumnVisibility + { + get => _minColumnVisibility; + private set => SetProperty(ref _minColumnVisibility, value); + } + + public bool Plot + { + get => _plot; + set + { + if (Sensor == null || !SetProperty(ref _plot, value)) + return; + + _settings?.SetValue(new Identifier(Sensor.Identifier, "plot").ToString(), value); + } + } + + public Color? PenColor + { + get => _penColor; + set + { + if (Sensor == null || Nullable.Equals(_penColor, value)) + return; + + _penColor = value; + string settingName = new Identifier(Sensor.Identifier, "penColor").ToString(); + if (value.HasValue) + _settings?.SetValue(settingName, value.Value); + else + _settings?.Remove(settingName); + + OnPropertyChanged(); + } + } + + public Visibility PlotCheckVisibility => Sensor == null ? Visibility.Collapsed : Visibility.Visible; + + public Visibility RowVisibility + { + get => _rowVisibility; + private set => SetProperty(ref _rowVisibility, value); + } + + public ISensor? Sensor { get; private init; } + + public string Text + { + get + { + if (Sensor != null) + return Sensor.Name; + if (Hardware != null) + return Hardware.Name; + return _text; + } + set + { + if (Sensor != null) + Sensor.Name = value; + else if (Hardware != null) + Hardware.Name = value; + else + _text = value; + + OnPropertyChanged(); + } + } + + public string ToolTip + { + get + { + if (Sensor != null) + return SensorFormatter.GetToolTip(Sensor, TemperatureUnit); + + if (Hardware?.Properties.Count > 0) + { + StringBuilder builder = new(); + builder.AppendLine("Hardware properties:"); + foreach (KeyValuePair property in Hardware.Properties) + builder.AppendFormat(" - {0}: {1}{2}", property.Key, property.Value, Environment.NewLine); + return builder.ToString(); + } + + return ""; + } + } + + public TemperatureUnit TemperatureUnit + { + get => _temperatureUnit; + private set + { + if (!SetProperty(ref _temperatureUnit, value)) + return; + + RefreshValues(); + } + } + + public string Value => Sensor == null ? "" : SensorFormatter.FormatValue(Sensor, Sensor.Value, TemperatureUnit); + + public Visibility ValueColumnVisibility + { + get => _valueColumnVisibility; + private set => SetProperty(ref _valueColumnVisibility, value); + } + + private string _text = ""; + + public static SensorTreeItemViewModel CreateRoot(string text) + { + return new SensorTreeItemViewModel(SensorTreeItemKind.Root, null) + { + _text = text, + IconGlyph = "\uE7F4", + _isExpanded = true + }; + } + + public static SensorTreeItemViewModel FromHardware(IHardware hardware, AppSettings settings) + { + string expandedSettingName = new Identifier(hardware.Identifier, "expanded").ToString(); + SensorTreeItemViewModel item = new(SensorTreeItemKind.Hardware, settings) + { + Hardware = hardware, + IconGlyph = SensorTypeDisplay.GetHardwareGlyph(hardware.HardwareType), + _expandedSettingName = expandedSettingName, + _isExpanded = settings.GetValue(expandedSettingName, true) + }; + + foreach (SensorType sensorType in Enum.GetValues()) + { + ISensor[] sensors = hardware.Sensors + .Where(sensor => sensor.SensorType == sensorType) + .OrderBy(sensor => sensor.Index) + .ToArray(); + + if (sensors.Length == 0) + continue; + + item.Children.Add(FromSensorType(hardware.Identifier, sensorType, sensors, settings)); + } + + foreach (IHardware subHardware in hardware.SubHardware.OrderBy(subHardware => subHardware.HardwareType).ThenBy(subHardware => subHardware.Name, StringComparer.OrdinalIgnoreCase)) + item.Children.Add(FromHardware(subHardware, settings)); + + item.UpdateVisibilityState(); + return item; + } + + public void Configure(TemperatureUnit temperatureUnit, bool showHiddenSensors, bool showValueColumn, bool showMinColumn, bool showMaxColumn) + { + SetTemperatureUnit(temperatureUnit); + SetShowHiddenSensors(showHiddenSensors); + SetColumnVisibility(showValueColumn, showMinColumn, showMaxColumn); + } + + public IEnumerable Enumerate() + { + yield return this; + foreach (SensorTreeItemViewModel child in Children) + { + foreach (SensorTreeItemViewModel node in child.Enumerate()) + yield return node; + } + } + + public IEnumerable EnumerateSensors() + { + return Enumerate().Where(item => item.Sensor != null); + } + + public void RefreshValues() + { + OnPropertyChanged(nameof(Value)); + OnPropertyChanged(nameof(Min)); + OnPropertyChanged(nameof(Max)); + OnPropertyChanged(nameof(ToolTip)); + foreach (SensorTreeItemViewModel child in Children) + child.RefreshValues(); + } + + public void SetAllExpanded(bool isExpanded) + { + IsExpanded = isExpanded; + foreach (SensorTreeItemViewModel child in Children) + child.SetAllExpanded(isExpanded); + } + + public void SetColumnVisibility(bool showValueColumn, bool showMinColumn, bool showMaxColumn) + { + ValueColumnVisibility = showValueColumn ? Visibility.Visible : Visibility.Collapsed; + MinColumnVisibility = showMinColumn ? Visibility.Visible : Visibility.Collapsed; + MaxColumnVisibility = showMaxColumn ? Visibility.Visible : Visibility.Collapsed; + + foreach (SensorTreeItemViewModel child in Children) + child.SetColumnVisibility(showValueColumn, showMinColumn, showMaxColumn); + } + + public void SetShowHiddenSensors(bool showHiddenSensors) + { + _showHiddenSensors = showHiddenSensors; + UpdateVisibilityState(); + foreach (SensorTreeItemViewModel child in Children) + child.SetShowHiddenSensors(showHiddenSensors); + } + + public void SetTemperatureUnit(TemperatureUnit temperatureUnit) + { + TemperatureUnit = temperatureUnit; + foreach (SensorTreeItemViewModel child in Children) + child.SetTemperatureUnit(temperatureUnit); + } + + private static SensorTreeItemViewModel FromSensor(ISensor sensor, AppSettings settings) + { + string hiddenSettingName = new Identifier(sensor.Identifier, "hidden").ToString(); + string plotSettingName = new Identifier(sensor.Identifier, "plot").ToString(); + string penColorSettingName = new Identifier(sensor.Identifier, "penColor").ToString(); + return new SensorTreeItemViewModel(SensorTreeItemKind.Sensor, settings) + { + Sensor = sensor, + IconGlyph = SensorTypeDisplay.GetGlyph(sensor.SensorType), + _isVisible = !settings.GetValue(hiddenSettingName, sensor.IsDefaultHidden), + _plot = settings.GetValue(plotSettingName, false), + _penColor = settings.Contains(penColorSettingName) ? settings.GetValue(penColorSettingName, Color.FromArgb(255, 0, 0, 0)) : null + }; + } + + private static SensorTreeItemViewModel FromSensorType(Identifier parentIdentifier, SensorType sensorType, IEnumerable sensors, AppSettings settings) + { + string expandedSettingName = new Identifier(parentIdentifier, sensorType.ToString(), ".expanded").ToString(); + SensorTreeItemViewModel item = new(SensorTreeItemKind.SensorType, settings) + { + _text = SensorTypeDisplay.GetText(sensorType), + IconGlyph = SensorTypeDisplay.GetGlyph(sensorType), + _expandedSettingName = expandedSettingName, + _isExpanded = settings.GetValue(expandedSettingName, true) + }; + + foreach (ISensor sensor in sensors) + item.Children.Add(FromSensor(sensor, settings)); + + item.UpdateVisibilityState(); + return item; + } + + private void UpdateVisibilityState() + { + bool visible = Kind switch + { + SensorTreeItemKind.Sensor => _showHiddenSensors || IsVisible, + SensorTreeItemKind.SensorType => _showHiddenSensors || Children.Any(child => child.IsVisible), + _ => true + }; + + RowVisibility = visible ? Visibility.Visible : Visibility.Collapsed; + } +} diff --git a/LibreHardwareMonitor.Windows.WinUI/ViewModels/TemperatureUnit.cs b/LibreHardwareMonitor.Windows.WinUI/ViewModels/TemperatureUnit.cs new file mode 100644 index 0000000..fdfdf78 --- /dev/null +++ b/LibreHardwareMonitor.Windows.WinUI/ViewModels/TemperatureUnit.cs @@ -0,0 +1,11 @@ +// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +// Copyright (C) LibreHardwareMonitor and Contributors. + +namespace LibreHardwareMonitor.Windows.WinUI.ViewModels; + +public enum TemperatureUnit +{ + Celsius = 0, + Fahrenheit = 1 +} diff --git a/LibreHardwareMonitor.Windows.WinUI/ViewModels/ViewModelBase.cs b/LibreHardwareMonitor.Windows.WinUI/ViewModels/ViewModelBase.cs new file mode 100644 index 0000000..e94d890 --- /dev/null +++ b/LibreHardwareMonitor.Windows.WinUI/ViewModels/ViewModelBase.cs @@ -0,0 +1,29 @@ +// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +// Copyright (C) LibreHardwareMonitor and Contributors. + +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace LibreHardwareMonitor.Windows.WinUI.ViewModels; + +public abstract class ViewModelBase : INotifyPropertyChanged +{ + public event PropertyChangedEventHandler? PropertyChanged; + + protected bool SetProperty(ref T field, T value, [CallerMemberName] string? propertyName = null) + { + if (EqualityComparer.Default.Equals(field, value)) + return false; + + field = value; + OnPropertyChanged(propertyName); + return true; + } + + protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } +} diff --git a/LibreHardwareMonitor.Windows.WinUI/app.manifest b/LibreHardwareMonitor.Windows.WinUI/app.manifest new file mode 100644 index 0000000..ac355e6 --- /dev/null +++ b/LibreHardwareMonitor.Windows.WinUI/app.manifest @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + true/pm + PerMonitorV2 + + + diff --git a/LibreHardwareMonitor.sln b/LibreHardwareMonitor.sln index 94d24ad..86aaccc 100644 --- a/LibreHardwareMonitor.sln +++ b/LibreHardwareMonitor.sln @@ -3,10 +3,12 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 18 VisualStudioVersion = 18.0.11018.127 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibreHardwareMonitorLib", "LibreHardwareMonitorLib\LibreHardwareMonitorLib.csproj", "{B0397530-545A-471D-BB74-027AE456DF1A}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibreHardwareMonitor.Windows.Forms", "LibreHardwareMonitor.Windows.Forms\LibreHardwareMonitor.Windows.Forms.csproj", "{F5E0C1F7-9E9B-46F2-AC88-8C9C1C923880}" -EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibreHardwareMonitor.Windows.WinUI", "LibreHardwareMonitor.Windows.WinUI\LibreHardwareMonitor.Windows.WinUI.csproj", "{49D41E03-7028-4B28-9F5E-4558D0E26EA2}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibreHardwareMonitorLib", "LibreHardwareMonitorLib\LibreHardwareMonitorLib.csproj", "{B0397530-545A-471D-BB74-027AE456DF1A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibreHardwareMonitor.Windows.Forms", "LibreHardwareMonitor.Windows.Forms\LibreHardwareMonitor.Windows.Forms.csproj", "{F5E0C1F7-9E9B-46F2-AC88-8C9C1C923880}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aga.Controls", "Aga.Controls\Aga.Controls.csproj", "{E73BB233-D88B-44A7-A98F-D71EE158381D}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Controls", "Controls", "{23F54DBD-8998-4E22-A4E4-60F4F77F9B65}" @@ -51,6 +53,18 @@ Global {F5E0C1F7-9E9B-46F2-AC88-8C9C1C923880}.Release|x64.Build.0 = Release|x64 {F5E0C1F7-9E9B-46F2-AC88-8C9C1C923880}.Release|x86.ActiveCfg = Release|x86 {F5E0C1F7-9E9B-46F2-AC88-8C9C1C923880}.Release|x86.Build.0 = Release|x86 + {49D41E03-7028-4B28-9F5E-4558D0E26EA2}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {49D41E03-7028-4B28-9F5E-4558D0E26EA2}.Debug|ARM64.Build.0 = Debug|ARM64 + {49D41E03-7028-4B28-9F5E-4558D0E26EA2}.Debug|x64.ActiveCfg = Debug|x64 + {49D41E03-7028-4B28-9F5E-4558D0E26EA2}.Debug|x64.Build.0 = Debug|x64 + {49D41E03-7028-4B28-9F5E-4558D0E26EA2}.Debug|x86.ActiveCfg = Debug|x86 + {49D41E03-7028-4B28-9F5E-4558D0E26EA2}.Debug|x86.Build.0 = Debug|x86 + {49D41E03-7028-4B28-9F5E-4558D0E26EA2}.Release|ARM64.ActiveCfg = Release|ARM64 + {49D41E03-7028-4B28-9F5E-4558D0E26EA2}.Release|ARM64.Build.0 = Release|ARM64 + {49D41E03-7028-4B28-9F5E-4558D0E26EA2}.Release|x64.ActiveCfg = Release|x64 + {49D41E03-7028-4B28-9F5E-4558D0E26EA2}.Release|x64.Build.0 = Release|x64 + {49D41E03-7028-4B28-9F5E-4558D0E26EA2}.Release|x86.ActiveCfg = Release|x86 + {49D41E03-7028-4B28-9F5E-4558D0E26EA2}.Release|x86.Build.0 = Release|x86 {E73BB233-D88B-44A7-A98F-D71EE158381D}.Debug|ARM64.ActiveCfg = Debug|ARM64 {E73BB233-D88B-44A7-A98F-D71EE158381D}.Debug|ARM64.Build.0 = Debug|ARM64 {E73BB233-D88B-44A7-A98F-D71EE158381D}.Debug|x64.ActiveCfg = Debug|x64