/* * Copyright (c) 2025 Proton AG * * This file is part of ProtonVPN. * * ProtonVPN is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * ProtonVPN is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with ProtonVPN. If not, see . */ using CommunityToolkit.Mvvm.ComponentModel; using ProtonVPN.Client.Common.Enums; using ProtonVPN.Client.Common.Helpers; using ProtonVPN.Client.Contracts.Messages; using ProtonVPN.Client.Core.Bases; using ProtonVPN.Client.Core.Bases.ViewModels; using ProtonVPN.Client.Core.Services.Activation; using ProtonVPN.Client.EventMessaging.Contracts; using ProtonVPN.Client.Localization.Extensions; using ProtonVPN.Client.Logic.Connection.Contracts.History; using ProtonVPN.Client.Logic.Connection.Contracts.Messages; using ProtonVPN.Common.Core.Networking; namespace ProtonVPN.Client.UI.Main.Home.Details.Connection; public partial class SpeedChartComponentViewModel : ActivatableViewModelBase, IEventMessageReceiver, IEventMessageReceiver { private const double Y_AXIS_BUFFER = 1.1; private readonly INetworkTrafficManager _networkTrafficManager; private readonly IMainWindowActivator _mainWindowActivator; [ObservableProperty] [NotifyPropertyChangedFor(nameof(FormattedDownloadSpeed))] private long _downloadSpeed; [ObservableProperty] [NotifyPropertyChangedFor(nameof(FormattedUploadSpeed))] private long _uploadSpeed; [ObservableProperty] [NotifyPropertyChangedFor(nameof(FormattedDownloadVolume))] [NotifyPropertyChangedFor(nameof(FormattedTotalVolume))] private long _downloadVolume; [ObservableProperty] [NotifyPropertyChangedFor(nameof(FormattedUploadVolume))] [NotifyPropertyChangedFor(nameof(FormattedTotalVolume))] private long _uploadVolume; [ObservableProperty] [NotifyPropertyChangedFor(nameof(SpeedUnit))] private ByteMetrics _metric; public string FormattedDownloadSpeed => Localizer.GetFormattedSpeed(DownloadSpeed); public string FormattedUploadSpeed => Localizer.GetFormattedSpeed(UploadSpeed); public string FormattedDownloadVolume => Localizer.GetFormattedSize(DownloadVolume); public string FormattedUploadVolume => Localizer.GetFormattedSize(UploadVolume); public string FormattedTotalVolume => Localizer.GetFormattedSize(DownloadVolume + UploadVolume); public string SpeedUnit => Localizer.GetFormat("Format_SpeedUnit", Localizer.GetSpeedUnit(Metric)); [ObservableProperty] private IReadOnlyList _downloadSpeedHistory = []; [ObservableProperty] private IReadOnlyList _uploadSpeedHistory = []; [ObservableProperty] private double _lowerBound; [ObservableProperty] private double _midpoint; [ObservableProperty] private double _upperBound; public SpeedChartComponentViewModel( INetworkTrafficManager networkTrafficManager, IMainWindowActivator mainWindowActivator, IViewModelHelper viewModelHelper) : base(viewModelHelper) { _networkTrafficManager = networkTrafficManager; _mainWindowActivator = mainWindowActivator; } public void Receive(NetworkTrafficChangedMessage message) { ExecuteOnUIThread(InvalidateAll); } public void Receive(MainWindowVisibilityChangedMessage message) { ExecuteOnUIThread(InvalidateAll); } protected override void OnActivated() { base.OnActivated(); InvalidateSpeedGraph(); } protected override void OnLanguageChanged() { base.OnLanguageChanged(); OnPropertyChanged(nameof(FormattedDownloadSpeed)); OnPropertyChanged(nameof(FormattedUploadSpeed)); OnPropertyChanged(nameof(FormattedDownloadVolume)); OnPropertyChanged(nameof(FormattedUploadVolume)); OnPropertyChanged(nameof(FormattedTotalVolume)); OnPropertyChanged(nameof(SpeedUnit)); } private void InvalidateAll() { if (!IsActive || !_mainWindowActivator.IsWindowVisible) { return; } NetworkTraffic speed = _networkTrafficManager.GetSpeed(); NetworkTraffic volume = _networkTrafficManager.GetVolume(); DownloadSpeed = (long)speed.BytesDownloaded; UploadSpeed = (long)speed.BytesUploaded; DownloadVolume = (long)volume.BytesDownloaded; UploadVolume = (long)volume.BytesUploaded; InvalidateSpeedGraph(); } private void InvalidateSpeedGraph() { IReadOnlyList speedHistory = _networkTrafficManager.GetSpeedHistory(); IEnumerable downloadHistory = speedHistory.Select(nt => (long)nt.BytesDownloaded); IEnumerable uploadHistory = speedHistory.Select(nt => (long)nt.BytesUploaded); long maxSpeed = Math.Max(downloadHistory.Max(), uploadHistory.Max()); (double scaledMaxSpeed, ByteMetrics metric) = ByteConversionHelper.CalculateSize(maxSpeed); Metric = metric; double scaleFactor = ByteConversionHelper.GetScaleFactor(Metric); double roundedScaledMaxSpeed = CalculateYAxisLimit(scaledMaxSpeed); LowerBound = 0.0; Midpoint = roundedScaledMaxSpeed / 2.0; UpperBound = roundedScaledMaxSpeed; DownloadSpeedHistory = downloadHistory.Select(n => GetScaledDataPoint(n, scaleFactor)).ToList(); UploadSpeedHistory = uploadHistory.Select(n => GetScaledDataPoint(n, scaleFactor)).ToList(); } private double GetScaledDataPoint(long number, double scaleFactor) { return Math.Round(number / scaleFactor, 1); } private double CalculateYAxisLimit(double maxValue) { if (maxValue <= 0) { return 10; } maxValue *= Y_AXIS_BUFFER; double roundedLimit; if (maxValue < 1) { // For values less than 1, round up to the nearest 0.2 roundedLimit = Math.Ceiling(maxValue * 5) / 5; } else if (maxValue <= 5) { // For values between 1 and 5, round up to the nearest 1 roundedLimit = Math.Ceiling(maxValue); } else if (maxValue <= 20) { // For values between 5 and 20, round up to the nearest 2 roundedLimit = Math.Ceiling(maxValue / 2) * 2; } else { // For values greater than 10, use a scale-based rounding factor double scale = Math.Pow(10, Math.Floor(Math.Log10(maxValue))); roundedLimit = Math.Ceiling(maxValue / scale) * scale; } return roundedLimit; } }