The entry window types contacts and colours them as they are typed; the log, check, bandmap, score and packet windows read the same session. Contacts are shared with the other stations of a multi-operator entry in N1MM's contact message, so an N1MM station on the same network sees them too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
329 lines
11 KiB
C#
329 lines
11 KiB
C#
using Avalonia.Controls;
|
|
using Avalonia.Interactivity;
|
|
using Avalonia.Platform.Storage;
|
|
using Nonemm.App.Configuration;
|
|
using Nonemm.App.Dialogs;
|
|
using Nonemm.Core;
|
|
using Nonemm.Formats.Adif;
|
|
using Nonemm.Formats.Cabrillo;
|
|
using Nonemm.Storage;
|
|
|
|
namespace Nonemm.App.Windows;
|
|
|
|
/// The entry window's menu. Each item does one thing and says what happened in
|
|
/// the status line.
|
|
public sealed partial class EntryWindow
|
|
{
|
|
private static readonly HttpClient Http = new() { Timeout = TimeSpan.FromSeconds(30) };
|
|
|
|
private async void OnNewDatabase(object? sender, RoutedEventArgs e)
|
|
{
|
|
IStorageFile? file = await StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
|
|
{
|
|
Title = "New log database",
|
|
SuggestedFileName = "ham.s3db",
|
|
DefaultExtension = "s3db",
|
|
SuggestedStartLocation = await Folder(session.Paths.Databases),
|
|
});
|
|
OpenDatabaseAt(file);
|
|
}
|
|
|
|
private async void OnOpenDatabase(object? sender, RoutedEventArgs e)
|
|
{
|
|
IReadOnlyList<IStorageFile> files = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
|
|
{
|
|
Title = "Open log database",
|
|
AllowMultiple = false,
|
|
SuggestedStartLocation = await Folder(session.Paths.Databases),
|
|
});
|
|
OpenDatabaseAt(files.FirstOrDefault());
|
|
}
|
|
|
|
private void OpenDatabaseAt(IStorageFile? file)
|
|
{
|
|
if (file?.TryGetLocalPath() is not { } path)
|
|
{
|
|
return;
|
|
}
|
|
try
|
|
{
|
|
session.OpenDatabase(path);
|
|
Status($"log database {Path.GetFileName(path)}");
|
|
}
|
|
catch (Exception error) when (error is IOException or InvalidOperationException)
|
|
{
|
|
Status($"could not open {path}: {error.Message}");
|
|
}
|
|
}
|
|
|
|
private async void OnNewContest(object? sender, RoutedEventArgs e)
|
|
{
|
|
if (!HasDatabase())
|
|
{
|
|
return;
|
|
}
|
|
ContestSetupDialog dialog = new(session);
|
|
ContestInstance? chosen = await dialog.ShowDialog<ContestInstance?>(this);
|
|
if (chosen is null)
|
|
{
|
|
return;
|
|
}
|
|
session.StartContest(chosen);
|
|
Status($"{chosen.ContestName} started");
|
|
}
|
|
|
|
private async void OnOpenContest(object? sender, RoutedEventArgs e)
|
|
{
|
|
if (!HasDatabase())
|
|
{
|
|
return;
|
|
}
|
|
ContestPickerDialog dialog = new(session.Store.Contests());
|
|
int? chosen = await dialog.ShowDialog<int?>(this);
|
|
if (chosen is not null)
|
|
{
|
|
session.OpenContest(chosen.Value);
|
|
}
|
|
}
|
|
|
|
private async void OnExportCabrillo(object? sender, RoutedEventArgs e)
|
|
{
|
|
if (Logging is null)
|
|
{
|
|
Status("no contest is open");
|
|
return;
|
|
}
|
|
IStorageFile? file = await StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
|
|
{
|
|
Title = "Export Cabrillo",
|
|
SuggestedFileName = $"{session.Settings.Station.Callsign}.log",
|
|
});
|
|
if (file?.TryGetLocalPath() is not { } path)
|
|
{
|
|
return;
|
|
}
|
|
CabrilloWriter writer = new(Logging.Contest, Logging.Me);
|
|
CabrilloHeader header = new()
|
|
{
|
|
Contest = Logging.Contest.CabrilloName,
|
|
Callsign = Logging.Me.Callsign,
|
|
OperatorCategory = Logging.Instance.OperatorCategory,
|
|
AssistedCategory = Logging.Instance.AssistedCategory,
|
|
BandCategory = Logging.Instance.BandCategory,
|
|
ModeCategory = Logging.Instance.ModeCategory,
|
|
PowerCategory = Logging.Instance.PowerCategory,
|
|
StationCategory = Logging.Instance.StationCategory,
|
|
TransmitterCategory = Logging.Instance.TransmitterCategory,
|
|
OverlayCategory = Logging.Instance.OverlayCategory,
|
|
TimeCategory = Logging.Instance.TimeCategory,
|
|
ClaimedScore = Logging.Log.TotalScore,
|
|
Club = Logging.Me.Club,
|
|
Name = Logging.Me.Name,
|
|
Operators = Logging.Instance.Operators,
|
|
Soapbox = Logging.Instance.Soapbox,
|
|
};
|
|
await File.WriteAllTextAsync(path, writer.Write(header, Logging.Log.Qsos));
|
|
Status($"{Logging.Log.Qsos.Count} contacts written to {Path.GetFileName(path)}");
|
|
}
|
|
|
|
private async void OnExportAdif(object? sender, RoutedEventArgs e)
|
|
{
|
|
if (Logging is null)
|
|
{
|
|
Status("no contest is open");
|
|
return;
|
|
}
|
|
IStorageFile? file = await StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
|
|
{
|
|
Title = "Export ADIF",
|
|
SuggestedFileName = $"{session.Settings.Station.Callsign}.adi",
|
|
});
|
|
if (file?.TryGetLocalPath() is not { } path)
|
|
{
|
|
return;
|
|
}
|
|
await File.WriteAllTextAsync(path, new AdifWriter(Logging.Me).Write(Logging.Log.Qsos));
|
|
Status($"{Logging.Log.Qsos.Count} contacts written to {Path.GetFileName(path)}");
|
|
}
|
|
|
|
private async void OnImportAdif(object? sender, RoutedEventArgs e)
|
|
{
|
|
if (Logging is null)
|
|
{
|
|
Status("no contest is open");
|
|
return;
|
|
}
|
|
IReadOnlyList<IStorageFile> files = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
|
|
{
|
|
Title = "Import ADIF",
|
|
AllowMultiple = false,
|
|
});
|
|
if (files.FirstOrDefault()?.TryGetLocalPath() is not { } path)
|
|
{
|
|
return;
|
|
}
|
|
IReadOnlyList<Qso> read = AdifReader.Read(
|
|
await File.ReadAllTextAsync(path),
|
|
Logging.Contest.Name,
|
|
Logging.Instance.ContestNumber);
|
|
foreach (Qso qso in read)
|
|
{
|
|
session.Store.Add(qso);
|
|
}
|
|
session.OpenContest(Logging.Instance.ContestNumber);
|
|
Status($"{read.Count} contacts read from {Path.GetFileName(path)}");
|
|
}
|
|
|
|
private void OnExit(object? sender, RoutedEventArgs e) => Close();
|
|
|
|
private void OnShowLog(object? sender, RoutedEventArgs e) => Show(() => new LogWindow(session));
|
|
|
|
private void OnShowCheck(object? sender, RoutedEventArgs e) => Show(() => new CheckWindow(session, () => Logging?.Entry.Call ?? ""));
|
|
|
|
private void OnShowBandmap(object? sender, RoutedEventArgs e) => Show(() => new BandmapWindow(session, Tune));
|
|
|
|
private void OnShowScore(object? sender, RoutedEventArgs e) => Show(() => new ScoreWindow(session));
|
|
|
|
private void OnShowPacket(object? sender, RoutedEventArgs e) => Show(() => new PacketWindow(session));
|
|
|
|
private async void OnStationSettings(object? sender, RoutedEventArgs e)
|
|
{
|
|
StationDialog dialog = new(session.Settings.Station);
|
|
StoredStation? updated = await dialog.ShowDialog<StoredStation?>(this);
|
|
if (updated is not null)
|
|
{
|
|
session.Save(session.Settings with { Station = updated });
|
|
if (Logging is not null)
|
|
{
|
|
session.OpenContest(Logging.Instance.ContestNumber);
|
|
}
|
|
}
|
|
}
|
|
|
|
private async void OnRadioSettings(object? sender, RoutedEventArgs e)
|
|
{
|
|
RadioDialog dialog = new(session.Settings);
|
|
Settings? updated = await dialog.ShowDialog<Settings?>(this);
|
|
if (updated is null)
|
|
{
|
|
return;
|
|
}
|
|
session.Save(updated);
|
|
if (updated.RadioEnabled)
|
|
{
|
|
session.ConnectRadio();
|
|
Status($"radio: rigctld at {updated.RigctldHost}:{updated.RigctldPort}");
|
|
}
|
|
else
|
|
{
|
|
session.DisconnectRadio();
|
|
Status("radio disconnected");
|
|
}
|
|
}
|
|
|
|
private async void OnClusterSettings(object? sender, RoutedEventArgs e)
|
|
{
|
|
ClusterDialog dialog = new(session.Settings);
|
|
Settings? updated = await dialog.ShowDialog<Settings?>(this);
|
|
if (updated is null)
|
|
{
|
|
return;
|
|
}
|
|
session.Save(updated);
|
|
if (updated.ClusterEnabled)
|
|
{
|
|
session.ConnectCluster();
|
|
Status($"cluster: {updated.ClusterHost}:{updated.ClusterPort}");
|
|
}
|
|
else
|
|
{
|
|
session.DisconnectCluster();
|
|
Status("cluster disconnected");
|
|
}
|
|
}
|
|
|
|
private async void OnNetworkSettings(object? sender, RoutedEventArgs e)
|
|
{
|
|
NetworkDialog dialog = new(session.Settings);
|
|
Settings? updated = await dialog.ShowDialog<Settings?>(this);
|
|
if (updated is not null)
|
|
{
|
|
session.Save(updated);
|
|
session.ApplyNetworkSettings();
|
|
Status(updated.NetworkEnabled ? "networked with the other stations" : "networking off");
|
|
}
|
|
}
|
|
|
|
private async void OnDownloadCountryFile(object? sender, RoutedEventArgs e) =>
|
|
await Download(
|
|
downloader => downloader.DownloadCountryFileAsync(session.Paths.CountryFile),
|
|
"country file");
|
|
|
|
private async void OnDownloadCallDatabase(object? sender, RoutedEventArgs e) =>
|
|
await Download(
|
|
downloader => downloader.DownloadCallDatabaseAsync(session.Paths.CallDatabaseFile),
|
|
"callsign database");
|
|
|
|
private void OnReloadSupportFiles(object? sender, RoutedEventArgs e)
|
|
{
|
|
session.ReloadSupportFiles();
|
|
Status($"country file {(session.Countries is null ? "missing" : "loaded")}, " +
|
|
$"{session.Calls.Count} callsigns");
|
|
}
|
|
|
|
private async Task Download(Func<SupportFileDownloader, Task<string>> fetch, string what)
|
|
{
|
|
Status($"fetching the {what}…");
|
|
try
|
|
{
|
|
string result = await fetch(new SupportFileDownloader(Http));
|
|
session.ReloadSupportFiles();
|
|
Status($"{what}: {result}");
|
|
}
|
|
catch (Exception error) when (error is InvalidOperationException or IOException)
|
|
{
|
|
Status(error.Message);
|
|
}
|
|
}
|
|
|
|
private void Tune(Frequency frequency, string call)
|
|
{
|
|
if (Logging is null)
|
|
{
|
|
return;
|
|
}
|
|
Logging.Tune(frequency);
|
|
_ = session.Radio?.TuneAsync(frequency);
|
|
Logging.Entry.Call = call;
|
|
SyncBoxes();
|
|
boxes[0].Focus();
|
|
Refresh();
|
|
}
|
|
|
|
private bool HasDatabase()
|
|
{
|
|
if (session.Settings.DatabasePath.Length > 0)
|
|
{
|
|
return true;
|
|
}
|
|
Status("open a log database first: File ▸ New Database");
|
|
return false;
|
|
}
|
|
|
|
private void Show<T>(Func<T> create) where T : Window
|
|
{
|
|
if (openWindows.TryGetValue(typeof(T), out Window? existing))
|
|
{
|
|
existing.Activate();
|
|
return;
|
|
}
|
|
T window = create();
|
|
openWindows[typeof(T)] = window;
|
|
window.Closed += (_, _) => openWindows.Remove(typeof(T));
|
|
window.Show(this);
|
|
}
|
|
|
|
private async Task<IStorageFolder?> Folder(string path) =>
|
|
await StorageProvider.TryGetFolderFromPathAsync(path);
|
|
}
|