Lay out Available Mults and Qs like N1MM's window

The window is now a summary table of mults, Qs and totals per band over a
sortable spot grid with Call, Freq, Mode, Dir, Mlt, SN, TS, Spotter, Grid,
Sun, Pts and a preferred-spotter column. Columns can be hidden, headers
sorted, and the list stops re-sorting for six seconds under the mouse.

Adds the Mults / Mults & Qs button, a Bands & Modes filter, delete and
blacklist from the right-click menu, Alt+A stepping, and QSY to the top spot
after logging. Spot gains Snr and IsSkimmer, Bandmap gains Remaining, and
AvailableStation carries the path to the station.

The menu item no longer has apostrophes. SO2R, SO2V and the other missing
pieces are listed in docs/unfinished.md.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-25 11:56:32 +00:00
co-authored by Claude Sonnet 5.5
parent 5cf8453534
commit 0a23815f38
15 changed files with 1051 additions and 73 deletions
@@ -177,4 +177,14 @@ public class AvailableStationsTests
AvailableStation station = Assert.Single(available.All());
Assert.False(station.IsMultiplier);
}
[Fact]
public void AStationInJapanIsToTheNortheastOfGermany()
{
(_, Bandmap bandmap, AvailableStations available) = Station();
bandmap.Add(At("JA1XYZ", On20));
AvailableStation station = Assert.Single(available.All());
Assert.InRange(station.Heading ?? -1, 1, 89);
}
}
@@ -118,4 +118,22 @@ public class BandmapTests
map.Near(Frequency.FromKilohertz(14_010.2), Frequency.FromKilohertz(0.5))?.Call.Text);
Assert.Null(map.Near(Frequency.FromKilohertz(14_030), Frequency.FromKilohertz(0.5)));
}
[Fact]
public void ASpotHasItsLifetimeLessTheTimeItHasBeenThere()
{
DateTime clock = Now;
Bandmap map = new(TimeSpan.FromMinutes(30), () => clock);
Spot spot = At("DL1ABC", 14_025);
map.Add(spot);
clock = Now.AddMinutes(12);
Assert.Equal(TimeSpan.FromMinutes(18), map.Remaining(spot));
}
[Fact]
public void ASpotThatIsNotOnTheMapHasNoTimeLeft()
{
Assert.Null(new Bandmap().Remaining(At("DL1ABC", 14_025)));
}
}
@@ -0,0 +1,28 @@
using Nonemm.Core;
namespace Nonemm.Spotting.Tests;
public class SpotSnrTests
{
private static Spot Commented(string comment, string spotter = "OK1TEST") =>
new(Callsign.Parse("DL1ABC"), Frequency.FromKilohertz(14_025), DateTime.UtcNow, SpotSource.Cluster, spotter, comment);
[Fact]
public void ASkimmerCommentGivesTheDecibels()
{
Assert.Equal(24, Commented("CW 24 dB 28 WPM CQ").Snr);
}
[Fact]
public void ACommentWithNoDecibelsHasNoSnr()
{
Assert.Null(Commented("up 2").Snr);
}
[Fact]
public void ASpotterEndingInHashIsASkimmer()
{
Assert.True(Commented("", "DL1ABC-#").IsSkimmer);
Assert.False(Commented("", "DL1ABC").IsSkimmer);
}
}