feat(winui3): persist theme preference to UIPreferences
Some checks failed
CI / build-and-test (push) Failing after 26s

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.
This commit is contained in:
Zac Gaetano 2026-05-13 21:35:31 -04:00
parent 7c269f2c40
commit f7249c31c2
2 changed files with 105 additions and 3 deletions

View file

@ -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();
}
/// <summary>Set the preference and broadcast the resolved theme.</summary>
/// <summary>Set the preference, persist to disk, broadcast the resolved theme.</summary>
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());
}

View file

@ -0,0 +1,85 @@
using System;
using System.IO;
using System.Text.Json;
namespace TeamsISO.App.WinUI.Services;
/// <summary>
/// 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.
/// </summary>
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 }
/// <summary>The on-disk shape. New fields added here become opt-in for older files via default values.</summary>
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<Prefs>(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.
}
}
/// <summary>Update just the Theme field without touching other prefs.</summary>
public static void SetTheme(string theme)
{
var current = Load();
Save(current with { Theme = theme });
}
}