Add "auto save non-read only profiles" preference (silent)
New global setting in Preferences -> General, right after "Browse for profiles folder": a list "Auto-save non-read-only profiles" with Never / Every 2 / 5 / 10 / 15 / 20 / 30 minutes. When enabled, RemSound periodically saves the current profile, but ONLY if it is a real saved profile, is NOT read-only, and has unsaved changes -- and it saves SILENTLY (no save cue, no confirmation), so it never interrupts the user. - AppConfig.AutoSaveNonReadOnlyMinutes (machine-wide, 0 = Never = default). - SaveProfileTo gains a playCue flag; auto-save passes playCue: false. - MainForm autoSaveTimer + ApplyAutoSaveTimer + ShouldAutoSave guard, re-applied live when the setting changes in Preferences. - New self-test "Auto-save non-read-only profiles": options list, AppConfig persistence, the guard (read-only / blank / unchanged all skipped), and the timer turning on/off. Gate 28/28. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
71fb065233
commit
26903950f7
@@ -481,6 +481,11 @@ public sealed class MainForm : Form
|
||||
private readonly Dictionary<CheckedListBox, int> lastFocusedListIndices = [];
|
||||
|
||||
private readonly System.Windows.Forms.Timer statusTimer = new() { Interval = 1000 };
|
||||
// Periodic silent auto-save of the current profile (Preferences → General → "auto save non-read only
|
||||
// profiles"). Off by default; when enabled it fires every N minutes and saves the active profile only
|
||||
// if it's a real saved profile, NOT read-only, and has unsaved changes — WITHOUT the save cue. Interval
|
||||
// and enable/disable come from AppConfig.AutoSaveNonReadOnlyMinutes via ApplyAutoSaveTimer().
|
||||
private readonly System.Windows.Forms.Timer autoSaveTimer = new();
|
||||
// Device-list refresh. As of v3.4 this is EVENT-DRIVEN, not polled: an
|
||||
// AudioDeviceChangeNotifier registers for Windows audio endpoint-change notifications and
|
||||
// pokes this timer when the device set actually changes (USB hot-plug / unplug, default-device
|
||||
@@ -1398,6 +1403,7 @@ public sealed class MainForm : Form
|
||||
// message-only window handle to be freed at GC finalization. MainForm is rebuilt on every
|
||||
// profile switch, so disposing here releases those handles deterministically each time.
|
||||
statusTimer.Stop(); statusTimer.Dispose();
|
||||
autoSaveTimer.Stop(); autoSaveTimer.Dispose();
|
||||
deviceRefreshTimer.Stop(); deviceRefreshTimer.Dispose();
|
||||
continuousTuneTimer.Stop(); continuousTuneTimer.Dispose();
|
||||
updateCheckTimer.Stop(); updateCheckTimer.Dispose();
|
||||
@@ -1584,6 +1590,12 @@ public sealed class MainForm : Form
|
||||
|
||||
statusTimer.Start();
|
||||
|
||||
// Silent periodic auto-save (opt-in, off by default). Event wiring here; the interval and whether
|
||||
// it runs at all are set by ApplyAutoSaveTimer() from the saved preference, and re-applied live
|
||||
// when the user changes it in Preferences.
|
||||
autoSaveTimer.Tick += (_, _) => AutoSaveCurrentProfileIfDue();
|
||||
ApplyAutoSaveTimer();
|
||||
|
||||
// Hot-plug detection is event-driven (see the deviceRefreshTimer comment): register for
|
||||
// Windows audio endpoint-change notifications and refresh the device lists only when the
|
||||
// device set actually changes. If that registration fails, fall back to the pre-v3.4
|
||||
@@ -2835,6 +2847,7 @@ public sealed class MainForm : Form
|
||||
},
|
||||
checkForUpdatesNow: () => CheckForUpdatesManually(),
|
||||
onUpdateFrequencyChanged: ApplyUpdateCheckTimer,
|
||||
onAutoSaveIntervalChanged: ApplyAutoSaveTimer,
|
||||
applyUpnpEnabled: enabled =>
|
||||
{
|
||||
// The persist already happened in the dialog; this callback only flips the
|
||||
@@ -8228,7 +8241,9 @@ public sealed class MainForm : Form
|
||||
/// title, refreshes button visibility, and shows a confirmation popup).</summary>
|
||||
private void SaveProfileTo(string title) => SaveProfileTo(title, showConfirmation: true);
|
||||
|
||||
private void SaveProfileTo(string title, bool showConfirmation)
|
||||
private void SaveProfileTo(string title, bool showConfirmation) => SaveProfileTo(title, showConfirmation, playCue: true);
|
||||
|
||||
private void SaveProfileTo(string title, bool showConfirmation, bool playCue)
|
||||
{
|
||||
if (profileStore is null) return;
|
||||
try
|
||||
@@ -8238,8 +8253,9 @@ public sealed class MainForm : Form
|
||||
// Save cue (2026-05-28): fires after any successful save — Save AND Save As, since
|
||||
// both routes funnel through this single method. Honours the EnableSaveCue per-
|
||||
// profile flag; the cue is silent if the user has unticked it in Preferences or if
|
||||
// sounds\save.wav doesn't exist and no custom override has been set.
|
||||
if (settings.LoadEnableSaveCue()) saveSound?.Play();
|
||||
// sounds\save.wav doesn't exist and no custom override has been set. Auto-save passes
|
||||
// playCue: false so it never interrupts the user with a save sound.
|
||||
if (playCue && settings.LoadEnableSaveCue()) saveSound?.Play();
|
||||
unsavedChanges = false;
|
||||
if (showConfirmation && !AppConfig.Load().SaveProfileConfirmationSuppressed)
|
||||
{
|
||||
@@ -8260,6 +8276,41 @@ public sealed class MainForm : Form
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Applies the "auto save non-read only profiles" preference to the timer: stops it when the
|
||||
/// setting is Never (0 minutes), otherwise sets the interval and starts it. Called at launch and again
|
||||
/// whenever the user changes the setting in Preferences, so the change takes effect immediately.</summary>
|
||||
internal void ApplyAutoSaveTimer() => ApplyAutoSaveTimer(AppConfig.Load().AutoSaveNonReadOnlyMinutes);
|
||||
|
||||
/// <summary>Overload taking the interval directly, so a self-test can drive it without touching the
|
||||
/// real config.</summary>
|
||||
internal void ApplyAutoSaveTimer(int minutes)
|
||||
{
|
||||
autoSaveTimer.Stop();
|
||||
if (minutes <= 0) return; // Never
|
||||
autoSaveTimer.Interval = minutes * 60 * 1000;
|
||||
autoSaveTimer.Start();
|
||||
}
|
||||
|
||||
/// <summary>Timer tick: silently save the active profile, but ONLY if it's a real saved profile, is NOT
|
||||
/// read-only, and actually has unsaved changes. Uses the shared save path with the cue and the
|
||||
/// confirmation dialog suppressed, so it's completely unobtrusive — no sound, no popup.</summary>
|
||||
private void AutoSaveCurrentProfileIfDue()
|
||||
{
|
||||
if (!ShouldAutoSave(profileStore is not null, currentProfileTitle, currentProfileReadOnly, unsavedChanges)) return;
|
||||
SaveProfileTo(currentProfileTitle!, showConfirmation: false, playCue: false);
|
||||
}
|
||||
|
||||
/// <summary>Pure guard for the periodic auto-save (unit-testable). Only a real, saved profile that is
|
||||
/// NOT read-only and has unsaved changes may be auto-saved — a blank template, a read-only profile, or
|
||||
/// an unchanged one is left alone.</summary>
|
||||
internal static bool ShouldAutoSave(bool hasStore, string? currentTitle, bool readOnly, bool dirty)
|
||||
=> hasStore && !string.IsNullOrEmpty(currentTitle) && !readOnly && dirty;
|
||||
|
||||
// Test seams for the auto-save timer (headless): confirm ApplyAutoSaveTimer turns it on/off and sets
|
||||
// the interval from AppConfig.AutoSaveNonReadOnlyMinutes.
|
||||
internal bool AutoSaveTimerEnabledForTest => autoSaveTimer.Enabled;
|
||||
internal int AutoSaveTimerIntervalForTest => autoSaveTimer.Interval;
|
||||
|
||||
/// <summary>Builds a Profile from the current control state and writes it via the store.
|
||||
/// Doesn't touch UI feedback — that's the caller's job. Throws on store failure.</summary>
|
||||
private void SaveCurrentStateToProfileFile(string title)
|
||||
|
||||
Reference in New Issue
Block a user