1
0
Эх сурвалжийг харах

Merge branch 'feature/graphs' into develop

Antony Male 8 жил өмнө
parent
commit
43e240e5e0

+ 1 - 0
src/SyncTrayzor/Design/DummyFileTransfersTrayViewModel.cs

@@ -1,5 +1,6 @@
 using Stylet;
 using SyncTrayzor.Pages;
+using SyncTrayzor.Pages.Tray;
 using SyncTrayzor.Syncthing.ApiClient;
 using SyncTrayzor.Syncthing.TransferHistory;
 

+ 1 - 0
src/SyncTrayzor/NotifyIcon/NotifyIconViewModel.cs

@@ -1,6 +1,7 @@
 using Stylet;
 using SyncTrayzor.Pages;
 using SyncTrayzor.Pages.Settings;
+using SyncTrayzor.Pages.Tray;
 using SyncTrayzor.Services;
 using SyncTrayzor.Services.Config;
 using SyncTrayzor.Syncthing;

+ 11 - 1
src/SyncTrayzor/Pages/ThirdPartyComponentsViewModel.cs

@@ -1,6 +1,7 @@
 using Stylet;
 using SyncTrayzor.Services;
 using System.IO;
+using System.Linq;
 using System.Reflection;
 
 namespace SyncTrayzor.Pages
@@ -169,7 +170,16 @@ namespace SyncTrayzor.Pages
                     Notes = "Used internally for some background operations",
                     LicenseText = this.LoadLicense("Rx.txt")
                 },
-            });
+                new ThirdPartyComponent()
+                {
+                    Name = "OxyPlot",
+                    Description = "OxyPlot is a cross-platform plotting library for .NET",
+                    Homepage = "http://www.oxyplot.org",
+                    License = "MIT",
+                    Notes = "Use to draw the network usage graph in the tray popup",
+                    LicenseText = this.LoadLicense("OxyPlot.txt")
+                }
+            }.OrderBy(x => x.Name));
         }
 
         private string LoadLicense(string licenseName)

+ 102 - 0
src/SyncTrayzor/Pages/Tray/FileTransferViewModel.cs

@@ -0,0 +1,102 @@
+using Stylet;
+using SyncTrayzor.Properties;
+using SyncTrayzor.Syncthing.ApiClient;
+using SyncTrayzor.Syncthing.TransferHistory;
+using SyncTrayzor.Utils;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Interop;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using System.Windows.Threading;
+
+namespace SyncTrayzor.Pages.Tray
+{
+    public class FileTransferViewModel : PropertyChangedBase
+    {
+        public readonly FileTransfer FileTransfer;
+        private readonly DispatcherTimer completedTimeAgoUpdateTimer;
+
+        public string Path { get; }
+        public string FolderId { get; }
+        public string FullPath { get; }
+        public ImageSource Icon { get; }
+        public string Error { get; private set; }
+        public bool WasDeleted { get; }
+
+        public DateTime Completed => this.FileTransfer.FinishedUtc.GetValueOrDefault().ToLocalTime();
+
+        public string CompletedTimeAgo
+        {
+            get
+            {
+                if (this.FileTransfer.FinishedUtc.HasValue)
+                    return FormatUtils.TimeSpanToTimeAgo(DateTime.UtcNow - this.FileTransfer.FinishedUtc.Value);
+                else
+                    return null;
+            }
+        }
+
+        public string ProgressString { get; private set; }
+        public float ProgressPercent { get; private set; }
+
+        public FileTransferViewModel(FileTransfer fileTransfer)
+        {
+            this.completedTimeAgoUpdateTimer = new DispatcherTimer()
+            {
+                Interval = TimeSpan.FromMinutes(1),
+            };
+            this.completedTimeAgoUpdateTimer.Tick += (o, e) => this.NotifyOfPropertyChange(() => this.CompletedTimeAgo);
+            this.completedTimeAgoUpdateTimer.Start();
+
+            this.FileTransfer = fileTransfer;
+            this.Path = Pri.LongPath.Path.GetFileName(this.FileTransfer.Path);
+            this.FullPath = this.FileTransfer.Path;
+            this.FolderId = this.FileTransfer.FolderId;
+            using (var icon = ShellTools.GetIcon(this.FileTransfer.Path, this.FileTransfer.ItemType != ItemChangedItemType.Dir))
+            {
+                var bs = Imaging.CreateBitmapSourceFromHIcon(icon.Handle, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions());
+                bs.Freeze();
+                this.Icon = bs;
+            }
+            this.WasDeleted = this.FileTransfer.ActionType == ItemChangedActionType.Delete;
+
+            this.UpdateState();
+        }
+
+        public void UpdateState()
+        {
+            switch (this.FileTransfer.Status)
+            {
+                case FileTransferStatus.InProgress:
+                    if (this.FileTransfer.DownloadBytesPerSecond.HasValue)
+                    {
+                        this.ProgressString = String.Format(Resources.FileTransfersTrayView_Downloading_RateKnown,
+                            FormatUtils.BytesToHuman(this.FileTransfer.BytesTransferred),
+                            FormatUtils.BytesToHuman(this.FileTransfer.TotalBytes),
+                            FormatUtils.BytesToHuman(this.FileTransfer.DownloadBytesPerSecond.Value, 1));
+                    }
+                    else
+                    {
+                        this.ProgressString = String.Format(Resources.FileTransfersTrayView_Downloading_RateUnknown,
+                            FormatUtils.BytesToHuman(this.FileTransfer.BytesTransferred),
+                            FormatUtils.BytesToHuman(this.FileTransfer.TotalBytes));
+                    }
+
+                    this.ProgressPercent = ((float)this.FileTransfer.BytesTransferred / (float)this.FileTransfer.TotalBytes) * 100;
+                    break;
+
+                case FileTransferStatus.Completed:
+                    this.ProgressPercent = 100;
+                    this.ProgressString = null;
+                    break;
+            }
+
+            this.Error = this.FileTransfer.Error;
+        }
+    }
+}

+ 16 - 7
src/SyncTrayzor/Pages/FileTransfersTrayView.xaml → src/SyncTrayzor/Pages/Tray/FileTransfersTrayView.xaml

@@ -1,4 +1,4 @@
-<UserControl x:Class="SyncTrayzor.Pages.FileTransfersTrayView"
+<UserControl x:Class="SyncTrayzor.Pages.Tray.FileTransfersTrayView"
              xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
              xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
@@ -9,7 +9,8 @@
              xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
              mc:Ignorable="d" 
              x:Name="RootObject"
-             Height="300" d:DesignWidth="300"
+             Height="350"
+             d:DesignWidth="300"
              d:DataContext="{Binding Source={StaticResource ViewModelLocator}, Path=FileTransfersTrayViewModel}">
     <UserControl.Resources>
         <Style x:Key="SectionTitleStyle" TargetType="TextBlock">
@@ -32,13 +33,21 @@
             <Border DockPanel.Dock="Top" BorderBrush="Gray" BorderThickness="0,0,0,1">
                 <DockPanel Margin="5,8,5,0" LastChildFill="False">
                     <TextBlock DockPanel.Dock="Left" Height="25" FontSize="15" FontWeight="Bold">SyncTrayzor</TextBlock>
-                    <TextBlock DockPanel.Dock="Right" VerticalAlignment="Center"
-                               Text="{l:Loc FileTransfersTrayView_OutConnectionRate, ValueBinding={Binding OutConnectionRate}}"/>
-                    <TextBlock DockPanel.Dock="Right" VerticalAlignment="Center" Margin="0,0,10,0"
-                               Text="{l:Loc FileTransfersTrayView_InConnectionRate, ValueBinding={Binding InConnectionRate}}"/>
+                    <StackPanel DockPanel.Dock="Right" Orientation="Horizontal" Visibility="{Binding OutConnectionRate, Converter={x:Static s:BoolToVisibilityConverter.Instance}}">
+                        <Line X2="10" Stroke="Green" StrokeThickness="2" VerticalAlignment="Center" />
+                        <TextBlock VerticalAlignment="Center" Margin="5,0,0,0"
+                                   Text="{l:Loc FileTransfersTrayView_OutConnectionRate, ValueBinding={Binding OutConnectionRate}}"/>
+                    </StackPanel>
+                    <StackPanel DockPanel.Dock="Right" Orientation="Horizontal" Visibility="{Binding InConnectionRate, Converter={x:Static s:BoolToVisibilityConverter.Instance}}">
+                        <Line X2="10" Stroke="Red" StrokeThickness="2" VerticalAlignment="Center" />
+                        <TextBlock DockPanel.Dock="Right" VerticalAlignment="Center" Margin="5,0,10,0"
+                                   Text="{l:Loc FileTransfersTrayView_InConnectionRate, ValueBinding={Binding InConnectionRate}}"/>
+                    </StackPanel>
                 </DockPanel>
             </Border>
-            
+
+            <ContentControl DockPanel.Dock="Top" s:View.Model="{Binding NetworkGraph}"/>
+
             <Grid>
                 <TextBlock DockPanel.Dock="Top" HorizontalAlignment="Center" VerticalAlignment="Center"
                        Visibility="{Binding AnyTransfers, Converter={x:Static s:BoolToVisibilityConverter.InverseInstance}}"

+ 41 - 108
src/SyncTrayzor/Pages/FileTransfersTrayViewModel.cs → src/SyncTrayzor/Pages/Tray/FileTransfersTrayViewModel.cs

@@ -1,6 +1,5 @@
 using Pri.LongPath;
 using Stylet;
-using SyncTrayzor.Properties;
 using SyncTrayzor.Services;
 using SyncTrayzor.Syncthing;
 using SyncTrayzor.Syncthing.ApiClient;
@@ -8,111 +7,18 @@ using SyncTrayzor.Syncthing.TransferHistory;
 using SyncTrayzor.Utils;
 using System;
 using System.Linq;
-using System.Windows.Threading;
-using System.Windows.Media;
-using System.Windows.Interop;
-using System.Windows;
-using System.Windows.Media.Imaging;
 
-namespace SyncTrayzor.Pages
+namespace SyncTrayzor.Pages.Tray
 {
-    public class FileTransferViewModel : PropertyChangedBase
-    {
-        public readonly FileTransfer FileTransfer;
-        private readonly DispatcherTimer completedTimeAgoUpdateTimer;
-
-        public string Path { get; }
-        public string FolderId { get; }
-        public string FullPath { get; }
-        public ImageSource Icon { get; }
-        public string Error { get; private set; }
-        public bool WasDeleted { get;  }
-
-        public DateTime Completed => this.FileTransfer.FinishedUtc.GetValueOrDefault().ToLocalTime();
-
-        public string CompletedTimeAgo
-        {
-            get
-            {
-                if (this.FileTransfer.FinishedUtc.HasValue)
-                    return FormatUtils.TimeSpanToTimeAgo(DateTime.UtcNow - this.FileTransfer.FinishedUtc.Value);
-                else
-                    return null;
-            }
-        }
-
-        public string ProgressString { get; private set; }
-        public float ProgressPercent { get; private set; }
-
-        public FileTransferViewModel(FileTransfer fileTransfer)
-        {
-            this.completedTimeAgoUpdateTimer = new DispatcherTimer()
-            {
-                Interval = TimeSpan.FromMinutes(1),
-            };
-            this.completedTimeAgoUpdateTimer.Tick += (o, e) => this.NotifyOfPropertyChange(() => this.CompletedTimeAgo);
-            this.completedTimeAgoUpdateTimer.Start();
-
-            this.FileTransfer = fileTransfer;
-            this.Path = Pri.LongPath.Path.GetFileName(this.FileTransfer.Path);
-            this.FullPath = this.FileTransfer.Path;
-            this.FolderId = this.FileTransfer.FolderId;
-            using (var icon = ShellTools.GetIcon(this.FileTransfer.Path, this.FileTransfer.ItemType != ItemChangedItemType.Dir))
-            {
-                var bs = Imaging.CreateBitmapSourceFromHIcon(icon.Handle, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions());
-                bs.Freeze();
-                this.Icon = bs;
-            }
-            this.WasDeleted = this.FileTransfer.ActionType == ItemChangedActionType.Delete;
-
-            this.UpdateState();
-        }
-
-        public void UpdateState()
-        {
-            switch (this.FileTransfer.Status)
-            {
-                case FileTransferStatus.InProgress:
-                    if (this.FileTransfer.DownloadBytesPerSecond.HasValue)
-                    {
-                        this.ProgressString = String.Format(Resources.FileTransfersTrayView_Downloading_RateKnown,
-                            FormatUtils.BytesToHuman(this.FileTransfer.BytesTransferred),
-                            FormatUtils.BytesToHuman(this.FileTransfer.TotalBytes),
-                            FormatUtils.BytesToHuman(this.FileTransfer.DownloadBytesPerSecond.Value, 1));
-                    }
-                    else
-                    {
-                        this.ProgressString = String.Format(Resources.FileTransfersTrayView_Downloading_RateUnknown,
-                            FormatUtils.BytesToHuman(this.FileTransfer.BytesTransferred),
-                            FormatUtils.BytesToHuman(this.FileTransfer.TotalBytes));
-                    }
-
-                    this.ProgressPercent = ((float)this.FileTransfer.BytesTransferred / this.FileTransfer.TotalBytes) * 100;
-                    break;
-
-                case FileTransferStatus.Completed:
-                    this.ProgressPercent = 100;
-                    this.ProgressString = null;
-                    break;
-
-                case FileTransferStatus.Started:
-                    break;
-
-                default:
-                    break;
-            }
-
-            this.Error = this.FileTransfer.Error;
-        }
-    }
-
-    public class FileTransfersTrayViewModel : Screen
+    public class FileTransfersTrayViewModel : Screen, IDisposable
     {
         private const int initialCompletedTransfersToDisplay = 100;
 
         private readonly ISyncthingManager syncthingManager;
         private readonly IProcessStartProvider processStartProvider;
 
+        public NetworkGraphViewModel NetworkGraph { get; }
+
         public BindableCollection<FileTransferViewModel> CompletedTransfers { get; private set; }
         public BindableCollection<FileTransferViewModel> InProgressTransfers { get; private set; }
 
@@ -124,11 +30,16 @@ namespace SyncTrayzor.Pages
 
         public bool AnyTransfers => this.HasCompletedTransfers || this.HasInProgressTransfers;
 
-        public FileTransfersTrayViewModel(ISyncthingManager syncthingManager, IProcessStartProvider processStartProvider)
+        public FileTransfersTrayViewModel(ISyncthingManager syncthingManager, IProcessStartProvider processStartProvider, NetworkGraphViewModel networkGraph)
         {
             this.syncthingManager = syncthingManager;
             this.processStartProvider = processStartProvider;
 
+            this.syncthingManager.StateChanged += this.SyncthingStateChanged;
+
+            this.NetworkGraph = networkGraph;
+            this.NetworkGraph.ConductWith(this);
+
             this.CompletedTransfers = new BindableCollection<FileTransferViewModel>();
             this.InProgressTransfers = new BindableCollection<FileTransferViewModel>();
 
@@ -194,18 +105,33 @@ namespace SyncTrayzor.Pages
             this.UpdateConnectionStats(e.TotalConnectionStats);
         }
 
+        private void SyncthingStateChanged(object sender, SyncthingStateChangedEventArgs e)
+        {
+            if (this.syncthingManager.State == SyncthingState.Running)
+                this.UpdateConnectionStats(0, 0);
+            else
+                this.UpdateConnectionStats(null, null);
+        }
+
         private void UpdateConnectionStats(SyncthingConnectionStats connectionStats)
         {
-            if (connectionStats == null)
-            {
-                this.InConnectionRate = "0.0B";
-                this.OutConnectionRate = "0.0B";
-            }
+            if (this.syncthingManager.State == SyncthingState.Running)
+                this.UpdateConnectionStats(connectionStats.InBytesPerSecond, connectionStats.OutBytesPerSecond);
             else
-            {
-                this.InConnectionRate = FormatUtils.BytesToHuman(connectionStats.InBytesPerSecond, 1);
-                this.OutConnectionRate = FormatUtils.BytesToHuman(connectionStats.OutBytesPerSecond, 1);
-            }
+                this.UpdateConnectionStats(null, null);
+        }
+
+        private void UpdateConnectionStats(double? inBytesPerSecond, double? outBytesPerSecond)
+        {
+            if (inBytesPerSecond == null)
+                this.InConnectionRate = null;
+            else
+                this.InConnectionRate = FormatUtils.BytesToHuman(inBytesPerSecond.Value, 1);
+
+            if (outBytesPerSecond == null)
+                this.OutConnectionRate = null;
+            else
+                this.OutConnectionRate = FormatUtils.BytesToHuman(outBytesPerSecond.Value, 1);
         }
 
         public void ItemClicked(FileTransferViewModel fileTransferVm)
@@ -223,5 +149,12 @@ namespace SyncTrayzor.Pages
                     this.processStartProvider.ShowFolderInExplorer(Path.Combine(folder.Path, fileTransfer.Path));
             }
         }
+
+        public void Dispose()
+        {
+            this.syncthingManager.StateChanged -= this.SyncthingStateChanged;
+
+            this.NetworkGraph.Dispose();
+        }
     }
 }

+ 19 - 0
src/SyncTrayzor/Pages/Tray/NetworkGraphView.xaml

@@ -0,0 +1,19 @@
+<UserControl x:Class="SyncTrayzor.Pages.Tray.NetworkGraphView"
+             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
+             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
+             xmlns:s="https://github.com/canton7/Stylet"
+             xmlns:local="clr-namespace:SyncTrayzor.Pages.Tray"
+             xmlns:oxy="http://oxyplot.org/wpf"
+             mc:Ignorable="d" 
+             d:DataContext="{d:DesignInstance local:NetworkGraphViewModel}"
+             d:DesignHeight="300" d:DesignWidth="300"
+             Height="50"
+             Visibility="{Binding ShowGraph, Converter={x:Static s:BoolToVisibilityConverter.Instance}}">
+    <DockPanel>
+        <TextBlock DockPanel.Dock="Right" VerticalAlignment="Top" Margin="0,3,5,0" Foreground="DarkGray" FontSize="10" Text="{Binding MaxYValue}"/>
+
+        <oxy:PlotView Grid.Row="0" Model="{Binding OxyPlotModel}" DefaultTrackerTemplate="{x:Null}"/>
+    </DockPanel>
+</UserControl>

+ 180 - 0
src/SyncTrayzor/Pages/Tray/NetworkGraphViewModel.cs

@@ -0,0 +1,180 @@
+using OxyPlot;
+using OxyPlot.Axes;
+using OxyPlot.Series;
+using Stylet;
+using SyncTrayzor.Syncthing;
+using SyncTrayzor.Utils;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace SyncTrayzor.Pages.Tray
+{
+    public class NetworkGraphViewModel : Screen, IDisposable
+    {
+        private static readonly DateTime epoch = DateTime.UtcNow; // Some arbitrary value in the past
+        private static readonly TimeSpan window = TimeSpan.FromMinutes(15);
+
+        private const double minYValue = 1024 * 100; // 100 KBit/s
+
+        private readonly ISyncthingManager syncthingManager;
+
+        private readonly LinearAxis yAxis;
+        private readonly LinearAxis xAxis;
+
+        private readonly LineSeries inboundSeries;
+        private readonly LineSeries outboundSeries;
+
+        public PlotModel OxyPlotModel { get; } = new PlotModel();
+        public bool ShowGraph { get; private set; }
+
+        public string MaxYValue { get; private set; }
+
+        public NetworkGraphViewModel(ISyncthingManager syncthingManager)
+        {
+            this.syncthingManager = syncthingManager;
+
+            this.OxyPlotModel.PlotAreaBorderColor = OxyColors.LightGray;
+
+            this.xAxis = new LinearAxis()
+            {
+                Position = AxisPosition.Bottom,
+                IsZoomEnabled = false,
+                IsPanEnabled = false,
+                IsAxisVisible = false,
+                MajorGridlineColor = OxyColors.Gray,
+                MajorGridlineStyle = LineStyle.Dash,
+            };
+            this.OxyPlotModel.Axes.Add(this.xAxis);
+
+            this.yAxis = new LinearAxis()
+            {
+                Position = AxisPosition.Right,
+                IsZoomEnabled = false,
+                IsPanEnabled = false,
+                IsAxisVisible = false,
+                AbsoluteMinimum = -1, // Leave a little bit of room for the line to draw
+            };
+            this.OxyPlotModel.Axes.Add(this.yAxis);
+
+            this.inboundSeries = new LineSeries()
+            {
+                Color = OxyColors.Red,
+            };
+            this.OxyPlotModel.Series.Add(this.inboundSeries);
+
+            this.outboundSeries = new LineSeries()
+            {
+                Color = OxyColors.Green,
+            };
+            this.OxyPlotModel.Series.Add(this.outboundSeries);
+
+            this.ResetToEmptyGraph();
+
+            this.Update(this.syncthingManager.TotalConnectionStats);
+            this.syncthingManager.TotalConnectionStatsChanged += this.TotalConnectionStatsChanged;
+            this.syncthingManager.StateChanged += this.SyncthingStateChanged;
+        }
+
+        protected override void OnActivate()
+        {
+            base.OnActivate();
+            this.OxyPlotModel.InvalidatePlot(true);
+        }
+
+        private void SyncthingStateChanged(object sender, SyncthingStateChangedEventArgs e)
+        {
+            if (e.OldState == SyncthingState.Running)
+            {
+                this.ResetToEmptyGraph();
+            }
+
+            this.ShowGraph = e.NewState == SyncthingState.Running;
+        }
+
+        private void ResetToEmptyGraph()
+        {
+            var now = DateTime.UtcNow;
+            var earliest = (now - window - epoch).TotalSeconds;
+            var latest = (now - epoch).TotalSeconds;
+
+            // Put points on the far left, so we get a line from them
+            this.inboundSeries.Points.Clear();
+            this.inboundSeries.Points.Add(new DataPoint(earliest, 0));
+            this.inboundSeries.Points.Add(new DataPoint(latest, 0));
+
+            this.outboundSeries.Points.Clear();
+            this.outboundSeries.Points.Add(new DataPoint(earliest, 0));
+            this.outboundSeries.Points.Add(new DataPoint(latest, 0));
+
+            this.xAxis.Minimum = earliest;
+            this.xAxis.Maximum = latest;
+
+            this.yAxis.Maximum = minYValue;
+            this.MaxYValue = FormatUtils.BytesToHuman(minYValue) + "/s";
+
+            if (this.IsActive)
+                this.OxyPlotModel.InvalidatePlot(true);
+        }
+
+        private void TotalConnectionStatsChanged(object sender, ConnectionStatsChangedEventArgs e)
+        {
+            this.Update(e.TotalConnectionStats);
+        }
+
+        private void Update(SyncthingConnectionStats stats)
+        {
+            var now = DateTime.UtcNow;
+            double earliest = (now - window - epoch).TotalSeconds;
+
+            this.Update(earliest, this.inboundSeries, stats.InBytesPerSecond);
+            this.Update(earliest, this.outboundSeries, stats.OutBytesPerSecond);
+
+            this.xAxis.Minimum = earliest;
+            this.xAxis.Maximum = (now - epoch).TotalSeconds;
+
+            // This increases the value to the nearest 1024 boundary
+            double maxValue = this.inboundSeries.Points.Concat(this.outboundSeries.Points).Max(x => x.Y);
+            double roundedMax;
+            if (maxValue > minYValue)
+            {
+                double factor = Math.Pow(1024, (int)Math.Log(maxValue, 1024));
+                roundedMax = Math.Ceiling(maxValue / factor) * factor;
+            }
+            else
+            {
+                roundedMax = minYValue;
+            }
+
+            // Give the graph a little bit of headroom, otherwise the line gets chopped
+            this.yAxis.Maximum = roundedMax * 1.05;
+            this.MaxYValue = FormatUtils.BytesToHuman(roundedMax) + "/s";
+
+            if (this.IsActive)
+                this.OxyPlotModel.InvalidatePlot(true);
+        }
+
+        private void Update(double earliest, LineSeries series, double bytesPerSecond)
+        {
+            // Keep one data point below 'earliest'
+
+            int i = 0;
+            for (; i < series.Points.Count && series.Points[i].X < earliest; i++) { }
+            i--;
+            if (i > 0)
+            {
+                series.Points.RemoveRange(0, i);
+            }
+
+            series.Points.Add(new DataPoint((DateTime.UtcNow - epoch).TotalSeconds, bytesPerSecond));
+        }
+
+        public void Dispose()
+        {
+            this.syncthingManager.TotalConnectionStatsChanged -= this.TotalConnectionStatsChanged;
+            this.syncthingManager.StateChanged -= this.SyncthingStateChanged;
+        }
+    }
+}

+ 22 - 0
src/SyncTrayzor/Resources/Licenses/OxyPlot.txt

@@ -0,0 +1,22 @@
+The MIT License (MIT)
+
+Copyright (c) 2014 OxyPlot contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+ 19 - 2
src/SyncTrayzor/SyncTrayzor.csproj

@@ -118,6 +118,14 @@
       <HintPath>..\packages\NLog.4.4.2\lib\net45\NLog.dll</HintPath>
       <Private>True</Private>
     </Reference>
+    <Reference Include="OxyPlot, Version=1.0.0.0, Culture=neutral, PublicKeyToken=638079a8f0bd61e9, processorArchitecture=MSIL">
+      <HintPath>..\packages\OxyPlot.Core.1.0.0\lib\net45\OxyPlot.dll</HintPath>
+      <Private>True</Private>
+    </Reference>
+    <Reference Include="OxyPlot.Wpf, Version=1.0.0.0, Culture=neutral, PublicKeyToken=75e952ba404cdbb0, processorArchitecture=MSIL">
+      <HintPath>..\packages\OxyPlot.Wpf.1.0.0\lib\net45\OxyPlot.Wpf.dll</HintPath>
+      <Private>True</Private>
+    </Reference>
     <Reference Include="PresentationFramework" />
     <Reference Include="PresentationFramework.Aero" />
     <Reference Include="Pri.LongPath, Version=2.0.42.0, Culture=neutral, processorArchitecture=MSIL">
@@ -209,7 +217,7 @@
     <Compile Include="Pages\ConflictResolution\ConflictViewModel.cs" />
     <Compile Include="Pages\ConflictResolution\MultipleConflictsResolutionViewModel.cs" />
     <Compile Include="Pages\ConflictResolution\SingleConflictResolutionViewModel.cs" />
-    <Compile Include="Pages\FileTransfersTrayViewModel.cs" />
+    <Compile Include="Pages\Tray\FileTransfersTrayViewModel.cs" />
     <Compile Include="Pages\NewVersionInstalledToastViewModel.cs" />
     <Compile Include="Pages\Settings\KeyValueStringParser.cs" />
     <Compile Include="Pages\Settings\SettingItem.cs" />
@@ -217,6 +225,8 @@
     <Compile Include="Pages\Settings\SyncthingApiKeyValidator.cs" />
     <Compile Include="Pages\Settings\SyncthingCommandLineFlagsValidator.cs" />
     <Compile Include="Pages\Settings\SyncthingEnvironmentalVariablesValidator.cs" />
+    <Compile Include="Pages\Tray\FileTransferViewModel.cs" />
+    <Compile Include="Pages\Tray\NetworkGraphViewModel.cs" />
     <Compile Include="Properties\Resources.Designer.cs">
       <AutoGen>True</AutoGen>
       <DesignTime>True</DesignTime>
@@ -467,7 +477,7 @@
       <Generator>MSBuild:Compile</Generator>
       <SubType>Designer</SubType>
     </Page>
-    <Page Include="Pages\FileTransfersTrayView.xaml">
+    <Page Include="Pages\Tray\FileTransfersTrayView.xaml">
       <SubType>Designer</SubType>
       <Generator>MSBuild:Compile</Generator>
     </Page>
@@ -503,6 +513,10 @@
       <SubType>Designer</SubType>
       <Generator>MSBuild:Compile</Generator>
     </Page>
+    <Page Include="Pages\Tray\NetworkGraphView.xaml">
+      <SubType>Designer</SubType>
+      <Generator>MSBuild:Compile</Generator>
+    </Page>
     <Page Include="Pages\UnhandledExceptionView.xaml">
       <SubType>Designer</SubType>
       <Generator>MSBuild:Compile</Generator>
@@ -614,6 +628,9 @@
   <ItemGroup>
     <EmbeddedResource Include="Resources\Licenses\Rx.txt" />
   </ItemGroup>
+  <ItemGroup>
+    <EmbeddedResource Include="Resources\Licenses\OxyPlot.txt" />
+  </ItemGroup>
   <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
   <Target Name="Configs">
     <XslTransformation XmlInputPaths="App.config" XslInputPath="App.Installer.config.xslt" OutputPaths="$(OutputPath)$(RootNamespace).exe.Installer.config" />

+ 15 - 18
src/SyncTrayzor/Syncthing/SyncthingConnectionsWatcher.cs

@@ -27,7 +27,6 @@ namespace SyncTrayzor.Syncthing
         
         private DateTime lastPollCompletion;
         private Connections prevConnections;
-        private bool haveNotifiedOfNoChange;
 
         public event EventHandler<ConnectionStatsChangedEventArgs> TotalConnectionStatsChanged;
 
@@ -40,11 +39,16 @@ namespace SyncTrayzor.Syncthing
         protected override void OnStart()
         {
             this.apiClient = this.apiClientWrapper.Value;
+            this.prevConnections = null;
         }
 
         protected override void OnStop()
         {
             this.apiClient = null;
+            var prev = this.prevConnections.Total;
+
+            // Send an update with zero transfer rate, since that's what we're now doing
+            this.Update(this.prevConnections);
         }
 
         protected override async Task PollAsync(CancellationToken cancellationToken)
@@ -53,7 +57,12 @@ namespace SyncTrayzor.Syncthing
 
             // We can be stopped in the time it takes this to complete
             cancellationToken.ThrowIfCancellationRequested();
-            
+
+            this.Update(connections);
+        }
+
+        private void Update(Connections connections)
+        {
             var elapsed = DateTime.UtcNow - this.lastPollCompletion;
             this.lastPollCompletion = DateTime.UtcNow;
 
@@ -63,23 +72,11 @@ namespace SyncTrayzor.Syncthing
                 var total = connections.Total;
                 var prevTotal = this.prevConnections.Total;
 
-                if (total.InBytesTotal != prevTotal.InBytesTotal || total.OutBytesTotal != prevTotal.OutBytesTotal)
-                {
-                    this.haveNotifiedOfNoChange = false;
-
-                    double inBytesPerSecond = (total.InBytesTotal - prevTotal.InBytesTotal) / elapsed.TotalSeconds;
-                    double outBytesPerSecond = (total.OutBytesTotal - prevTotal.OutBytesTotal) / elapsed.TotalSeconds;
-
-                    var totalStats = new SyncthingConnectionStats(total.InBytesTotal, total.OutBytesTotal, inBytesPerSecond, outBytesPerSecond);
-                    this.OnTotalConnectionStatsChanged(totalStats);
-                }
-                else if (!this.haveNotifiedOfNoChange)
-                {
-                    this.haveNotifiedOfNoChange = true;
+                double inBytesPerSecond = (total.InBytesTotal - prevTotal.InBytesTotal) / elapsed.TotalSeconds;
+                double outBytesPerSecond = (total.OutBytesTotal - prevTotal.OutBytesTotal) / elapsed.TotalSeconds;
 
-                    var totalStats = new SyncthingConnectionStats(total.InBytesTotal, total.OutBytesTotal, 0, 0);
-                    this.OnTotalConnectionStatsChanged(totalStats);
-                }
+                var totalStats = new SyncthingConnectionStats(total.InBytesTotal, total.OutBytesTotal, inBytesPerSecond, outBytesPerSecond);
+                this.OnTotalConnectionStatsChanged(totalStats);
             }
             this.prevConnections = connections;
         }

+ 1 - 1
src/SyncTrayzor/Syncthing/SyncthingManager.cs

@@ -110,7 +110,7 @@ namespace SyncTrayzor.Syncthing
         public event EventHandler<FolderRejectedEventArgs> FolderRejected;
 
         private readonly object totalConnectionStatsLock = new object();
-        private SyncthingConnectionStats _totalConnectionStats;
+        private SyncthingConnectionStats _totalConnectionStats = new SyncthingConnectionStats(0, 0, 0, 0);
         public SyncthingConnectionStats TotalConnectionStats
         {
             get { lock (this.totalConnectionStatsLock) { return this._totalConnectionStats; } }

+ 1 - 1
src/SyncTrayzor/Xaml/Resources.xaml

@@ -1,6 +1,6 @@
 <ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
-    
+
     <Style x:Key="DialogButton" TargetType="Button">
         <Setter Property="Height" Value="25"/>
         <Setter Property="Margin" Value="10,0,0,0"/>

+ 2 - 0
src/SyncTrayzor/packages.config

@@ -10,6 +10,8 @@
   <package id="Hardcodet.NotifyIcon.Wpf" version="1.0.8" targetFramework="net451" />
   <package id="Newtonsoft.Json" version="9.0.1" targetFramework="net451" />
   <package id="NLog" version="4.4.2" targetFramework="net451" />
+  <package id="OxyPlot.Core" version="1.0.0" targetFramework="net451" />
+  <package id="OxyPlot.Wpf" version="1.0.0" targetFramework="net451" />
   <package id="Pri.LongPath" version="2.0.42" targetFramework="net451" />
   <package id="PropertyChanged.Fody" version="1.52.1" targetFramework="net451" developmentDependency="true" />
   <package id="RestEase" version="1.3.2" targetFramework="net451" />