The root is Documents/N1MM Logger+ now, so a station that runs both programs has one set of files rather than two: one Databases folder, one SupportFiles, one Wav with a folder per operator in it. What N1MM has no place for goes in a Nonemm folder inside it, which is where settings.json sits. A system that does not say where Documents is — most Linux systems — gets ~/Documents/N1MM Logger+. Files an older version kept in Documents\Nonemm or ~/.config/nonemm are copied in the first time this version runs, and the log and call history paths in the settings are moved with them, so the copy that opens is the copy that came over. The older folder is left as it was. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
221 lines
8.2 KiB
C#
221 lines
8.2 KiB
C#
namespace Nonemm.App.Configuration;
|
|
|
|
/// Where the operator's files live: `Documents/N1MM Logger+`, which is N1MM's
|
|
/// own folder, with N1MM's folders inside it and N1MM's names on them. A
|
|
/// station that runs both programs keeps one set of files:
|
|
/// `Wav/<operator>/Cq.wav` is where N1MM plays that operator's recordings from,
|
|
/// and so is this.
|
|
///
|
|
/// What N1MM has no place for goes in a `Nonemm` folder of its own, which is
|
|
/// where the settings file sits. N1MM's three `*DDL` folders and its `Piper`
|
|
/// folders are not made: those hold the migrations for N1MM's own admin
|
|
/// databases and the program it speaks with, and neither has anything to do
|
|
/// with a log.
|
|
public sealed class UserPaths
|
|
{
|
|
public UserPaths(string root)
|
|
{
|
|
Root = root;
|
|
Databases = Path.Combine(root, "Databases");
|
|
UserDefinedContests = Path.Combine(root, "UserDefinedContests");
|
|
SupportFiles = Path.Combine(root, "SupportFiles");
|
|
FunctionKeyMessages = Path.Combine(root, "FunctionKeyMessages");
|
|
CallHistoryFiles = Path.Combine(root, "CallHistoryFiles");
|
|
ExportFiles = Path.Combine(root, "ExportFiles");
|
|
GoalFiles = Path.Combine(root, "GoalFiles");
|
|
LettersFiles = Path.Combine(root, "LettersFiles");
|
|
QsoRecording = Path.Combine(root, "QsoRecording");
|
|
SkinsAndLayouts = Path.Combine(root, "SkinsAndLayouts");
|
|
SystemFiles = Path.Combine(root, "SystemFiles");
|
|
TransactionLogFiles = Path.Combine(root, "TransactionLogFiles");
|
|
Diagnostics = Path.Combine(root, "Diagnostics");
|
|
Wav = Path.Combine(root, "Wav");
|
|
Own = Path.Combine(root, "Nonemm");
|
|
}
|
|
|
|
public static UserPaths Default() => new(DefaultRoot());
|
|
|
|
public string Root { get; }
|
|
|
|
public string Databases { get; }
|
|
|
|
public string UserDefinedContests { get; }
|
|
|
|
public string SupportFiles { get; }
|
|
|
|
/// The `.mc` function key files, which N1MM imports and exports here.
|
|
public string FunctionKeyMessages { get; }
|
|
|
|
public string CallHistoryFiles { get; }
|
|
|
|
/// Cabrillo and ADIF, in and out.
|
|
public string ExportFiles { get; }
|
|
|
|
/// N1MM's rate targets, its letter and number recordings, its recorded
|
|
/// contacts, its window layouts, its own settings files, its transaction
|
|
/// logs and its traces. Nothing here writes to them yet; they are made so
|
|
/// that a station that keeps files in them does not lose them when it moves
|
|
/// between the two programs.
|
|
public string GoalFiles { get; }
|
|
|
|
public string LettersFiles { get; }
|
|
|
|
public string QsoRecording { get; }
|
|
|
|
public string SkinsAndLayouts { get; }
|
|
|
|
public string SystemFiles { get; }
|
|
|
|
public string TransactionLogFiles { get; }
|
|
|
|
public string Diagnostics { get; }
|
|
|
|
/// The voice keyer's recordings. Each operator has a folder of his own
|
|
/// under it, named for his callsign, which is how N1MM finds the voice of
|
|
/// whoever is at the microphone.
|
|
public string Wav { get; }
|
|
|
|
/// This program's own folder inside N1MM's, for what N1MM has no place
|
|
/// for.
|
|
public string Own { get; }
|
|
|
|
public string SettingsFile => Path.Combine(Own, "settings.json");
|
|
|
|
public string CountryFile => Path.Combine(SupportFiles, "wl_cty.dat");
|
|
|
|
public string CallDatabaseFile => Path.Combine(SupportFiles, "MASTER.SCP");
|
|
|
|
/// The published list of cluster nodes, kept as the page it was downloaded
|
|
/// from so a later version can read more out of it.
|
|
public string ClusterListFile => Path.Combine(SupportFiles, "cluster-list.html");
|
|
|
|
/// One operator's recordings: `Wav/OM3KFF/Cq.wav` and the rest. Without an
|
|
/// operator the folder itself is the answer, which is where a station with
|
|
/// one operator keeps them.
|
|
public string WavFor(string operatorCallsign) =>
|
|
operatorCallsign.Trim().Length == 0
|
|
? Wav
|
|
: Path.Combine(Wav, operatorCallsign.Trim().ToUpperInvariant());
|
|
|
|
public void CreateFolders()
|
|
{
|
|
foreach (string folder in All())
|
|
{
|
|
Directory.CreateDirectory(folder);
|
|
}
|
|
}
|
|
|
|
/// Every folder that is made at startup, the root first.
|
|
public IReadOnlyList<string> All() =>
|
|
[
|
|
Root,
|
|
Databases,
|
|
UserDefinedContests,
|
|
SupportFiles,
|
|
FunctionKeyMessages,
|
|
CallHistoryFiles,
|
|
ExportFiles,
|
|
GoalFiles,
|
|
LettersFiles,
|
|
QsoRecording,
|
|
SkinsAndLayouts,
|
|
SystemFiles,
|
|
TransactionLogFiles,
|
|
Diagnostics,
|
|
Wav,
|
|
Own,
|
|
];
|
|
|
|
/// N1MM's folder name, under Documents. On a system that does not say
|
|
/// where Documents is — which is most Linux systems until someone sets it —
|
|
/// it is `~/Documents`, the same place a desktop would put it.
|
|
private static string DefaultRoot() => Path.Combine(Documents(), "N1MM Logger+");
|
|
|
|
private static string Documents()
|
|
{
|
|
if (Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments) is { Length: > 0 } known)
|
|
{
|
|
return known;
|
|
}
|
|
return Environment.GetEnvironmentVariable("XDG_DOCUMENTS_DIR") is { Length: > 0 } xdg
|
|
? xdg
|
|
: Path.Combine(
|
|
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
|
"Documents");
|
|
}
|
|
|
|
/// Where versions before this one kept everything. Their files are copied
|
|
/// into N1MM's folders the first time this one runs.
|
|
public static string LegacyRoot() =>
|
|
OperatingSystem.IsWindows()
|
|
? Path.Combine(
|
|
Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
|
|
"Nonemm")
|
|
: Path.Combine(
|
|
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
|
"nonemm");
|
|
|
|
/// Copies what an older version kept into these folders, once: it runs only
|
|
/// while there is no settings file here and there is one there. The older
|
|
/// folder is left as it was, so nothing is lost if the operator goes back
|
|
/// to the older version.
|
|
///
|
|
/// Paths inside the settings that pointed into the older folder — the log
|
|
/// database and the call history file — are moved with it, so the copy that
|
|
/// is opened is the copy that was brought over.
|
|
public bool AdoptOlderFiles(string? legacyRoot = null)
|
|
{
|
|
string older = legacyRoot ?? LegacyRoot();
|
|
string olderSettings = Path.Combine(older, "settings.json");
|
|
if (File.Exists(SettingsFile) || !File.Exists(olderSettings) || SameFolder(older, Root))
|
|
{
|
|
return false;
|
|
}
|
|
Copy(older, Root);
|
|
Settings settings = Settings.Load(olderSettings);
|
|
settings = settings with
|
|
{
|
|
DatabasePath = Brought(settings.DatabasePath, older),
|
|
CallHistoryFile = Brought(settings.CallHistoryFile, older),
|
|
};
|
|
settings.Save(SettingsFile);
|
|
return true;
|
|
}
|
|
|
|
/// A file that was inside the older folder is now inside this one, in the
|
|
/// same place under it. A file kept anywhere else is left where it is.
|
|
private string Brought(string path, string older)
|
|
{
|
|
if (path.Trim().Length == 0
|
|
|| !path.StartsWith(older + Path.DirectorySeparatorChar, StringComparison.Ordinal))
|
|
{
|
|
return path;
|
|
}
|
|
return Path.Combine(Root, path[(older.Length + 1)..]);
|
|
}
|
|
|
|
private static void Copy(string from, string to)
|
|
{
|
|
Directory.CreateDirectory(to);
|
|
foreach (string file in Directory.GetFiles(from))
|
|
{
|
|
// the settings file is written again afterwards, with the paths
|
|
// inside it moved
|
|
if (!Path.GetFileName(file).Equals("settings.json", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
File.Copy(file, Path.Combine(to, Path.GetFileName(file)), overwrite: false);
|
|
}
|
|
}
|
|
foreach (string folder in Directory.GetDirectories(from))
|
|
{
|
|
Copy(folder, Path.Combine(to, Path.GetFileName(folder)));
|
|
}
|
|
}
|
|
|
|
private static bool SameFolder(string one, string other) =>
|
|
string.Equals(
|
|
Path.TrimEndingDirectorySeparator(Path.GetFullPath(one)),
|
|
Path.TrimEndingDirectorySeparator(Path.GetFullPath(other)),
|
|
StringComparison.Ordinal);
|
|
}
|