using System.Text.Json; namespace RemSound.Core; /// How often the self-updater polls GitHub Releases for a newer build. Values are /// stable: don't reorder; deserialisation reads the underlying int from remsound.config.json. public enum UpdateCheckFrequency { Never = 0, EveryHour = 1, Every6Hours = 2, Every24Hours = 3, } /// /// App-level configuration that lives next to the exe as remsound.config.json. /// Distinct from : profiles are user-chosen sets of audio / /// connectivity / device settings; the app config is the *meta* layer that holds /// preferences that should be sticky regardless of which profile is loaded. Profiles are /// per-setup; this file is per-installation. /// /// What lives here: /// * — where the profile JSONs are read from. /// /// (Pre-2026-05-11 also held BothModeWarningSuppressed — the "do not show me again" /// tick on the WASAPI+ASIO latency popup. The popup was retired along with the audio-mode /// listbox; old config JSONs that still contain the key just have it ignored.) /// /// Persisted location: <exe>\remsound.config.json. If the file is missing or /// malformed, defaults are used and the app behaves exactly as it did pre-2026-05-05 /// (per-machine subfolder under the exe). The file is only written when the user /// explicitly changes a setting. /// public sealed class AppConfig { /// Filesystem path to the directory the app should read profiles from. When /// null, RemSound uses the legacy default: <exe>\profiles\<machine>\. /// When set to an explicit folder, that folder IS the profiles folder — no per-machine /// subfolder is appended (the user picked it, they meant it; that also lets a user point /// at a Dropbox folder shared between machines). public string? ProfilesDirectory { get; set; } /// True if the user has ticked "do not show me this message again" on the /// confirmation popup that fires when Save (Ctrl+S / File → Save) successfully /// overwrites the currently-loaded profile. Lives here (not in Profile) so the /// preference sticks across profile switches — once you've decided you don't need /// the "Profile saved" nag, you don't expect it to come back when you load a /// different profile. The Save-As path doesn't use this flag: the Save-As dialog /// itself is the user-visible confirmation, so a follow-up popup is redundant. public bool SaveProfileConfirmationSuppressed { get; set; } /// If true, RemSound minimises to the system tray immediately after the main /// window finishes loading. Lets the user "boot up the machine and have RemSound /// already running quietly". Default false. public bool StartMinimised { get; set; } /// If true, RemSound writes a tab-separated diagnostic log to /// <exe>\logs\. Lives here (not in ) because logging /// is a debugging affordance for the installation, not a user-facing audio preference — /// switching profiles shouldn't accidentally re-enable a flood of writes the user had /// turned off, and a one-machine "yes log everything" decision shouldn't have to ride /// along on every saved profile. Default false: no log file is created until the user /// ticks Enable logs in the Preferences dialog. public bool LoggingEnabled { get; set; } /// If non-null and a profile with this title exists, RemSound skips the /// startup profile picker and loads this profile directly. Combine with /// + the Windows auto-start registry entry /// (see StartupAutoStart) to get a fully unattended boot-into-streaming flow. /// To re-show the picker temporarily, untick "Start with a specific profile" in the /// Startup behaviour dialog. Null = always show the picker (legacy behaviour). public string? StartWithProfileTitle { get; set; } /// How often RemSound polls the GitHub Releases API for a newer build. Default /// . Set to /// to disable background checks entirely (the user can still trigger a manual check via /// the Preferences button or the Help menu). public UpdateCheckFrequency UpdateCheckFrequency { get; set; } = UpdateCheckFrequency.Every24Hours; /// If true, RemSound downloads and applies a new release without prompting: /// the running instance writes the new files to a staging folder, spawns a small /// detached helper that waits for the exe to exit, swaps in the new files, and restarts /// RemSound. Default false — the user gets a confirmation dialog before each install. public bool SilentlyInstallUpdates { get; set; } /// UTC timestamp of the last successful update check. Used by the background /// update timer to space out polls across launches — if you set the frequency to /// "every 24 hours" and re-launch the app three times that day, it still hits the API /// only once. Null on a fresh install. public DateTime? LastUpdateCheckUtc { get; set; } /// Most-recently-opened profile paths, newest first, capped at /// . Populated by every /// time a profile is loaded, surfaced in the File → Recent profiles submenu. Stored as /// full paths so profiles saved outside the canonical profiles folder are also /// reachable (Save-As to an arbitrary path stays in the recents list). public List RecentProfiles { get; set; } = new(); /// Cap on how many entries we keep in . Five is the /// most that fits comfortably as 1–5 single-digit mnemonics inside a submenu without /// the user needing to read the names to remember which row they want. public const int MaxRecentProfiles = 5; /// Push a profile path to the front of the recents list. Removes any existing /// entry that matches (case-insensitive) so a recently re-opened profile rises to the /// top instead of being duplicated. Caps the list at . /// Caller must after mutating. public void NoteRecentProfile(string? path) { if (string.IsNullOrWhiteSpace(path)) return; RecentProfiles.RemoveAll(p => string.Equals(p, path, StringComparison.OrdinalIgnoreCase)); RecentProfiles.Insert(0, path); while (RecentProfiles.Count > MaxRecentProfiles) { RecentProfiles.RemoveAt(RecentProfiles.Count - 1); } } private static string ConfigPath => Path.Combine(AppContext.BaseDirectory, "remsound.config.json"); /// Reads the app config from disk. Always returns a non-null instance — a missing /// or malformed file becomes a defaults-only AppConfig rather than throwing. public static AppConfig Load() { try { if (!File.Exists(ConfigPath)) return new AppConfig(); var json = File.ReadAllText(ConfigPath); return JsonSerializer.Deserialize(json) ?? new AppConfig(); } catch { // Corrupt config file shouldn't keep RemSound from launching. Fall back to // defaults; the user can re-pick a folder via the dialog and we'll overwrite // the bad file on the next save. return new AppConfig(); } } /// Writes this config to disk. Throws on filesystem failures (caller should /// surface a MessageBox — failure to persist a directory choice is user-visible). public void Save() { var json = JsonSerializer.Serialize(this, new JsonSerializerOptions { WriteIndented = true }); File.WriteAllText(ConfigPath, json); } /// Convenience: build the appropriate for the /// current config. Falls back to the default store (per-machine subfolder) if the /// configured folder is missing, blank, or doesn't exist on disk. public ProfileStore CreateStore() { if (!string.IsNullOrWhiteSpace(ProfilesDirectory) && Directory.Exists(ProfilesDirectory)) { return new ProfileStore(ProfilesDirectory); } return new ProfileStore(); } }