chore: release v2.4
- Add theme system (Dark/Light palettes, ModernTheme, ThemeManager) - Add InputDialog, Spinner common view - Add DuplicatesCsvExportService - Refresh views, dialogs, and view models across tabs - Update localization strings (en/fr) - Tweak services (transfer, permissions, search, user access, ownership elevation, bulk operations) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
using System.Windows;
|
||||
using Microsoft.Win32;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace SharepointToolbox.Services;
|
||||
|
||||
public enum ThemeMode { System, Light, Dark }
|
||||
|
||||
/// <summary>
|
||||
/// Swaps the merged palette dictionary at runtime so all DynamicResource brush lookups retint live.
|
||||
/// "System" mode reads HKCU AppsUseLightTheme (0 = dark, 1 = light) and subscribes to system theme changes.
|
||||
/// </summary>
|
||||
public class ThemeManager
|
||||
{
|
||||
private const string LightPaletteSource = "pack://application:,,,/Themes/LightPalette.xaml";
|
||||
private const string DarkPaletteSource = "pack://application:,,,/Themes/DarkPalette.xaml";
|
||||
private const string PersonalizeKey = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize";
|
||||
|
||||
private readonly ILogger<ThemeManager> _logger;
|
||||
private ThemeMode _mode = ThemeMode.System;
|
||||
private bool _systemSubscribed;
|
||||
|
||||
public event EventHandler? ThemeChanged;
|
||||
|
||||
public ThemeMode Mode => _mode;
|
||||
public bool IsDarkActive { get; private set; }
|
||||
|
||||
public ThemeManager(ILogger<ThemeManager> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public void ApplyMode(ThemeMode mode)
|
||||
{
|
||||
_mode = mode;
|
||||
bool dark = ResolveDark(mode);
|
||||
ApplyPalette(dark);
|
||||
EnsureSystemSubscription(mode);
|
||||
}
|
||||
|
||||
public void ApplyFromString(string? value)
|
||||
{
|
||||
var mode = (value ?? "System") switch
|
||||
{
|
||||
"Light" => ThemeMode.Light,
|
||||
"Dark" => ThemeMode.Dark,
|
||||
_ => ThemeMode.System,
|
||||
};
|
||||
ApplyMode(mode);
|
||||
}
|
||||
|
||||
private bool ResolveDark(ThemeMode mode) => mode switch
|
||||
{
|
||||
ThemeMode.Light => false,
|
||||
ThemeMode.Dark => true,
|
||||
_ => ReadSystemPrefersDark(),
|
||||
};
|
||||
|
||||
private bool ReadSystemPrefersDark()
|
||||
{
|
||||
try
|
||||
{
|
||||
using var key = Registry.CurrentUser.OpenSubKey(PersonalizeKey);
|
||||
if (key?.GetValue("AppsUseLightTheme") is int v)
|
||||
return v == 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to read system theme preference, defaulting to light");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void ApplyPalette(bool dark)
|
||||
{
|
||||
var app = Application.Current;
|
||||
if (app is null) return;
|
||||
|
||||
var newPalette = new ResourceDictionary
|
||||
{
|
||||
Source = new Uri(dark ? DarkPaletteSource : LightPaletteSource, UriKind.Absolute)
|
||||
};
|
||||
|
||||
var dicts = app.Resources.MergedDictionaries;
|
||||
int replaced = -1;
|
||||
for (int i = 0; i < dicts.Count; i++)
|
||||
{
|
||||
var src = dicts[i].Source?.OriginalString ?? string.Empty;
|
||||
if (src.EndsWith("LightPalette.xaml", StringComparison.OrdinalIgnoreCase) ||
|
||||
src.EndsWith("DarkPalette.xaml", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
replaced = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (replaced >= 0)
|
||||
dicts[replaced] = newPalette;
|
||||
else
|
||||
dicts.Insert(0, newPalette);
|
||||
|
||||
IsDarkActive = dark;
|
||||
ThemeChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private void EnsureSystemSubscription(ThemeMode mode)
|
||||
{
|
||||
if (mode == ThemeMode.System && !_systemSubscribed)
|
||||
{
|
||||
SystemEvents.UserPreferenceChanged += OnUserPreferenceChanged;
|
||||
_systemSubscribed = true;
|
||||
}
|
||||
else if (mode != ThemeMode.System && _systemSubscribed)
|
||||
{
|
||||
SystemEvents.UserPreferenceChanged -= OnUserPreferenceChanged;
|
||||
_systemSubscribed = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnUserPreferenceChanged(object? sender, UserPreferenceChangedEventArgs e)
|
||||
{
|
||||
if (e.Category != UserPreferenceCategory.General) return;
|
||||
if (_mode != ThemeMode.System) return;
|
||||
|
||||
var app = Application.Current;
|
||||
if (app is null) return;
|
||||
|
||||
app.Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
bool dark = ReadSystemPrefersDark();
|
||||
if (dark != IsDarkActive)
|
||||
ApplyPalette(dark);
|
||||
}));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user