using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using Avalonia; using Avalonia.Controls; using Avalonia.Media; using Avalonia.Styling; using Avalonia.Threading; using Avalonia.Media.Imaging; using LanMountainDesktop.Models; using LanMountainDesktop.PluginSdk; using LanMountainDesktop.Services.Settings; using LanMountainDesktop.Theme; using LibVLCSharp.Shared; using Microsoft.Win32; namespace LanMountainDesktop.Services; public enum MaterialSurfaceRole { WindowBackground = 0, SettingsWindowBackground = 1, DockBackground = 2, StatusBarBackground = 3, DesktopComponentHost = 4, StatusBarComponentHost = 5, OverlayPanel = 6 } public sealed record AppearanceMaterialSurface( Color BackgroundColor, Color BorderColor, double BlurRadius, double Opacity); public sealed record AppearanceThemeSnapshot( bool IsNightMode, string ThemeColorMode, string? UserThemeColor, string? SelectedWallpaperSeed, string ResolvedSeedSource, MonetPalette MonetPalette, Color AccentColor, Color EffectiveSeedColor, IReadOnlyList WallpaperSeedCandidates, string SystemMaterialMode, IReadOnlyList AvailableSystemMaterialModes, bool CanChangeSystemMaterial, bool UseSystemChrome, string? ResolvedWallpaperPath); public interface IAppearanceThemeService { AppearanceThemeSnapshot GetCurrent(); AppearanceThemeSnapshot BuildPreview(ThemeAppearanceSettingsState pendingState); event EventHandler? Changed; void ApplyThemeResources(IResourceDictionary resources); AppearanceMaterialSurface GetMaterialSurface(MaterialSurfaceRole role); void ApplyWindowMaterial(Window window, MaterialSurfaceRole role); } internal interface ISystemWallpaperService { bool IsSupported { get; } string? GetWallpaperPath(); } internal interface IWindowMaterialService { IReadOnlyList GetAvailableModes(); bool CanChangeMode { get; } void Apply(Window window, string materialMode); } internal interface IMaterialSurfaceService { AppearanceMaterialSurface GetSurface(ThemeColorContext context, MaterialSurfaceRole role); } internal interface IVideoWallpaperSeedExtractor { IReadOnlyList ExtractSeedCandidates(string videoPath, MonetColorService monetColorService); } internal readonly record struct WallpaperSeedSourceDescriptor( string SourceKind, string SourceKey, string? ResolvedWallpaperPath, string? FilePath, Color? SolidColor); internal sealed record WallpaperSeedExtractionResult( string SourceKind, string SourceKey, string? ResolvedWallpaperPath, IReadOnlyList SeedCandidates); internal readonly record struct WallpaperPaletteResolution( MonetPalette Palette, IReadOnlyList SeedCandidates, string ResolvedSeedSource, Color EffectiveSeedColor, string? ResolvedWallpaperPath); internal sealed class LibVlcVideoWallpaperSeedExtractor : IVideoWallpaperSeedExtractor { public IReadOnlyList ExtractSeedCandidates(string videoPath, MonetColorService monetColorService) { if (string.IsNullOrWhiteSpace(videoPath) || !File.Exists(videoPath)) { return []; } var snapshotPath = Path.Combine( Path.GetTempPath(), $"lanmountaindesktop-video-seed-{Guid.NewGuid():N}.png"); try { using var libVlc = new LibVLC("--no-audio", "--intf=dummy", "--no-video-title-show"); using var media = new Media(libVlc, new Uri(videoPath)); using var mediaPlayer = new MediaPlayer(libVlc) { Media = media }; mediaPlayer.Play(); var stopwatch = Stopwatch.StartNew(); while (stopwatch.Elapsed < TimeSpan.FromSeconds(5)) { Thread.Sleep(180); if (!mediaPlayer.TakeSnapshot(0, snapshotPath, 320, 180)) { continue; } var fileInfo = new FileInfo(snapshotPath); if (!fileInfo.Exists || fileInfo.Length <= 0) { continue; } using var bitmap = new Bitmap(snapshotPath); return monetColorService.ExtractSeedCandidates(bitmap); } } catch (Exception ex) { AppLogger.Warn( "Appearance.VideoWallpaperPalette", $"Failed to extract wallpaper seed candidates from video '{videoPath}'.", ex); } finally { try { if (File.Exists(snapshotPath)) { File.Delete(snapshotPath); } } catch { // Best effort cleanup only. } } return []; } } internal sealed class SystemWallpaperService : ISystemWallpaperService { public bool IsSupported => OperatingSystem.IsWindows(); public string? GetWallpaperPath() { if (!OperatingSystem.IsWindows()) { return null; } try { using var key = Registry.CurrentUser.OpenSubKey(@"Control Panel\Desktop", writable: false); var wallpaperPath = key?.GetValue("WallPaper") as string; return string.IsNullOrWhiteSpace(wallpaperPath) || !File.Exists(wallpaperPath) ? null : wallpaperPath; } catch (Exception ex) { AppLogger.Warn("Appearance.SystemWallpaper", "Failed to resolve the current system wallpaper path.", ex); return null; } } } internal sealed class WindowMaterialService : IWindowMaterialService { private const int Windows11Build = 22000; private const int Windows11_24H2Build = 26100; public bool CanChangeMode => GetSupportProfile() == WindowMaterialSupportProfile.FullSwitching; public IReadOnlyList GetAvailableModes() { return GetSupportProfile() switch { WindowMaterialSupportProfile.FullSwitching => [ ThemeAppearanceValues.MaterialNone, ThemeAppearanceValues.MaterialMica, ThemeAppearanceValues.MaterialAcrylic ], WindowMaterialSupportProfile.FixedMica => [ ThemeAppearanceValues.MaterialNone, ThemeAppearanceValues.MaterialMica ], WindowMaterialSupportProfile.FixedAcrylic => [ ThemeAppearanceValues.MaterialNone, ThemeAppearanceValues.MaterialAcrylic ], _ => [ ThemeAppearanceValues.MaterialNone ] }; } public void Apply(Window window, string materialMode) { ArgumentNullException.ThrowIfNull(window); var normalizedMode = ThemeAppearanceValues.NormalizeSystemMaterialMode(materialMode); if (normalizedMode == ThemeAppearanceValues.MaterialNone) { window.Background = Brushes.White; window.TransparencyLevelHint = [WindowTransparencyLevel.None]; return; } window.Background = Brushes.Transparent; if (!OperatingSystem.IsWindows() || !IsTransparencyEnabled()) { window.TransparencyLevelHint = [ WindowTransparencyLevel.None ]; return; } window.TransparencyLevelHint = normalizedMode switch { ThemeAppearanceValues.MaterialMica => [ WindowTransparencyLevel.Mica, WindowTransparencyLevel.Blur, WindowTransparencyLevel.None ], ThemeAppearanceValues.MaterialAcrylic => [ WindowTransparencyLevel.AcrylicBlur, WindowTransparencyLevel.Blur, WindowTransparencyLevel.None ], _ => [ WindowTransparencyLevel.None ] }; } private static bool IsTransparencyEnabled() { if (!OperatingSystem.IsWindows()) { return false; } try { using var key = Registry.CurrentUser.OpenSubKey( @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize", writable: false); var value = key?.GetValue("EnableTransparency"); return value switch { int intValue => intValue != 0, byte byteValue => byteValue != 0, _ => true }; } catch { return true; } } private static WindowMaterialSupportProfile GetSupportProfile() { if (!OperatingSystem.IsWindows() || !IsTransparencyEnabled()) { return WindowMaterialSupportProfile.NoneOnly; } if (OperatingSystem.IsWindowsVersionAtLeast(10, 0, Windows11_24H2Build)) { return WindowMaterialSupportProfile.FullSwitching; } if (OperatingSystem.IsWindowsVersionAtLeast(10, 0, Windows11Build)) { return WindowMaterialSupportProfile.FixedMica; } if (OperatingSystem.IsWindowsVersionAtLeast(10, 0)) { return WindowMaterialSupportProfile.FixedAcrylic; } return WindowMaterialSupportProfile.NoneOnly; } private enum WindowMaterialSupportProfile { NoneOnly = 0, FixedMica = 1, FixedAcrylic = 2, FullSwitching = 3 } } internal sealed class MaterialSurfaceService : IMaterialSurfaceService { public AppearanceMaterialSurface GetSurface(ThemeColorContext context, MaterialSurfaceRole role) { var monetPalette = context.MonetPalette; var monetColors = context.MonetColors?.Where(color => color.A > 0).ToArray() ?? []; var primary = context.UseNeutralSurfaces ? context.AccentColor : monetPalette?.Primary ?? (monetColors.Length > 0 ? monetColors[0] : context.AccentColor); var secondary = monetPalette?.Secondary ?? (monetColors.Length > 1 ? monetColors[1] : ColorMath.Blend(primary, Color.Parse("#FFFFFFFF"), 0.14)); var neutralPrimary = monetPalette?.Neutral ?? (monetColors.Length > 3 ? monetColors[3] : ResolveNeutralBase(context.IsNightMode, role)); var neutralSecondary = monetPalette?.NeutralVariant ?? (monetColors.Length > 4 ? monetColors[4] : ResolveLiftBase(context.IsNightMode, role)); var materialMode = ThemeAppearanceValues.NormalizeSystemMaterialMode(context.SystemMaterialMode); var (tintStrength, liftStrength, alpha, blurRadius) = ResolveModeParameters(materialMode, role, context.IsNightMode); var neutralBase = ResolveNeutralBase(context.IsNightMode, role); var neutralLift = ResolveLiftBase(context.IsNightMode, role); var isDockLike = role is MaterialSurfaceRole.DockBackground; var isComponentLike = role is MaterialSurfaceRole.DesktopComponentHost or MaterialSurfaceRole.StatusBarComponentHost; var baseMix = isDockLike ? 0.88 : isComponentLike ? 0.74 : 0.82; var liftMix = isDockLike ? 0.58 : isComponentLike ? 0.34 : 0.46; var neutralMix = isDockLike ? 0.22 : 0.16; var background = ColorMath.Blend(neutralBase, neutralPrimary, baseMix); background = ColorMath.Blend(background, neutralLift, liftMix); background = ColorMath.Blend(background, neutralSecondary, neutralMix); if (!context.UseNeutralSurfaces) { background = ColorMath.Blend(background, primary, tintStrength); background = ColorMath.Blend(background, secondary, liftStrength); } if (isDockLike && !context.IsNightMode) { background = ColorMath.Blend(background, Color.Parse("#FFFFFFFF"), 0.12); } background = Color.FromArgb(alpha, background.R, background.G, background.B); var borderSeed = context.IsNightMode ? ColorMath.Blend(neutralSecondary, Color.Parse("#FFFFFFFF"), 0.16) : ColorMath.Blend(neutralSecondary, Color.Parse("#FF334155"), 0.08); if (!context.UseNeutralSurfaces && !isComponentLike) { borderSeed = ColorMath.Blend(borderSeed, primary, 0.08); } var borderAlpha = role switch { MaterialSurfaceRole.DockBackground => context.IsNightMode ? (byte)0x34 : (byte)0x18, MaterialSurfaceRole.DesktopComponentHost or MaterialSurfaceRole.StatusBarComponentHost => context.IsNightMode ? (byte)0x18 : (byte)0x10, MaterialSurfaceRole.StatusBarBackground => (byte)0x00, _ => context.IsNightMode ? (byte)0x26 : (byte)0x16 }; var border = ColorMath.WithAlpha(borderSeed, borderAlpha); return new AppearanceMaterialSurface(background, border, blurRadius, 1.0); } private static (double TintStrength, double LiftStrength, byte Alpha, double BlurRadius) ResolveModeParameters( string materialMode, MaterialSurfaceRole role, bool isNightMode) { var isOverlay = role is MaterialSurfaceRole.DockBackground or MaterialSurfaceRole.StatusBarBackground or MaterialSurfaceRole.OverlayPanel; return materialMode switch { ThemeAppearanceValues.MaterialAcrylic => ( isOverlay ? 0.30 : 0.20, isOverlay ? 0.22 : 0.14, isNightMode ? (byte)0xD8 : (byte)0xE0, isOverlay ? 36 : 28), ThemeAppearanceValues.MaterialMica => ( isOverlay ? 0.20 : 0.14, isOverlay ? 0.12 : 0.08, isNightMode ? (byte)0xEC : (byte)0xF2, isOverlay ? 28 : 20), _ => ( isOverlay ? 0.12 : 0.08, isOverlay ? 0.08 : 0.05, (byte)0xFF, 0) }; } private static Color ResolveNeutralBase(bool isNightMode, MaterialSurfaceRole role) { return role switch { MaterialSurfaceRole.WindowBackground => isNightMode ? Color.Parse("#FF0A0F16") : Color.Parse("#FFF7F8FA"), MaterialSurfaceRole.SettingsWindowBackground => isNightMode ? Color.Parse("#FF0C121A") : Color.Parse("#FFF8FAFC"), MaterialSurfaceRole.DockBackground => isNightMode ? Color.Parse("#FF111A24") : Color.Parse("#FFFAFBFD"), MaterialSurfaceRole.StatusBarBackground => isNightMode ? Color.Parse("#FF101720") : Color.Parse("#FFF9FBFE"), MaterialSurfaceRole.StatusBarComponentHost => isNightMode ? Color.Parse("#FF111A23") : Color.Parse("#FFFCFDFE"), MaterialSurfaceRole.OverlayPanel => isNightMode ? Color.Parse("#FF131C27") : Color.Parse("#FFF4F7FB"), _ => isNightMode ? Color.Parse("#FF121B26") : Color.Parse("#FFFDFEFF") }; } private static Color ResolveLiftBase(bool isNightMode, MaterialSurfaceRole role) { return role switch { MaterialSurfaceRole.DockBackground or MaterialSurfaceRole.StatusBarBackground or MaterialSurfaceRole.OverlayPanel => isNightMode ? Color.Parse("#FF1B2633") : Color.Parse("#FFFFFFFF"), _ => isNightMode ? Color.Parse("#FF17212D") : Color.Parse("#FFFFFFFF") }; } } internal sealed class AppearanceThemeService : IAppearanceThemeService, IDisposable { private static readonly Color DefaultAccentColor = Color.Parse("#FF3B82F6"); private static readonly Color NeutralFallbackSeedColor = Color.Parse("#FF8A8A8A"); private readonly ISettingsFacadeService _settingsFacade; private readonly ISystemWallpaperService _systemWallpaperService; private readonly IWindowMaterialService _windowMaterialService; private readonly IMaterialSurfaceService _materialSurfaceService; private readonly IVideoWallpaperSeedExtractor _videoWallpaperSeedExtractor; private readonly MonetColorService _monetColorService = new(); private readonly string _liveThemeColorMode; private readonly string _liveSystemMaterialMode; private readonly string? _liveSelectedWallpaperSeed; private readonly object _paletteGate = new(); private readonly Dictionary _wallpaperSeedCache = new(StringComparer.OrdinalIgnoreCase); private readonly HashSet _pendingWallpaperSeedKeys = new(StringComparer.OrdinalIgnoreCase); public AppearanceThemeService( ISettingsFacadeService settingsFacade, ISystemWallpaperService systemWallpaperService, IWindowMaterialService windowMaterialService, IMaterialSurfaceService materialSurfaceService, IVideoWallpaperSeedExtractor? videoWallpaperSeedExtractor = null) { _settingsFacade = settingsFacade ?? throw new ArgumentNullException(nameof(settingsFacade)); _systemWallpaperService = systemWallpaperService ?? throw new ArgumentNullException(nameof(systemWallpaperService)); _windowMaterialService = windowMaterialService ?? throw new ArgumentNullException(nameof(windowMaterialService)); _materialSurfaceService = materialSurfaceService ?? throw new ArgumentNullException(nameof(materialSurfaceService)); _videoWallpaperSeedExtractor = videoWallpaperSeedExtractor ?? new LibVlcVideoWallpaperSeedExtractor(); var initialThemeState = _settingsFacade.Theme.Get(); _liveThemeColorMode = ThemeAppearanceValues.NormalizeThemeColorMode( initialThemeState.ThemeColorMode, initialThemeState.ThemeColor); _liveSystemMaterialMode = ResolveSupportedMaterialMode(initialThemeState.SystemMaterialMode); _liveSelectedWallpaperSeed = initialThemeState.SelectedWallpaperSeed; _settingsFacade.Settings.Changed += OnSettingsChanged; } public event EventHandler? Changed; public AppearanceThemeSnapshot GetCurrent() { return BuildCurrentSnapshot(queueWallpaperPaletteBuild: true); } public AppearanceThemeSnapshot BuildPreview(ThemeAppearanceSettingsState pendingState) { ArgumentNullException.ThrowIfNull(pendingState); var normalizedThemeColorMode = ThemeAppearanceValues.NormalizeThemeColorMode( pendingState.ThemeColorMode, pendingState.ThemeColor); var normalizedSystemMaterialMode = ResolveSupportedMaterialMode(pendingState.SystemMaterialMode); return BuildSnapshot( pendingState with { ThemeColorMode = normalizedThemeColorMode, SystemMaterialMode = normalizedSystemMaterialMode }, normalizedThemeColorMode, normalizedSystemMaterialMode, pendingState.SelectedWallpaperSeed, queueWallpaperPaletteBuild: true); } public void ApplyThemeResources(IResourceDictionary resources) { ArgumentNullException.ThrowIfNull(resources); var snapshot = GetCurrent(); var context = CreateThemeContext(snapshot); ThemeColorSystemService.ApplyThemeResources(resources, context); GlassEffectService.ApplyGlassResources(resources, context); } public AppearanceMaterialSurface GetMaterialSurface(MaterialSurfaceRole role) { var snapshot = GetCurrent(); return _materialSurfaceService.GetSurface(CreateThemeContext(snapshot), role); } public void ApplyWindowMaterial(Window window, MaterialSurfaceRole role) { ArgumentNullException.ThrowIfNull(window); // Avoid hot-switching real backdrops on already-visible windows. This has been // a stability hotspot when users flip theme source/material at runtime. if (window.IsVisible) { return; } var snapshot = GetCurrent(); try { _windowMaterialService.Apply(window, snapshot.SystemMaterialMode); } catch (Exception ex) { AppLogger.Warn( "Appearance.WindowMaterial", $"Failed to apply window material '{snapshot.SystemMaterialMode}'. Falling back to none.", ex); _windowMaterialService.Apply(window, ThemeAppearanceValues.MaterialNone); } } public void Dispose() { _settingsFacade.Settings.Changed -= OnSettingsChanged; } private AppearanceThemeSnapshot BuildCurrentSnapshot(bool queueWallpaperPaletteBuild) { var themeState = _settingsFacade.Theme.Get(); return BuildSnapshot( themeState, _liveThemeColorMode, _liveSystemMaterialMode, _liveSelectedWallpaperSeed, queueWallpaperPaletteBuild); } private void OnSettingsChanged(object? sender, SettingsChangedEvent e) { _ = sender; if (e.Scope != SettingsScope.App) { return; } var changedKeys = e.ChangedKeys?.ToArray(); var refreshAll = changedKeys is null || changedKeys.Length == 0; var respondsToThemeColor = string.Equals( _liveThemeColorMode, ThemeAppearanceValues.ColorModeSeedMonet, StringComparison.OrdinalIgnoreCase); var respondsToWallpaper = string.Equals( _liveThemeColorMode, ThemeAppearanceValues.ColorModeWallpaperMonet, StringComparison.OrdinalIgnoreCase); if (!refreshAll && !changedKeys.Contains(nameof(AppSettingsSnapshot.IsNightMode), StringComparer.OrdinalIgnoreCase) && !changedKeys.Contains(nameof(AppSettingsSnapshot.UseSystemChrome), StringComparer.OrdinalIgnoreCase) && !(respondsToThemeColor && changedKeys.Contains(nameof(AppSettingsSnapshot.ThemeColor), StringComparer.OrdinalIgnoreCase)) && !(respondsToWallpaper && (changedKeys.Contains(nameof(AppSettingsSnapshot.WallpaperPath), StringComparer.OrdinalIgnoreCase) || changedKeys.Contains(nameof(AppSettingsSnapshot.WallpaperType), StringComparer.OrdinalIgnoreCase) || changedKeys.Contains(nameof(AppSettingsSnapshot.WallpaperColor), StringComparer.OrdinalIgnoreCase)))) { return; } RaiseChanged(queueWallpaperPaletteBuild: true); } private AppearanceThemeSnapshot BuildSnapshot( ThemeAppearanceSettingsState themeState, string themeColorMode, string systemMaterialMode, string? selectedWallpaperSeed, bool queueWallpaperPaletteBuild) { var availableModes = _windowMaterialService.GetAvailableModes(); MonetPalette palette; IReadOnlyList wallpaperSeedCandidates; Color effectiveSeedColor; string resolvedSeedSource; string? resolvedWallpaperPath; if (string.Equals(themeColorMode, ThemeAppearanceValues.ColorModeWallpaperMonet, StringComparison.OrdinalIgnoreCase)) { var wallpaperState = _settingsFacade.Wallpaper.Get(); var wallpaperResolution = ResolveWallpaperPalette( themeState.IsNightMode, wallpaperState, selectedWallpaperSeed, queueWallpaperPaletteBuild); palette = wallpaperResolution.Palette; wallpaperSeedCandidates = wallpaperResolution.SeedCandidates; effectiveSeedColor = wallpaperResolution.EffectiveSeedColor; resolvedSeedSource = wallpaperResolution.ResolvedSeedSource; resolvedWallpaperPath = wallpaperResolution.ResolvedWallpaperPath; } else { var preferredSeedColor = string.Equals(themeColorMode, ThemeAppearanceValues.ColorModeSeedMonet, StringComparison.OrdinalIgnoreCase) ? themeState.ThemeColor : null; palette = _settingsFacade.Theme.BuildPalette(themeState.IsNightMode, null, preferredSeedColor); wallpaperSeedCandidates = []; effectiveSeedColor = ResolveEffectiveSeedColor(themeColorMode, themeState.ThemeColor, palette); resolvedSeedSource = string.Equals(themeColorMode, ThemeAppearanceValues.ColorModeDefaultNeutral, StringComparison.OrdinalIgnoreCase) ? "neutral" : "user_color"; resolvedWallpaperPath = null; } return new AppearanceThemeSnapshot( themeState.IsNightMode, themeColorMode, themeState.ThemeColor, selectedWallpaperSeed, resolvedSeedSource, palette, ResolveAccentColor(themeColorMode, themeState.ThemeColor, palette), effectiveSeedColor, wallpaperSeedCandidates, systemMaterialMode, availableModes, _windowMaterialService.CanChangeMode, themeState.UseSystemChrome, resolvedWallpaperPath); } private ThemeColorContext CreateThemeContext(AppearanceThemeSnapshot snapshot) { return new ThemeColorContext( snapshot.AccentColor, IsLightBackground: !snapshot.IsNightMode, IsLightNavBackground: !snapshot.IsNightMode, IsNightMode: snapshot.IsNightMode, MonetPalette: snapshot.MonetPalette, MonetColors: snapshot.MonetPalette.MonetColors, UseNeutralSurfaces: snapshot.ThemeColorMode == ThemeAppearanceValues.ColorModeDefaultNeutral, SystemMaterialMode: snapshot.SystemMaterialMode); } private string ResolveSupportedMaterialMode(string? requestedMode) { var normalized = ThemeAppearanceValues.NormalizeSystemMaterialMode(requestedMode); var availableModes = _windowMaterialService.GetAvailableModes(); return availableModes.Contains(normalized, StringComparer.OrdinalIgnoreCase) ? normalized : ThemeAppearanceValues.MaterialNone; } private WallpaperPaletteResolution ResolveWallpaperPalette( bool nightMode, WallpaperSettingsState wallpaperState, string? selectedWallpaperSeed, bool queueWallpaperPaletteBuild) { var source = ResolveWallpaperSeedSource(wallpaperState); if (string.Equals(source.SourceKind, "fallback", StringComparison.OrdinalIgnoreCase)) { return BuildFallbackWallpaperPaletteResolution(nightMode, source.ResolvedWallpaperPath); } if (string.Equals(source.SourceKind, "app_solid", StringComparison.OrdinalIgnoreCase)) { var candidates = source.SolidColor is { } solidColor ? new[] { solidColor } : []; return BuildWallpaperPaletteResolution(nightMode, source, candidates, selectedWallpaperSeed); } lock (_paletteGate) { if (_wallpaperSeedCache.TryGetValue(source.SourceKey, out var cachedSeedResult)) { if (cachedSeedResult.SeedCandidates.Count > 0) { return BuildWallpaperPaletteResolution( nightMode, source with { SourceKind = cachedSeedResult.SourceKind, ResolvedWallpaperPath = cachedSeedResult.ResolvedWallpaperPath }, cachedSeedResult.SeedCandidates, selectedWallpaperSeed); } return BuildFallbackWallpaperPaletteResolution(nightMode, cachedSeedResult.ResolvedWallpaperPath); } } if (queueWallpaperPaletteBuild) { QueueWallpaperSeedExtraction(source); } return BuildFallbackWallpaperPaletteResolution(nightMode, source.ResolvedWallpaperPath); } private static Color ResolveAccentColor( string themeColorMode, string? colorText, MonetPalette monetPalette) { if (themeColorMode == ThemeAppearanceValues.ColorModeDefaultNeutral) { return DefaultAccentColor; } if (monetPalette.Primary.A > 0) { return monetPalette.Primary; } if (!string.IsNullOrWhiteSpace(colorText) && Color.TryParse(colorText, out var parsedColor)) { return parsedColor; } return DefaultAccentColor; } private static Color ResolveEffectiveSeedColor( string themeColorMode, string? userThemeColor, MonetPalette monetPalette) { if (themeColorMode == ThemeAppearanceValues.ColorModeDefaultNeutral) { return DefaultAccentColor; } if (themeColorMode == ThemeAppearanceValues.ColorModeSeedMonet && !string.IsNullOrWhiteSpace(userThemeColor) && Color.TryParse(userThemeColor, out var parsedColor)) { return parsedColor; } return monetPalette.Seed; } private WallpaperPaletteResolution BuildWallpaperPaletteResolution( bool nightMode, WallpaperSeedSourceDescriptor source, IReadOnlyList seedCandidates, string? selectedWallpaperSeed) { var validatedSeed = ResolveSelectedWallpaperSeed(seedCandidates, selectedWallpaperSeed); var palette = _monetColorService.BuildPaletteFromSeedCandidates(seedCandidates, nightMode, validatedSeed); return new WallpaperPaletteResolution( palette, seedCandidates, source.SourceKind, palette.Seed, source.ResolvedWallpaperPath); } private WallpaperPaletteResolution BuildFallbackWallpaperPaletteResolution(bool nightMode, string? resolvedWallpaperPath) { var palette = _monetColorService.BuildPaletteFromSeedCandidates([], nightMode, NeutralFallbackSeedColor); return new WallpaperPaletteResolution( palette, [], "fallback", palette.Seed, resolvedWallpaperPath); } private void QueueWallpaperSeedExtraction(WallpaperSeedSourceDescriptor source) { if (string.Equals(source.SourceKind, "fallback", StringComparison.OrdinalIgnoreCase) || string.Equals(source.SourceKind, "app_solid", StringComparison.OrdinalIgnoreCase)) { return; } lock (_paletteGate) { if (_pendingWallpaperSeedKeys.Contains(source.SourceKey)) { return; } _pendingWallpaperSeedKeys.Add(source.SourceKey); } _ = Task.Run(() => { WallpaperSeedExtractionResult? extractionResult = null; try { extractionResult = ExtractWallpaperSeedCandidates(source); } catch (Exception ex) { AppLogger.Warn( "Appearance.WallpaperSeed", $"Failed to build wallpaper seed candidates asynchronously. Source='{source.SourceKind}'; Path='{source.FilePath}'.", ex); } finally { lock (_paletteGate) { _pendingWallpaperSeedKeys.Remove(source.SourceKey); if (extractionResult is not null) { _wallpaperSeedCache[source.SourceKey] = extractionResult; } } } if (extractionResult is not null) { RaiseChanged(queueWallpaperPaletteBuild: false); } }); } private WallpaperSeedExtractionResult ExtractWallpaperSeedCandidates(WallpaperSeedSourceDescriptor source) { IReadOnlyList seedCandidates = source.SourceKind switch { "app_wallpaper" or "system_wallpaper" => ExtractImageSeedCandidates(source.FilePath), "app_video" => ExtractVideoSeedCandidates(source.FilePath), "app_solid" when source.SolidColor is { } solidColor => new[] { solidColor }, _ => [] }; return new WallpaperSeedExtractionResult( source.SourceKind, source.SourceKey, source.ResolvedWallpaperPath, seedCandidates); } private IReadOnlyList ExtractImageSeedCandidates(string? wallpaperPath) { if (string.IsNullOrWhiteSpace(wallpaperPath) || !File.Exists(wallpaperPath)) { return []; } try { using var bitmap = new Bitmap(wallpaperPath); return _monetColorService.ExtractSeedCandidates(bitmap); } catch (Exception ex) { AppLogger.Warn( "Appearance.WallpaperSeed", $"Failed to extract wallpaper seed candidates from image '{wallpaperPath}'.", ex); return []; } } private IReadOnlyList ExtractVideoSeedCandidates(string? wallpaperPath) { if (string.IsNullOrWhiteSpace(wallpaperPath) || !File.Exists(wallpaperPath)) { return []; } return _videoWallpaperSeedExtractor.ExtractSeedCandidates(wallpaperPath, _monetColorService); } private WallpaperSeedSourceDescriptor ResolveWallpaperSeedSource(WallpaperSettingsState wallpaperState) { if (string.Equals(wallpaperState.Type, "SolidColor", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(wallpaperState.Color) && Color.TryParse(wallpaperState.Color, out var solidColor)) { var solidText = solidColor.ToString(); return new WallpaperSeedSourceDescriptor( "app_solid", $"app_solid|{solidText}", null, null, solidColor); } var wallpaperPath = string.IsNullOrWhiteSpace(wallpaperState.WallpaperPath) ? null : wallpaperState.WallpaperPath.Trim(); var appWallpaperMediaType = _settingsFacade.WallpaperMedia.DetectMediaType(wallpaperPath); if (!string.IsNullOrWhiteSpace(wallpaperPath) && File.Exists(wallpaperPath)) { if (appWallpaperMediaType == WallpaperMediaType.Image) { return new WallpaperSeedSourceDescriptor( "app_wallpaper", CreateWallpaperSourceKey("app_wallpaper", wallpaperPath), wallpaperPath, wallpaperPath, null); } if (appWallpaperMediaType == WallpaperMediaType.Video) { return new WallpaperSeedSourceDescriptor( "app_video", CreateWallpaperSourceKey("app_video", wallpaperPath), wallpaperPath, wallpaperPath, null); } } var systemWallpaper = _systemWallpaperService.GetWallpaperPath(); if (!string.IsNullOrWhiteSpace(systemWallpaper) && File.Exists(systemWallpaper) && _settingsFacade.WallpaperMedia.DetectMediaType(systemWallpaper) == WallpaperMediaType.Image) { return new WallpaperSeedSourceDescriptor( "system_wallpaper", CreateWallpaperSourceKey("system_wallpaper", systemWallpaper), systemWallpaper, systemWallpaper, null); } return new WallpaperSeedSourceDescriptor( "fallback", "fallback", null, null, null); } private void RaiseChanged(bool queueWallpaperPaletteBuild) { var snapshot = BuildCurrentSnapshot(queueWallpaperPaletteBuild); if (Dispatcher.UIThread.CheckAccess()) { Changed?.Invoke(this, snapshot); return; } Dispatcher.UIThread.Post(() => Changed?.Invoke(this, snapshot), DispatcherPriority.Background); } private static Color? ResolveSelectedWallpaperSeed( IReadOnlyList seedCandidates, string? selectedWallpaperSeed) { if (seedCandidates.Count == 0) { return null; } if (!string.IsNullOrWhiteSpace(selectedWallpaperSeed) && Color.TryParse(selectedWallpaperSeed, out var parsedSeed)) { foreach (var candidate in seedCandidates) { if (candidate == parsedSeed) { return candidate; } } } return seedCandidates[0]; } private static string CreateWallpaperSourceKey(string sourceKind, string wallpaperPath) { long lastWriteTicks = 0; long length = 0; try { var fileInfo = new FileInfo(wallpaperPath); if (fileInfo.Exists) { lastWriteTicks = fileInfo.LastWriteTimeUtc.Ticks; length = fileInfo.Length; } } catch { // Keep the cache key resilient even if metadata lookup fails. } return string.Concat( sourceKind, "|", wallpaperPath, "|", lastWriteTicks.ToString(), "|", length.ToString()); } } internal static class HostAppearanceThemeProvider { private static readonly object Gate = new(); private static AppearanceThemeService? _instance; public static IAppearanceThemeService GetOrCreate() { lock (Gate) { return _instance ??= new AppearanceThemeService( HostSettingsFacadeProvider.GetOrCreate(), new SystemWallpaperService(), new WindowMaterialService(), new MaterialSurfaceService()); } } }