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.
127 lines
4.6 KiB
C#
127 lines
4.6 KiB
C#
using System;
|
|
using Microsoft.UI;
|
|
using Microsoft.UI.Xaml;
|
|
using Windows.UI;
|
|
using Windows.UI.ViewManagement;
|
|
|
|
namespace TeamsISO.App.WinUI.Services;
|
|
|
|
/// <summary>
|
|
/// Owns the active theme for the WinUI 3 host. Three preferences:
|
|
/// <c>System</c> follows the Windows app-mode setting (default for new
|
|
/// users); <c>Dark</c> and <c>Light</c> pin one regardless of the OS choice.
|
|
/// The persistence path will land alongside the existing UIPreferences in
|
|
/// the next commit — for now state lives in-process.
|
|
///
|
|
/// All public mutations push <see cref="Themed"/> to subscribers so the
|
|
/// host (MainWindow) can update the AppWindow title-bar button colors
|
|
/// (system buttons aren't part of the visual tree and need a separate
|
|
/// poke when ElementTheme changes).
|
|
/// </summary>
|
|
public sealed class ThemeManager
|
|
{
|
|
public static ThemeManager Current { get; } = new();
|
|
|
|
private 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;
|
|
private string _preference = "System";
|
|
|
|
public string Preference => _preference;
|
|
|
|
public event EventHandler<ElementTheme>? Themed;
|
|
|
|
/// <summary>
|
|
/// Resolve the preference to an absolute <see cref="ElementTheme"/>
|
|
/// suitable for <see cref="FrameworkElement.RequestedTheme"/>.
|
|
/// <c>System</c> resolves to the OS app-mode.
|
|
/// </summary>
|
|
public ElementTheme ResolveTheme() => _preference switch
|
|
{
|
|
"Dark" => ElementTheme.Dark,
|
|
"Light" => ElementTheme.Light,
|
|
_ => IsSystemDark() ? ElementTheme.Dark : ElementTheme.Light,
|
|
};
|
|
|
|
public bool PreferenceMatches(string value) => string.Equals(_preference, value, StringComparison.Ordinal);
|
|
|
|
/// <summary>
|
|
/// Cycle dark ↔ light from the title-bar toggle. If the current
|
|
/// preference is <c>System</c>, the cycle pins to the opposite of the
|
|
/// currently-resolved theme so the click has a visible effect.
|
|
/// </summary>
|
|
public ElementTheme Toggle()
|
|
{
|
|
var current = ResolveTheme();
|
|
Set(current == ElementTheme.Dark ? "Light" : "Dark");
|
|
return ResolveTheme();
|
|
}
|
|
|
|
/// <summary>Set the preference, persist to disk, broadcast the resolved theme.</summary>
|
|
public void Set(string preference)
|
|
{
|
|
if (preference != "System" && preference != "Dark" && preference != "Light")
|
|
{
|
|
throw new ArgumentException("Preference must be System, Dark, or Light.", nameof(preference));
|
|
}
|
|
|
|
_preference = preference;
|
|
try { UIPreferences.SetTheme(preference); }
|
|
catch { /* persistence is best-effort */ }
|
|
Themed?.Invoke(this, ResolveTheme());
|
|
}
|
|
|
|
private bool IsSystemDark()
|
|
{
|
|
// UISettings.GetColorValue(UIColorType.Background) returns
|
|
// black-ish in dark mode, white-ish in light mode — the most
|
|
// reliable cross-version check for app mode on desktop WinUI 3.
|
|
var bg = _uiSettings.GetColorValue(UIColorType.Background);
|
|
return ((5 * bg.G) + (2 * bg.R) + bg.B) < 8 * 128;
|
|
}
|
|
|
|
private void OnSystemColorsChanged(UISettings sender, object args)
|
|
{
|
|
// Only re-broadcast if the operator hasn't pinned a preference —
|
|
// otherwise the explicit choice wins regardless of what the OS does.
|
|
if (_preference == "System")
|
|
{
|
|
Themed?.Invoke(this, ResolveTheme());
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Compute the AppWindow title-bar foreground for the given resolved
|
|
/// theme so the system min/max/close buttons stay readable.
|
|
/// </summary>
|
|
public static Color TitleBarForegroundFor(ElementTheme theme) =>
|
|
theme == ElementTheme.Dark
|
|
? Color.FromArgb(0xFF, 0xF4, 0xF4, 0xF6)
|
|
: Color.FromArgb(0xFF, 0x0A, 0x0A, 0x0A);
|
|
|
|
public static Color TitleBarHoverBgFor(ElementTheme theme) =>
|
|
theme == ElementTheme.Dark
|
|
? Color.FromArgb(0xFF, 0x33, 0x34, 0x3A)
|
|
: Color.FromArgb(0xFF, 0xEC, 0xEE, 0xF1);
|
|
}
|