From f7249c31c2fb2ccbfb53cde5b7b40635809c340a Mon Sep 17 00:00:00 2001 From: Zac Gaetano Date: Wed, 13 May 2026 21:35:31 -0400 Subject: [PATCH] feat(winui3): persist theme preference to UIPreferences MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Services/UIPreferences.cs — mirror of the WPF host's UIPreferences, sharing %LOCALAPPDATA%\TeamsISO\ui-prefs.json on disk. Adds a Theme field ("System" / "Dark" / "Light") that the WPF host's UIPreferences will pick up when its theme system lands (JSON deserialization is forward-compatible — extra fields are ignored, missing fields fall back to defaults). ThemeManager hydration: * Constructor reads UIPreferences.Theme on first .Current access. * Defaults to "System" when the file is missing, the value is invalid, or load throws (defensive — ThemeManager.Current is a static singleton, a throw would break theme resolution app-wide). ThemeManager.Set persistence: * Calls UIPreferences.SetTheme(preference) which does a read-modify- write of the JSON (so other fields aren't trampled). * Persistence is best-effort wrapped in try/catch — disk full, permission denied, etc. fall through and the in-memory state still holds for the session. End-to-end now: title-bar sun/moon toggle → ThemeManager.Toggle → .Set("Dark"/"Light") → JSON write → next launch reads the preference and applies before the first frame. Operator's theme choice survives across launches and across host swaps once the WPF host learns the field. --- .../Services/ThemeManager.cs | 23 ++++- .../Services/UIPreferences.cs | 85 +++++++++++++++++++ 2 files changed, 105 insertions(+), 3 deletions(-) create mode 100644 src/TeamsISO.App.WinUI/Services/UIPreferences.cs diff --git a/src/TeamsISO.App.WinUI/Services/ThemeManager.cs b/src/TeamsISO.App.WinUI/Services/ThemeManager.cs index 1553263..5e4880d 100644 --- a/src/TeamsISO.App.WinUI/Services/ThemeManager.cs +++ b/src/TeamsISO.App.WinUI/Services/ThemeManager.cs @@ -26,11 +26,26 @@ public sealed class ThemeManager { _uiSettings = new UISettings(); _uiSettings.ColorValuesChanged += OnSystemColorsChanged; + + // Hydrate the preference from disk so the operator's choice + // survives across launches. Defaults to "System" if the prefs + // file is missing or unreadable (Load() catches its own errors). + try + { + var prefs = UIPreferences.Load(); + if (prefs.Theme == "System" || prefs.Theme == "Dark" || prefs.Theme == "Light") + { + _preference = prefs.Theme; + } + } + catch + { + // Defensive — ThemeManager.Current is a static singleton; a + // throw here would prevent the app from getting any theme. + } } private readonly UISettings _uiSettings; - // Default: System (follow OS app-mode). Override at runtime via Set(); - // persistence to UIPreferences.Theme lands in the view-model commit. private string _preference = "System"; public string Preference => _preference; @@ -63,7 +78,7 @@ public sealed class ThemeManager return ResolveTheme(); } - /// Set the preference and broadcast the resolved theme. + /// Set the preference, persist to disk, broadcast the resolved theme. public void Set(string preference) { if (preference != "System" && preference != "Dark" && preference != "Light") @@ -72,6 +87,8 @@ public sealed class ThemeManager } _preference = preference; + try { UIPreferences.SetTheme(preference); } + catch { /* persistence is best-effort */ } Themed?.Invoke(this, ResolveTheme()); } diff --git a/src/TeamsISO.App.WinUI/Services/UIPreferences.cs b/src/TeamsISO.App.WinUI/Services/UIPreferences.cs new file mode 100644 index 0000000..da5a717 --- /dev/null +++ b/src/TeamsISO.App.WinUI/Services/UIPreferences.cs @@ -0,0 +1,85 @@ +using System; +using System.IO; +using System.Text.Json; + +namespace TeamsISO.App.WinUI.Services; + +/// +/// Persistent UI-side toggles shared between hosts via JSON at +/// %LOCALAPPDATA%\TeamsISO\ui-prefs.json. The WPF host's copy at +/// src/TeamsISO.App/Services/UIPreferences.cs reads/writes the same +/// file with the same record shape — new fields added in one host's +/// copy degrade gracefully in the other (JSON deserializer ignores +/// unknown fields, missing fields fall back to defaults). +/// +/// The WinUI copy adds the Theme field which the WPF host doesn't +/// know about yet; when the WPF host's UIPreferences gets the same +/// field, both hosts will share the operator's theme choice across +/// host swaps. +/// +public static class UIPreferences +{ + private static readonly object _gate = new(); + + private static string PrefsPath => + Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "TeamsISO", "ui-prefs.json"); + + public enum SortMode { JoinOrder, Alphabetical, OnlineFirst, LoudestFirst } + + /// The on-disk shape. New fields added here become opt-in for older files via default values. + public sealed record Prefs( + bool HideLocalSelf = true, + bool AutoDisableOnDeparture = false, + SortMode ParticipantSort = SortMode.JoinOrder, + bool MinimizeToTray = false, + bool ControlSurfaceLanReachable = false, + bool LaunchTeamsOnStartup = false, + bool AutoHideTeamsWindows = false, + bool AutoRecordOnCall = false, + bool EmbedTeamsWindow = false, + // Theme preference: "System" (follow OS app-mode), "Dark", or + // "Light". WinUI host owns the value for now; WPF host gets the + // same field in its UIPreferences when its theme system lands. + string Theme = "System"); + + public static Prefs Load() + { + try + { + if (!File.Exists(PrefsPath)) return new Prefs(); + var json = File.ReadAllText(PrefsPath); + return JsonSerializer.Deserialize(json) ?? new Prefs(); + } + catch + { + return new Prefs(); + } + } + + public static void Save(Prefs prefs) + { + try + { + lock (_gate) + { + var dir = Path.GetDirectoryName(PrefsPath); + if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir); + var json = JsonSerializer.Serialize(prefs, new JsonSerializerOptions { WriteIndented = true }); + File.WriteAllText(PrefsPath, json); + } + } + catch + { + // Disk full / permission denied — in-memory state still holds for this session. + } + } + + /// Update just the Theme field without touching other prefs. + public static void SetTheme(string theme) + { + var current = Load(); + Save(current with { Theme = theme }); + } +}