using System;
using System.Linq;
using System.Windows;
using Microsoft.Win32;
namespace TeamsISO.App.Services;
///
/// Owns the active theme for the WPF host. Three preferences:
///
/// - System — follows the Windows app-mode setting (default for new
/// users; re-reads on ).
/// - Dark — pin dark regardless of OS.
/// - Light — pin light regardless of OS.
///
/// The two color files (Theme.Dark.xaml + Theme.Light.xaml) are
/// kept in lockstep on the same set of brush keys; this manager swaps the
/// MergedDictionaries entry at runtime. Styles + control templates in
/// WildDragonTheme.xaml reach the brushes via ,
/// so the visual tree re-resolves without an app restart.
///
/// Preference is persisted via 's Theme field,
/// reserved on disk during the v1 → v2 rollout so the rebuild doesn't lose the
/// operator's choice.
///
public sealed class ThemeManager
{
public static ThemeManager Current { get; } = new();
private const string DarkUri = "/Themes/Theme.Dark.xaml";
private const string LightUri = "/Themes/Theme.Light.xaml";
private const string PreferenceKeySystem = "System";
private const string PreferenceKeyDark = "Dark";
private const string PreferenceKeyLight = "Light";
private ThemeManager()
{
// Hydrate preference from disk on first access. UIPreferences.Load()
// is best-effort — disk failures fall back to defaults so the app
// always boots into a deterministic theme.
try
{
var prefs = UIPreferences.Load();
if (IsValidPreference(prefs.Theme))
{
_preference = prefs.Theme;
}
}
catch
{
// Defensive — singleton ctor must not throw or the app loses theming.
}
// Re-evaluate when Windows app-mode flips, but only when the
// operator hasn't pinned a preference. The explicit choice wins.
SystemEvents.UserPreferenceChanged += OnSystemPreferenceChanged;
}
private string _preference = PreferenceKeySystem;
/// Current preference. One of "System", "Dark", "Light".
public string Preference => _preference;
/// Fires after a theme swap with the resolved (absolute) theme.
public event EventHandler? Themed;
///
/// Resolve the preference to an absolute theme name ("Dark" or "Light")
/// suitable for the dictionary lookup. "System" resolves to the OS
/// app-mode at the time of the call.
///
public string ResolveTheme() => _preference switch
{
PreferenceKeyDark => PreferenceKeyDark,
PreferenceKeyLight => PreferenceKeyLight,
_ => IsSystemDark() ? PreferenceKeyDark : PreferenceKeyLight,
};
///
/// Set the operator's preference, persist, and apply the resolved theme.
///
public void Set(string preference)
{
if (!IsValidPreference(preference))
{
throw new ArgumentException(
"Preference must be 'System', 'Dark', or 'Light'.",
nameof(preference));
}
_preference = preference;
try { UIPreferences.SetTheme(preference); }
catch { /* persistence is best-effort */ }
Apply();
}
///
/// Cycle the theme between Dark and Light (one-click toggle from the header
/// theme icon). If the current preference is "System", the cycle pins to
/// the OPPOSITE of the currently-resolved theme so the click has a
/// visible effect.
///
public void Toggle()
{
var current = ResolveTheme();
Set(current == PreferenceKeyDark ? PreferenceKeyLight : PreferenceKeyDark);
}
///
/// Apply the current resolved theme. Should be called once during app
/// startup (after Application.Current.Resources is initialized) and
/// whenever changes — already
/// does the latter for you.
///
public void Apply()
{
var theme = ResolveTheme();
var uri = theme == PreferenceKeyDark ? DarkUri : LightUri;
SwapColorDictionary(uri);
Themed?.Invoke(this, theme);
}
private static void SwapColorDictionary(string newUri)
{
var app = Application.Current;
if (app is null) return;
var dicts = app.Resources.MergedDictionaries;
// Find the existing theme color dictionary by source URI. We
// distinguish "color" dictionaries from "WildDragonTheme" by name —
// the color files are at Theme.Dark.xaml / Theme.Light.xaml; the
// styles file is at WildDragonTheme.xaml. Replace in place to
// preserve merge order so DynamicResource refs resolve to the new
// brushes.
ResourceDictionary? old = null;
for (var i = 0; i < dicts.Count; i++)
{
var src = dicts[i].Source?.OriginalString ?? string.Empty;
if (src.EndsWith("Theme.Dark.xaml", StringComparison.OrdinalIgnoreCase) ||
src.EndsWith("Theme.Light.xaml", StringComparison.OrdinalIgnoreCase))
{
old = dicts[i];
break;
}
}
var fresh = new ResourceDictionary { Source = new Uri(newUri, UriKind.Relative) };
if (old is null)
{
dicts.Insert(0, fresh);
}
else
{
var idx = dicts.IndexOf(old);
dicts.RemoveAt(idx);
dicts.Insert(idx, fresh);
}
}
///
/// Read Windows' AppsUseLightTheme registry value. 1 = light, 0 = dark.
/// Returns true (dark) on any read failure — the dark scene is the
/// default per DESIGN.md so a missing value still lands somewhere sensible.
///
private static bool IsSystemDark()
{
try
{
using var key = Registry.CurrentUser.OpenSubKey(
@"SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize");
if (key?.GetValue("AppsUseLightTheme") is int value)
{
return value == 0;
}
}
catch
{
// Registry access can fail under unusual security contexts.
}
return true;
}
private void OnSystemPreferenceChanged(object? sender, UserPreferenceChangedEventArgs e)
{
if (e.Category != UserPreferenceCategory.General) return;
if (_preference != PreferenceKeySystem) return;
// Marshal to the UI thread — registry events fire on a system pool
// thread and resource dictionary mutations require dispatcher access.
Application.Current?.Dispatcher.BeginInvoke(new Action(Apply));
}
private static bool IsValidPreference(string? value) =>
value is PreferenceKeySystem or PreferenceKeyDark or PreferenceKeyLight;
}