Release v3.8: New profile (blank template renamed), password-mismatch warning stays up, IP-pinning docs
New profile: a File-menu item + Ctrl+N that loads a fresh blank template as a new unsaved session via a LoadBlankTemplateNext handoff to Program.cs's relaunch loop — reachable even when "start with a specific profile" boots past the picker (issue #6). Offers to save the current profile first if dirty; deliberately silent (no profile-switch cue). Renamed the user-facing "blank template" to "New profile": the picker's synthetic entry (now a distinct marker TYPE, collision-safe against a real profile named "New profile"), the window title ("RemSound — New profile"), and the manual. Fixed the password-mismatch warning flashing away: it's raised from the 1 Hz statusTimer, which kept firing into the modal loop and rebuilt the peer lists (SyncAllPeerLists) under the dialog, knocking it out of the foreground. Now the tick is frozen while it's up, it's routed through ForegroundDialog, and a re-entry guard ensures one warning that stays put. Audited: it was the only popup raised from a recurring timer. Docs: manual section "Connecting to one specific IP address (and only that one)" explaining by-name vs by-fixed-IP and that a profile saves the exact address (issue #7 — functionality already existed); About box + RELEASE_NOTES for v3.8; MANUAL.md regenerated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
9e247112cb
commit
04b17ff1ab
@@ -20,6 +20,23 @@ internal sealed class AboutDialog : Form
|
||||
/// updates" path.</summary>
|
||||
private const string ReleaseNotes =
|
||||
"""
|
||||
RemSound v3.8
|
||||
|
||||
You can now start a brand-new profile at any time. A
|
||||
new "New profile" item at the top of the File menu (or
|
||||
Ctrl+N) opens a fresh, unsaved profile, so you can set up
|
||||
a profile for a different connection from scratch — even
|
||||
when RemSound is set to start straight into a specific
|
||||
profile and you'd otherwise never see the picker. If your
|
||||
current profile has unsaved changes, it offers to save
|
||||
them first. The startup picker and the window title now
|
||||
say "New profile" too, where they used to say "blank
|
||||
template".
|
||||
|
||||
And the "you and the other person have different
|
||||
passwords" warning now stays on screen until you press
|
||||
OK, instead of flashing away before you could reach it.
|
||||
|
||||
RemSound v3.7
|
||||
|
||||
Changing audio devices mid-session is smoother. A quick
|
||||
|
||||
@@ -537,6 +537,13 @@ public sealed class MainForm : Form
|
||||
private string? currentProfilePath;
|
||||
private Profile? pendingProfile;
|
||||
public string? NextProfileTitleToLoad { get; private set; }
|
||||
|
||||
/// <summary>Set by File → New profile. Program.cs's relaunch loop checks this FIRST and, when
|
||||
/// true, rebuilds the form on a fresh blank template (<see cref="Profile.NewBlank"/>, no title)
|
||||
/// instead of loading a saved profile. It's the only way to reach a blank template mid-session
|
||||
/// — the fix for being unable to create a new profile when "start with a specific profile"
|
||||
/// boots the user straight past the picker (issue #6).</summary>
|
||||
public bool LoadBlankTemplateNext { get; private set; }
|
||||
/// <summary>Full path of the next profile to load, set when the user opens a file via
|
||||
/// File → Open profile. Program.cs prefers this over <see cref="NextProfileTitleToLoad"/>
|
||||
/// when non-null — it deserialises the JSON from this exact path, not from the active
|
||||
@@ -1442,6 +1449,17 @@ public sealed class MainForm : Form
|
||||
var fileMenu = new ToolStripMenuItem("&File") { AccessibleName = "File menu" };
|
||||
var helpMenu = new ToolStripMenuItem("&Help") { AccessibleName = "Help menu" };
|
||||
|
||||
// New profile — starts a fresh blank template as a new unsaved session. Lives at the top of
|
||||
// the File menu (the conventional New / Open / Save order) and, crucially, is reachable even
|
||||
// when "start with a specific profile" boots straight past the picker (issue #6). Mnemonic
|
||||
// Alt+F, W ('w' — N is taken by Minimise) plus the conventional Ctrl+N global shortcut.
|
||||
var newProfileItem = new ToolStripMenuItem("Ne&w profile")
|
||||
{
|
||||
ShortcutKeys = Keys.Control | Keys.N,
|
||||
AccessibleName = "New profile",
|
||||
};
|
||||
newProfileItem.Click += (_, _) => NewProfile();
|
||||
|
||||
var openItem = new ToolStripMenuItem("&Open profile...")
|
||||
{
|
||||
ShortcutKeys = Keys.Control | Keys.O,
|
||||
@@ -1534,6 +1552,7 @@ public sealed class MainForm : Form
|
||||
|
||||
fileMenu.DropDownItems.AddRange(new ToolStripItem[]
|
||||
{
|
||||
newProfileItem,
|
||||
openItem,
|
||||
recentProfilesMenu,
|
||||
saveItem,
|
||||
@@ -1810,6 +1829,49 @@ public sealed class MainForm : Form
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>File → New profile. Starts a fresh blank template as a new unsaved session — the way
|
||||
/// to create a profile from scratch even when "start with a specific profile" boots the user
|
||||
/// straight past the picker (issue #6). Offers to save the current profile first if it has
|
||||
/// unsaved changes, then hands off to Program.cs's loop via <see cref="LoadBlankTemplateNext"/>.</summary>
|
||||
private void NewProfile()
|
||||
{
|
||||
// Don't silently lose unsaved work when abandoning the current session for a blank one.
|
||||
if (unsavedChanges && profileStore is not null && !currentProfileReadOnly)
|
||||
{
|
||||
var result = MessageBox.Show(this,
|
||||
"You have unsaved changes to your current profile. Save them before starting a new profile?\n\n" +
|
||||
"Yes — save, then start a new profile.\nNo — discard the changes and start a new profile.\nCancel — stay where you are.",
|
||||
"RemSound — unsaved changes",
|
||||
MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question, MessageBoxDefaultButton.Button3);
|
||||
if (result == DialogResult.Cancel) return;
|
||||
if (result == DialogResult.Yes)
|
||||
{
|
||||
if (string.IsNullOrEmpty(currentProfileTitle))
|
||||
{
|
||||
// Current session is itself a blank template — needs a name before it can be saved.
|
||||
var saveTitle = ProfileSaveAsPrompt.Show(this, profileStore, null);
|
||||
if (string.IsNullOrEmpty(saveTitle)) return; // cancelled the name prompt → abort the whole thing
|
||||
SaveProfileTo(saveTitle, showConfirmation: false);
|
||||
}
|
||||
else
|
||||
{
|
||||
SaveProfileTo(currentProfileTitle, showConfirmation: false);
|
||||
}
|
||||
}
|
||||
// No → fall through, discarding the unsaved changes.
|
||||
}
|
||||
|
||||
// No profile-switch cue here — deliberately. Unlike Recent/Open (which play it on click),
|
||||
// opening a fresh blank template should be silent; the switch sound feels wrong for "start
|
||||
// new" (Ed, 2026-06-11). The rebuilt form never replays the cue either (see OnShown), so the
|
||||
// entire New-profile path stays quiet.
|
||||
LoadBlankTemplateNext = true;
|
||||
// Stay in the tray if we were there, mirroring the quick-switch behaviour.
|
||||
startNextInstanceMinimized = !Visible || WindowState == FormWindowState.Minimized;
|
||||
AppendLogEntry("new profile: loading blank template");
|
||||
Close();
|
||||
}
|
||||
|
||||
/// <summary>Build the Record menu — Start/stop recording (toggling label), recording
|
||||
/// settings dialog, open the configured folder, and change the configured folder.
|
||||
/// Ctrl+R is the global toggle so the user can start/stop without going through the
|
||||
@@ -5699,14 +5761,14 @@ public sealed class MainForm : Form
|
||||
|
||||
/// <summary>Window title shows the active profile name explicitly so the user knows what
|
||||
/// they're editing. Format: "RemSound — Active profile: My profile name" (loaded) or
|
||||
/// just "RemSound" (blank template). Read-only profiles get a " (read-only)" suffix so
|
||||
/// "RemSound — New profile" (a fresh, unsaved session). Read-only profiles get a " (read-only)" suffix so
|
||||
/// NVDA announces the lock state on every title change and sighted users see it at a
|
||||
/// glance — important context that "anything I change here won't be saved".</summary>
|
||||
private string FormatWindowTitle(string? loadedTitle)
|
||||
{
|
||||
var readOnlySuffix = currentProfileReadOnly ? " (read-only)" : "";
|
||||
return string.IsNullOrEmpty(loadedTitle)
|
||||
? $"{AppName}{readOnlySuffix}"
|
||||
? $"{AppName} — New profile{readOnlySuffix}"
|
||||
: $"{AppName} — Active profile: {loadedTitle}{readOnlySuffix}";
|
||||
}
|
||||
|
||||
@@ -6080,11 +6142,16 @@ public sealed class MainForm : Form
|
||||
MarkProfileDirty();
|
||||
}
|
||||
|
||||
/// <summary>True while a peer-security warning dialog is on screen — set so the 1 Hz status
|
||||
/// tick that raises it can't re-enter and stack (or churn the UI under) a second one.</summary>
|
||||
private bool securityWarningShowing;
|
||||
|
||||
/// <summary>Once a second, surface any password mismatch / out-of-date peer the receiver has
|
||||
/// detected from peers' advertised fingerprints — once per change, not every tick — so a
|
||||
/// silent encrypted stream is never an unexplained mystery.</summary>
|
||||
private void CheckPeerSecurity()
|
||||
{
|
||||
if (securityWarningShowing) return; // a warning is already up — don't re-enter or stack
|
||||
foreach (var kv in receiver.GetPeerSecurityStatuses())
|
||||
{
|
||||
var addr = kv.Key;
|
||||
@@ -6101,7 +6168,24 @@ public sealed class MainForm : Form
|
||||
var msg = status == PeerSecurityStatus.PasswordMismatch
|
||||
? $"You and {addr} have different passwords, so no audio will pass between you.\n\nMake sure you've both set the same password (File → Change this profile's password)."
|
||||
: $"{addr} is running an older version of RemSound that can't connect securely. They need to update before audio can flow between you.";
|
||||
MessageBox.Show(this, msg, AppName, MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
// Show it front-and-centre, and FREEZE the 1 Hz status tick while it's up. This dialog is
|
||||
// raised FROM that tick; left running, the tick keeps firing into the modal loop and
|
||||
// re-runs the peer-list rebuild (SyncAllPeerLists) UNDER the dialog, which knocks it out
|
||||
// of the foreground — it "flashed away before I could click OK" (Ed, 2026-06-11). The
|
||||
// timer-stop + re-entry guard make exactly one warning show and stay put until dismissed.
|
||||
securityWarningShowing = true;
|
||||
statusTimer.Stop();
|
||||
try
|
||||
{
|
||||
ForegroundDialog.Show(owner =>
|
||||
MessageBox.Show(owner, msg, AppName, MessageBoxButtons.OK, MessageBoxIcon.Warning));
|
||||
}
|
||||
finally
|
||||
{
|
||||
statusTimer.Start();
|
||||
securityWarningShowing = false;
|
||||
}
|
||||
return; // one warning per tick; a second affected peer surfaces on the next tick
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7241,7 +7325,7 @@ public sealed class MainForm : Form
|
||||
// is what unblocks NVDA-less or remote-session-dropped shutdowns from deadlocking
|
||||
// on a dialog the user can't reach.
|
||||
var skipPrompt = !string.IsNullOrEmpty(NextProfileTitleToLoad) || ReloadFromScratch
|
||||
|| currentProfileReadOnly || updatingInProgress;
|
||||
|| LoadBlankTemplateNext || currentProfileReadOnly || updatingInProgress;
|
||||
|
||||
if (!skipPrompt && profileStore is not null && unsavedChanges)
|
||||
{
|
||||
|
||||
@@ -4,7 +4,7 @@ namespace RemSound.App;
|
||||
|
||||
/// <summary>
|
||||
/// Modal dialog shown at app startup to pick which profile to load. Listbox of saved
|
||||
/// profile titles plus a synthetic "(Blank template)" entry for an unsaved-defaults
|
||||
/// profile titles plus a synthetic "New profile" entry for a fresh unsaved-defaults
|
||||
/// session. Enter or OK selects; Esc does nothing (deliberately disabled — picking is
|
||||
/// required); Alt+F4 closes the dialog and exits the app; Del on a profile prompts to
|
||||
/// delete it with a yes/no confirm. The user can also browse to a custom profiles
|
||||
@@ -19,7 +19,15 @@ namespace RemSound.App;
|
||||
/// </summary>
|
||||
internal sealed class ProfileSelectionDialog : Form
|
||||
{
|
||||
private const string BlankTemplateLabel = "(Blank template)";
|
||||
// The synthetic top-of-list entry that starts a fresh, unsaved session. A distinct marker TYPE
|
||||
// (not a magic string) so it can never be confused with a real profile a user happens to name
|
||||
// "New profile" — identity is by type; the display text is its ToString().
|
||||
private const string NewProfileLabel = "New profile";
|
||||
private static readonly NewProfileMarker NewProfileEntry = new();
|
||||
private sealed record NewProfileMarker
|
||||
{
|
||||
public override string ToString() => NewProfileLabel;
|
||||
}
|
||||
|
||||
private ProfileStore store;
|
||||
private readonly ListBox listBox;
|
||||
@@ -59,7 +67,7 @@ internal sealed class ProfileSelectionDialog : Form
|
||||
|
||||
var instructions = new Label
|
||||
{
|
||||
Text = "Select a profile and press Enter, or pick \"" + BlankTemplateLabel + "\" to start fresh.",
|
||||
Text = "Select a profile and press Enter, or pick \"" + NewProfileLabel + "\" to start a new one.",
|
||||
Dock = DockStyle.Top,
|
||||
AutoSize = false,
|
||||
Height = 36,
|
||||
@@ -123,7 +131,7 @@ internal sealed class ProfileSelectionDialog : Form
|
||||
var prevSelectedTitle = GetSelectedTitle();
|
||||
listBox.BeginUpdate();
|
||||
listBox.Items.Clear();
|
||||
listBox.Items.Add(BlankTemplateLabel);
|
||||
listBox.Items.Add(NewProfileEntry);
|
||||
foreach (var t in store.ListProfileTitles())
|
||||
{
|
||||
// Wrap each title in a ProfileListItem so the displayed text can carry a
|
||||
@@ -153,9 +161,9 @@ internal sealed class ProfileSelectionDialog : Form
|
||||
folderLabel.AccessibleName = folderLabel.Text;
|
||||
}
|
||||
|
||||
/// <summary>Returns the currently-selected profile title (or the BlankTemplateLabel
|
||||
/// constant for the blank template), unwrapping the ProfileListItem if needed. Returns
|
||||
/// null when nothing is selected. Used by accept / delete to key into the store.</summary>
|
||||
/// <summary>Returns the currently-selected profile title, unwrapping the ProfileListItem if
|
||||
/// needed. Returns null for the synthetic "New profile" entry and when nothing is selected
|
||||
/// (callers distinguish those by checking the item type directly).</summary>
|
||||
private string? GetSelectedTitle()
|
||||
{
|
||||
var item = listBox.SelectedItem;
|
||||
@@ -165,6 +173,7 @@ internal sealed class ProfileSelectionDialog : Form
|
||||
private static string? TitleOfItem(object? item) => item switch
|
||||
{
|
||||
null => null,
|
||||
NewProfileMarker => null, // the synthetic "New profile" entry has no saved title
|
||||
string s => s,
|
||||
ProfileListItem p => p.Title,
|
||||
_ => item.ToString(),
|
||||
@@ -197,21 +206,24 @@ internal sealed class ProfileSelectionDialog : Form
|
||||
|
||||
private void Accept()
|
||||
{
|
||||
var selected = GetSelectedTitle();
|
||||
if (string.IsNullOrEmpty(selected)) return;
|
||||
if (selected == BlankTemplateLabel)
|
||||
var item = listBox.SelectedItem;
|
||||
if (item is null) return;
|
||||
if (item is NewProfileMarker)
|
||||
{
|
||||
// New profile = a fresh unsaved session; a null title/profile signals that to the caller.
|
||||
SelectedTitle = null;
|
||||
SelectedProfile = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
var selected = TitleOfItem(item);
|
||||
if (string.IsNullOrEmpty(selected)) return;
|
||||
SelectedTitle = selected;
|
||||
SelectedProfile = store.Load(selected);
|
||||
if (SelectedProfile is null)
|
||||
{
|
||||
MessageBox.Show(this,
|
||||
$"Could not read profile \"{selected}\". Treating as blank template.",
|
||||
$"Could not read profile \"{selected}\". Starting a new profile instead.",
|
||||
"RemSound", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
SelectedTitle = null;
|
||||
}
|
||||
@@ -222,8 +234,10 @@ internal sealed class ProfileSelectionDialog : Form
|
||||
|
||||
private void DeleteSelected()
|
||||
{
|
||||
var selected = GetSelectedTitle();
|
||||
if (string.IsNullOrEmpty(selected) || selected == BlankTemplateLabel) return;
|
||||
var item = listBox.SelectedItem;
|
||||
if (item is null or NewProfileMarker) return; // can't delete the "New profile" entry
|
||||
var selected = TitleOfItem(item);
|
||||
if (string.IsNullOrEmpty(selected)) return;
|
||||
var result = MessageBox.Show(this,
|
||||
$"Delete profile \"{selected}\"? This cannot be undone.",
|
||||
"Confirm delete", MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button2);
|
||||
|
||||
@@ -235,7 +235,14 @@ internal static class Program
|
||||
// (e.g. legacy switch-by-title flows that pre-date the path tracking).
|
||||
nextPath = form.NextProfilePathToLoad;
|
||||
var nextTitle = form.NextProfileTitleToLoad;
|
||||
if (!string.IsNullOrEmpty(nextPath))
|
||||
if (form.LoadBlankTemplateNext)
|
||||
{
|
||||
// File → New profile: rebuild on a fresh blank template, no saved profile.
|
||||
profile = Profile.NewBlank();
|
||||
title = null;
|
||||
nextPath = null;
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(nextPath))
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
tag_name on the latest GitHub release; bump it on every public release. The
|
||||
AssemblyVersion / FileVersion default to this value, and Assembly.GetName().Version
|
||||
is what the About dialog and the updater both read. -->
|
||||
<Version>3.7.0</Version>
|
||||
<Version>3.8.0</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
Reference in New Issue
Block a user