Move the files into N1MM's own folder
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
This commit is contained in:
22
README.md
22
README.md
@@ -402,9 +402,9 @@ of a degree.
|
||||
|
||||
### Where the files live
|
||||
|
||||
The program keeps its files under `Documents\Nonemm` on Windows and
|
||||
`~/.config/nonemm` elsewhere. The folders inside are N1MM's, with N1MM's names,
|
||||
so a station that runs both keeps its files in the same shape:
|
||||
The program keeps its files in N1MM's own folder, `Documents/N1MM Logger+`,
|
||||
with N1MM's folders inside it under N1MM's names. A station that runs both
|
||||
programs has one set of files rather than two:
|
||||
|
||||
| Folder | What goes in it |
|
||||
|---|---|
|
||||
@@ -416,10 +416,20 @@ so a station that runs both keeps its files in the same shape:
|
||||
| `ExportFiles` | Cabrillo and ADIF, in and out |
|
||||
| `Wav` | the voice keyer's recordings, one folder per operator: `Wav/OM3KFF/Cq.wav` |
|
||||
| `GoalFiles`, `LettersFiles`, `QsoRecording`, `SkinsAndLayouts`, `SystemFiles`, `TransactionLogFiles`, `Diagnostics` | N1MM's, and made for the same reason: a station that keeps files in them does not lose them moving between the two programs. Nothing here writes to them yet |
|
||||
| `Nonemm` | this program's own, for what N1MM has no place for: `settings.json` |
|
||||
|
||||
`settings.json` sits in the root. 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.
|
||||
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.
|
||||
|
||||
On a system that does not say where Documents is — most Linux systems, until
|
||||
someone sets `XDG_DOCUMENTS_DIR` — it is `~/Documents/N1MM Logger+`.
|
||||
|
||||
A version before this one kept everything in `Documents\Nonemm` or
|
||||
`~/.config/nonemm`. Those files are copied into N1MM's folders 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.
|
||||
|
||||
### Operators
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ public partial class App : Application
|
||||
{
|
||||
UserPaths paths = UserPaths.Default();
|
||||
paths.CreateFolders();
|
||||
paths.AdoptOlderFiles();
|
||||
Settings settings = Settings.Load(paths.SettingsFile);
|
||||
Themes.Use(settings.Theme);
|
||||
Themes.Changed += (_, _) => ApplyTheme();
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
namespace Nonemm.App.Configuration;
|
||||
|
||||
/// Where the operator's files live. Windows keeps them under Documents the way
|
||||
/// N1MM does; everywhere else follows the XDG configuration directory.
|
||||
/// 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.
|
||||
///
|
||||
/// The folders inside are N1MM's own, with N1MM's names, so a station that runs
|
||||
/// both programs keeps its files in the same shape: `Wav/<operator>/Cq.wav` is
|
||||
/// where N1MM looks for that operator's recordings, and so is this. N1MM's
|
||||
/// three `*DDL` folders and its `Piper` folders are left out: 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.
|
||||
/// 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)
|
||||
@@ -28,6 +30,7 @@ public sealed class UserPaths
|
||||
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());
|
||||
@@ -72,7 +75,11 @@ public sealed class UserPaths
|
||||
/// whoever is at the microphone.
|
||||
public string Wav { get; }
|
||||
|
||||
public string SettingsFile => Path.Combine(Root, "settings.json");
|
||||
/// 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");
|
||||
|
||||
@@ -116,9 +123,30 @@ public sealed class UserPaths
|
||||
TransactionLogFiles,
|
||||
Diagnostics,
|
||||
Wav,
|
||||
Own,
|
||||
];
|
||||
|
||||
private static string DefaultRoot() =>
|
||||
/// 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),
|
||||
@@ -126,4 +154,67 @@ public sealed class UserPaths
|
||||
: 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);
|
||||
}
|
||||
|
||||
@@ -26,8 +26,8 @@ public class UserPathsTests : IDisposable
|
||||
Assert.Equal(
|
||||
[
|
||||
"CallHistoryFiles", "Databases", "Diagnostics", "ExportFiles", "FunctionKeyMessages",
|
||||
"GoalFiles", "LettersFiles", "QsoRecording", "SkinsAndLayouts", "SupportFiles",
|
||||
"SystemFiles", "TransactionLogFiles", "UserDefinedContests", "Wav",
|
||||
"GoalFiles", "LettersFiles", "Nonemm", "QsoRecording", "SkinsAndLayouts",
|
||||
"SupportFiles", "SystemFiles", "TransactionLogFiles", "UserDefinedContests", "Wav",
|
||||
],
|
||||
made);
|
||||
}
|
||||
@@ -53,4 +53,45 @@ public class UserPathsTests : IDisposable
|
||||
|
||||
Assert.True(Directory.Exists(paths.Wav));
|
||||
}
|
||||
|
||||
/// What N1MM has no place for lives in a folder of this program's own.
|
||||
[Fact]
|
||||
public void TheSettingsFileIsInOurOwnFolder()
|
||||
{
|
||||
UserPaths paths = new(root);
|
||||
|
||||
Assert.Equal(Path.Combine(root, "Nonemm", "settings.json"), paths.SettingsFile);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnOlderVersionsFilesAreCopiedInOnce()
|
||||
{
|
||||
string older = Path.Combine(root, "older");
|
||||
Directory.CreateDirectory(Path.Combine(older, "Databases"));
|
||||
File.WriteAllText(Path.Combine(older, "Databases", "om5m.s3db"), "log");
|
||||
string database = Path.Combine(older, "Databases", "om5m.s3db");
|
||||
new Settings { DatabasePath = database }.Save(Path.Combine(older, "settings.json"));
|
||||
UserPaths paths = new(Path.Combine(root, "new"));
|
||||
paths.CreateFolders();
|
||||
|
||||
Assert.True(paths.AdoptOlderFiles(older));
|
||||
|
||||
Assert.Equal("log", File.ReadAllText(Path.Combine(paths.Databases, "om5m.s3db")));
|
||||
// the log that was opened is the log that was brought over
|
||||
Assert.Equal(
|
||||
Path.Combine(paths.Databases, "om5m.s3db"),
|
||||
Settings.Load(paths.SettingsFile).DatabasePath);
|
||||
Assert.False(paths.AdoptOlderFiles(older));
|
||||
// the older folder is left as it was
|
||||
Assert.True(File.Exists(database));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NothingIsCopiedWhenThereIsNoOlderFolder()
|
||||
{
|
||||
UserPaths paths = new(root);
|
||||
paths.CreateFolders();
|
||||
|
||||
Assert.False(paths.AdoptOlderFiles(Path.Combine(root, "nowhere")));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user