Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ NETworManager has integrated some **optional** third-party services to enhance f
- [api.github.com](https://docs.github.com/en/site-policy/privacy-policies/github-general-privacy-statement) - Check for application updates.
- [ipify.org](https://www.ipify.org/) - Retrieve the public IP address used by the client.
- [ip-api.com](https://ip-api.com/docs/legal) - Retrieve network information (e.g., geolocation, ISP, DNS resolver) used by the client.
- [speed.cloudflare.com](https://www.cloudflare.com/privacypolicy/) - Measure download/upload speed, latency and jitter.
## 📝 License
Expand Down
4 changes: 2 additions & 2 deletions Source/GlobalAssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]

[assembly: AssemblyVersion("2026.5.17.0")]
[assembly: AssemblyFileVersion("2026.5.17.0")]
[assembly: AssemblyVersion("2026.5.24.0")]
[assembly: AssemblyFileVersion("2026.5.24.0")]
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using System;
using System.Globalization;
using System.Windows.Data;

namespace NETworkManager.Converters;

/// <summary>
/// Converts a nullable <see cref="double"/> to a formatted string, returning "-/-" for null.
/// Pass a ConverterParameter of the form "F0|ms" or "F1|Mbps" to control the numeric format
/// specifier and the unit suffix, separated by '|'.
/// </summary>
public sealed class NullableDoubleToStringConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is not double d)
return "-/-";

if (parameter is string fmt)
{
var parts = fmt.Split('|');
var format = parts.Length > 0 ? parts[0] : "G";
var unit = parts.Length > 1 ? " " + parts[1] : string.Empty;
return d.ToString(format, culture) + unit;
}

return d.ToString(culture);
}

public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) =>
throw new NotImplementedException();
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ public static class ExternalServicesManager
new ExternalServicesInfo("ip-api.com", "https://ip-api.com/",
Strings.ExternalService_ip_api_Description),
new ExternalServicesInfo("ipify.org", "https://www.ipify.org/",
Strings.ExternalService_ipify_Description)
Strings.ExternalService_ipify_Description),
new ExternalServicesInfo("speed.cloudflare.com", "https://speed.cloudflare.com/",
Strings.ExternalService_speed_cloudflare_Description)
};
}
101 changes: 100 additions & 1 deletion Source/NETworkManager.Localization/Resources/Strings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 34 additions & 0 deletions Source/NETworkManager.Localization/Resources/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -4306,4 +4306,38 @@ You can copy your profile files from “{0}” to “{1}” to migrate your exis
<data name="ImportProfilesDots" xml:space="preserve">
<value>Import profiles...</value>
</data>
<data name="SpeedTest" xml:space="preserve">
<value>Speed Test</value>
</data>
<data name="RunSpeedTest" xml:space="preserve">
<value>Run speed test</value>
</data>
<data name="FetchingMetadataDots" xml:space="preserve">
<value>Fetching metadata...</value>
</data>
<data name="MeasuringLatencyDots" xml:space="preserve">
<value>Measuring latency...</value>
</data>
<data name="MeasuringDownloadSpeedDots" xml:space="preserve">
<value>Measuring download speed...</value>
</data>
<data name="MeasuringUploadSpeedDots" xml:space="preserve">
<value>Measuring upload speed...</value>
</data>
<data name="SpeedTestDisclaimerMessage" xml:space="preserve">
<value>Measure download and upload speeds, latency, and jitter with speed.cloudflare.com.
Cloudflare may log your IP address and network information. See Cloudflare's privacy policy for details.</value>
</data>
<data name="Latency" xml:space="preserve">
<value>Latency</value>
</data>
<data name="Jitter" xml:space="preserve">
<value>Jitter</value>
</data>
<data name="ExternalService_speed_cloudflare_Description" xml:space="preserve">
<value>Speed test service used to measure download speed, upload speed, latency, and jitter.</value>
</data>
<data name="Stop" xml:space="preserve">
<value>Stop</value>
</data>
</root>
73 changes: 73 additions & 0 deletions Source/NETworkManager.Models/Cloudflare/SpeedTestMetaInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
using Newtonsoft.Json;

namespace NETworkManager.Models.Cloudflare;

/// <summary>
/// Cloudflare PoP (Point of Presence) information returned by the
/// <c>speed.cloudflare.com/meta</c> endpoint.
/// </summary>
public class SpeedTestMetaColo
{
/// <summary>
/// IATA airport code of the PoP (e.g. "FRA").
/// </summary>
[JsonProperty("iata")]
public string Iata { get; set; }

/// <summary>
/// City of the PoP (e.g. "Frankfurt-am-Main").
/// </summary>
[JsonProperty("city")]
public string City { get; set; }

/// <summary>
/// ISO 3166-1 alpha-2 country code of the PoP (e.g. "DE").
/// </summary>
[JsonProperty("cca2")]
public string Cca2 { get; set; }
}

/// <summary>
/// Deserialized response of the <c>speed.cloudflare.com/meta</c> endpoint.
/// Provides client and Cloudflare PoP metadata used to enrich the speed
/// test result. Requires the <c>Origin: https://speed.cloudflare.com</c>
/// header on the request, otherwise an empty object is returned.
/// </summary>
public class SpeedTestMetaInfo
{
/// <summary>
/// Public IP address of the requesting client as seen by Cloudflare.
/// </summary>
[JsonProperty("clientIp")]
public string ClientIp { get; set; }

/// <summary>
/// Autonomous System Number of the client's ISP.
/// </summary>
[JsonProperty("asn")]
public int Asn { get; set; }

/// <summary>
/// Human-readable ISP name (e.g. "innogy TelNet").
/// </summary>
[JsonProperty("asOrganization")]
public string AsOrganization { get; set; }

/// <summary>
/// ISO 3166-1 alpha-2 country code of the client (e.g. "DE").
/// </summary>
[JsonProperty("country")]
public string Country { get; set; }

/// <summary>
/// City of the client (e.g. "Bochum").
/// </summary>
[JsonProperty("city")]
public string City { get; set; }

/// <summary>
/// Cloudflare PoP (Point of Presence) details.
/// </summary>
[JsonProperty("colo")]
public SpeedTestMetaColo Colo { get; set; }
}
46 changes: 46 additions & 0 deletions Source/NETworkManager.Models/Cloudflare/SpeedTestProgress.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
namespace NETworkManager.Models.Cloudflare;

/// <summary>
/// Phase of a Cloudflare speed test run, reported by
/// <see cref="SpeedTestService"/> via <see cref="System.IProgress{T}"/>.
/// </summary>
public enum SpeedTestPhase
{
FetchingMetadata,
MeasuringLatency,
MeasuringDownload,
MeasuringUpload
}

/// <summary>
/// Progress event passed by <see cref="SpeedTestService"/> to update the UI
/// with the currently running measurement phase and the latest live estimate
/// of each metric. Values are <c>null</c> until the first sample for that
/// metric has been collected.
/// </summary>
/// <param name="Phase">Current measurement phase.</param>
/// <param name="DownloadMbps">Live download throughput estimate (Mbps).</param>
/// <param name="UploadMbps">Live upload throughput estimate (Mbps).</param>
/// <param name="LatencyMs">Live latency estimate (50th percentile of probes).</param>
/// <param name="JitterMs">Live jitter estimate (average consecutive delta).</param>
/// <param name="NewDownloadSampleMbps">
/// Set when this emission marks a freshly completed download sample (one HTTP
/// request finished). Mid-stream live updates leave this <c>null</c>.
/// </param>
/// <param name="NewUploadSampleMbps">
/// Set when this emission marks a freshly completed upload sample.
/// </param>
/// <param name="Meta">
/// Cloudflare <c>/meta</c> response, emitted once after metadata is fetched
/// so ISP / location / server details can be displayed before the bandwidth
/// measurements complete.
/// </param>
public record SpeedTestProgress(
SpeedTestPhase Phase,
double? DownloadMbps = null,
double? UploadMbps = null,
double? LatencyMs = null,
double? JitterMs = null,
double? NewDownloadSampleMbps = null,
double? NewUploadSampleMbps = null,
SpeedTestMetaInfo Meta = null);
Loading
Loading