Build / Build (push) Successful in 33s
- Every number shown on a page goes through NumbersExtensions: counts, percentages, the nerd readouts and the values written into a style. - A failure the client builds itself (offline, timeout, retries run out, an answer it can't read) is a 400 that names its kind; only a server's own 5XX passes through. - Login, sign-up, recovery and password confirmations leave autocomplete to the browser, so it can offer the saved login. - The Daisy components start with empty lists and attributes, and a decorative image still renders alt="". - Arasaka.HouseRules.Analyzers checks all of this in every build, together with PrivaPub.ClientModels. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W4YtEJrgxG12Pc39Fai8Bd
60 lines
2.3 KiB
C#
60 lines
2.3 KiB
C#
using System.Globalization;
|
|
|
|
using decePubClient.Extensions;
|
|
|
|
namespace decePubClient.Components.Nerd;
|
|
|
|
/// <summary>One line of a NerdTip; a row without a value is not shown.</summary>
|
|
public sealed record NerdRow(string Label, string Value)
|
|
{
|
|
public static NerdRow Of(string label, object value) => new(label, value switch
|
|
{
|
|
null => null,
|
|
DateTime time => Time(time),
|
|
double number => number.ToReadout(),
|
|
bool flag => flag ? "yes" : "no",
|
|
IFormattable formattable => formattable.ToString(null, CultureInfo.InvariantCulture),
|
|
_ => value.ToString()
|
|
});
|
|
|
|
/// <summary>A UTC time to the millisecond, as PrivaPub records it.</summary>
|
|
public static string Time(DateTime time) =>
|
|
DateTime.SpecifyKind(time, time.Kind == DateTimeKind.Unspecified ? DateTimeKind.Utc : time.Kind).ToUniversalTime()
|
|
.ToString("yyyy-MM-dd HH:mm:ss.fff'Z'", CultureInfo.InvariantCulture);
|
|
|
|
/// <summary>A duration the way a person reads it: 850 ms, 12.4 s, 3 min 5 s, 2 h 10 min, 4 d 3 h.</summary>
|
|
public static string Duration(TimeSpan span)
|
|
{
|
|
var sign = span < TimeSpan.Zero ? "-" : string.Empty;
|
|
span = span.Duration();
|
|
return sign + (span.TotalSeconds switch
|
|
{
|
|
< 1 => $"{span.TotalMilliseconds.ToFixed(0)} ms",
|
|
< 60 => $"{span.TotalSeconds.ToFixed(1)} s",
|
|
< 3600 => $"{span.Minutes} min {span.Seconds} s",
|
|
< 86400 => $"{(int)span.TotalHours} h {span.Minutes} min",
|
|
_ => $"{(int)span.TotalDays} d {span.Hours} h"
|
|
});
|
|
}
|
|
|
|
/// <summary>A byte count: 912 B, 14.2 KiB, 3.1 MiB.</summary>
|
|
public static string Bytes(long? bytes) => bytes switch
|
|
{
|
|
null => null,
|
|
< 1024 => $"{bytes} B",
|
|
< 1024 * 1024 => $"{(bytes.Value / 1024d).ToFixed(1)} KiB",
|
|
_ => $"{(bytes.Value / 1048576d).ToFixed(1)} MiB"
|
|
};
|
|
|
|
/// <summary>The great-circle distance between two points in kilometres (haversine, Earth radius 6371 km).</summary>
|
|
public static double Kilometres(double latitude1, double longitude1, double latitude2, double longitude2)
|
|
{
|
|
const double radius = 6371;
|
|
double Radians(double degrees) => degrees * Math.PI / 180;
|
|
var dLatitude = Radians(latitude2 - latitude1);
|
|
var dLongitude = Radians(longitude2 - longitude1);
|
|
var a = Math.Pow(Math.Sin(dLatitude / 2), 2) + Math.Cos(Radians(latitude1)) * Math.Cos(Radians(latitude2)) * Math.Pow(Math.Sin(dLongitude / 2), 2);
|
|
return 2 * radius * Math.Asin(Math.Min(1, Math.Sqrt(a)));
|
|
}
|
|
}
|