diff --git a/LanMountainDesktop.PluginIsolation.Contracts/AppearanceContracts.cs b/LanMountainDesktop.PluginIsolation.Contracts/AppearanceContracts.cs
index b7281a8..45e64d0 100644
--- a/LanMountainDesktop.PluginIsolation.Contracts/AppearanceContracts.cs
+++ b/LanMountainDesktop.PluginIsolation.Contracts/AppearanceContracts.cs
@@ -1,5 +1,9 @@
namespace LanMountainDesktop.PluginIsolation.Contracts;
+///
+/// Wire request for the IPC appearance snapshot payload. This request targets the
+/// isolation-contract DTOs, not the runtime SDK snapshot with the same type name.
+///
public sealed record PluginAppearanceSnapshotRequest(string SessionId);
public sealed record PluginMaterialSurfaceSnapshot(
@@ -8,6 +12,10 @@ public sealed record PluginMaterialSurfaceSnapshot(
double BlurRadius,
double Opacity);
+///
+/// Wire-format appearance snapshot exchanged over IPC.
+/// Do not treat this as the same type as LanMountainDesktop.PluginSdk.PluginAppearanceSnapshot.
+///
public sealed record PluginAppearanceSnapshot(
string ThemeVariant,
string? AccentColor = null,
@@ -21,4 +29,7 @@ public sealed record PluginAppearanceSnapshot(
IReadOnlyDictionary? MaterialSurfaces = null,
IReadOnlyList? WallpaperSeedCandidates = null);
+///
+/// Wire notification carrying the IPC appearance snapshot.
+///
public sealed record PluginAppearanceChangedNotification(PluginAppearanceSnapshot Snapshot);
diff --git a/LanMountainDesktop.PluginSdk/PluginAppearanceSnapshot.cs b/LanMountainDesktop.PluginSdk/PluginAppearanceSnapshot.cs
index 7c13ef1..34ef99e 100644
--- a/LanMountainDesktop.PluginSdk/PluginAppearanceSnapshot.cs
+++ b/LanMountainDesktop.PluginSdk/PluginAppearanceSnapshot.cs
@@ -1,11 +1,21 @@
namespace LanMountainDesktop.PluginSdk;
+///
+/// This is the runtime snapshot shape consumed by plugins inside the host process.
+/// It is intentionally distinct from the wire DTO with the same name in
+/// LanMountainDesktop.PluginIsolation.Contracts.PluginAppearanceSnapshot.
+///
public sealed record PluginMaterialSurfaceSnapshot(
string BackgroundColor,
string BorderColor,
double BlurRadius,
double Opacity);
+///
+/// Runtime-facing appearance snapshot for plugins. This is not the same contract as the
+/// wire-format snapshot in LanMountainDesktop.PluginIsolation.Contracts, even though the
+/// type name matches.
+///
public sealed record PluginAppearanceSnapshot(
PluginCornerRadiusTokens CornerRadiusTokens,
string ThemeVariant,
diff --git a/LanMountainDesktop.Tests/ComponentColorSchemeHelperTests.cs b/LanMountainDesktop.Tests/ComponentColorSchemeHelperTests.cs
new file mode 100644
index 0000000..cb17004
--- /dev/null
+++ b/LanMountainDesktop.Tests/ComponentColorSchemeHelperTests.cs
@@ -0,0 +1,125 @@
+using System.Collections.Generic;
+using Avalonia;
+using Avalonia.Media;
+using LanMountainDesktop.ComponentSystem;
+using LanMountainDesktop.Models;
+using LanMountainDesktop.Services;
+using LanMountainDesktop.Shared.Contracts;
+using Xunit;
+
+namespace LanMountainDesktop.Tests;
+
+public sealed class ComponentColorSchemeHelperTests
+{
+ [Fact]
+ public void GetCurrentGlobalThemeColorMode_UsesMaterialColorSnapshot()
+ {
+ var snapshot = CreateSnapshot(ThemeAppearanceValues.ColorModeWallpaperMonet);
+
+ var mode = ComponentColorSchemeHelper.GetCurrentGlobalThemeColorMode(snapshot);
+
+ Assert.Equal(ThemeAppearanceValues.ColorModeWallpaperMonet, mode);
+ }
+
+ [Theory]
+ [InlineData(ThemeAppearanceValues.ColorSchemeNative, ThemeAppearanceValues.ColorModeWallpaperMonet, false)]
+ [InlineData(ThemeAppearanceValues.ColorSchemeFollowSystem, ThemeAppearanceValues.ColorModeDefaultNeutral, true)]
+ [InlineData(null, ThemeAppearanceValues.ColorModeDefaultNeutral, false)]
+ public void ShouldUseMonetColor_FollowsExpectedRules(
+ string? componentColorScheme,
+ string globalThemeColorMode,
+ bool expected)
+ {
+ var shouldUseMonetColor = ComponentColorSchemeHelper.ShouldUseMonetColor(componentColorScheme, globalThemeColorMode);
+
+ Assert.Equal(expected, shouldUseMonetColor);
+ }
+
+ private static MaterialColorSnapshot CreateSnapshot(string themeColorMode)
+ {
+ var seed = Color.Parse("#FF123456");
+ var accent = Color.Parse("#FF214365");
+ var palette = new MaterialColorPalette(
+ Color.Parse("#FF315577"),
+ Color.Parse("#FF557799"),
+ accent,
+ Color.Parse("#FFFFFFFF"),
+ Color.Parse("#FF5F7F9F"),
+ Color.Parse("#FF7F9FBF"),
+ Color.Parse("#FF9FBFDF"),
+ Color.Parse("#FF17314B"),
+ Color.Parse("#FF102840"),
+ Color.Parse("#FF082038"),
+ Color.Parse("#FF0B1118"),
+ Color.Parse("#FF141C24"),
+ Color.Parse("#FF1C2630"),
+ Color.Parse("#FFF5F7FA"),
+ Color.Parse("#FFC8D0DA"),
+ Color.Parse("#FF9EA8B4"),
+ Color.Parse("#FF91B8E8"),
+ Color.Parse("#FFF5F7FA"),
+ Color.Parse("#FFFFFFFF"),
+ Color.Parse("#FF9FBFDF"),
+ Color.Parse("#33141C24"),
+ Color.Parse("#441C2630"),
+ Color.Parse("#55315577"),
+ Color.Parse("#FF315577"),
+ Color.Parse("#88557799"),
+ Color.Parse("#667F9FBF"));
+ var monetPalette = new MonetPalette(
+ [seed],
+ seed,
+ palette.Primary,
+ palette.Secondary,
+ Color.Parse("#FF775577"),
+ Color.Parse("#FF202830"),
+ Color.Parse("#FF26313B"));
+ var surfaces = new Dictionary
+ {
+ [MaterialSurfaceRole.WindowBackground] = new(
+ MaterialSurfaceRole.WindowBackground,
+ Color.Parse("#FF101820"),
+ Color.Parse("#33557799"),
+ 18,
+ 0.92),
+ [MaterialSurfaceRole.OverlayPanel] = new(
+ MaterialSurfaceRole.OverlayPanel,
+ Color.Parse("#FF202A34"),
+ Color.Parse("#556688AA"),
+ 24,
+ 0.88)
+ };
+
+ return new MaterialColorSnapshot(
+ IsNightMode: true,
+ ThemeColorMode: themeColorMode,
+ ThemeWallpaperColorSource: ThemeAppearanceValues.WallpaperColorSourceAuto,
+ ColorSourceKind: MaterialColorSourceKind.CustomSeed,
+ ResolvedSeedSource: "user_color",
+ CornerRadiusTokens: new AppearanceCornerRadiusTokens(
+ new CornerRadius(2),
+ new CornerRadius(4),
+ new CornerRadius(6),
+ new CornerRadius(8),
+ new CornerRadius(10),
+ new CornerRadius(12),
+ new CornerRadius(14),
+ new CornerRadius(8)),
+ UserThemeColor: seed.ToString(),
+ SelectedWallpaperSeed: seed.ToString(),
+ EffectiveSeedColor: seed,
+ AccentColor: accent,
+ MonetPalette: monetPalette,
+ Palette: palette,
+ WallpaperSeedCandidates: [seed],
+ SystemMaterialMode: ThemeAppearanceValues.MaterialMica,
+ AvailableSystemMaterialModes: [ThemeAppearanceValues.MaterialAuto, ThemeAppearanceValues.MaterialMica],
+ CanChangeSystemMaterial: true,
+ UseSystemChrome: false,
+ ResolvedWallpaperPath: @"C:\wallpaper.png",
+ UseNativeWallpaperChangeEvents: true,
+ NativeWallpaperChangeEventsActive: true,
+ WallpaperPollingActive: true,
+ Surfaces: surfaces);
+ }
+}
diff --git a/LanMountainDesktop.Tests/PluginAppearanceBoundaryTests.cs b/LanMountainDesktop.Tests/PluginAppearanceBoundaryTests.cs
new file mode 100644
index 0000000..4645fcb
--- /dev/null
+++ b/LanMountainDesktop.Tests/PluginAppearanceBoundaryTests.cs
@@ -0,0 +1,379 @@
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+using System.Text.Json;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Media;
+using LanMountainDesktop.Appearance;
+using LanMountainDesktop.Models;
+using LanMountainDesktop.Plugins;
+using LanMountainDesktop.Services;
+using LanMountainDesktop.Services.Settings;
+using LanMountainDesktop.Settings.Core;
+using LanMountainDesktop.Shared.Contracts;
+using Xunit;
+using PluginIsolationAppearanceChangedNotification = LanMountainDesktop.PluginIsolation.Contracts.PluginAppearanceChangedNotification;
+using PluginIsolationAppearanceSnapshot = LanMountainDesktop.PluginIsolation.Contracts.PluginAppearanceSnapshot;
+using PluginIsolationAppearanceSnapshotRequest = LanMountainDesktop.PluginIsolation.Contracts.PluginAppearanceSnapshotRequest;
+using PluginIsolationMaterialSurfaceSnapshot = LanMountainDesktop.PluginIsolation.Contracts.PluginMaterialSurfaceSnapshot;
+using PluginIsolationJsonContext = LanMountainDesktop.PluginIsolation.Contracts.PluginIsolationJsonContext;
+using MaterialColorPalette = LanMountainDesktop.Models.MaterialColorPalette;
+using PluginSdkAppearanceSnapshot = LanMountainDesktop.PluginSdk.PluginAppearanceSnapshot;
+
+namespace LanMountainDesktop.Tests;
+
+public sealed class PluginAppearanceBoundaryTests
+{
+ [Fact]
+ public void PluginLoader_PrefersMaterialColorService_WhenBothAppearanceSourcesAreAvailable()
+ {
+ var materialService = new TrackingMaterialColorService(CreateMaterialSnapshot());
+ var themeService = new TrackingAppearanceThemeService(CreateThemeSnapshot());
+ var provider = new TrackingServiceProvider(materialService, themeService);
+
+ var snapshot = InvokeLoaderSnapshotBuilder(provider);
+
+ Assert.Equal(1, materialService.GetMaterialColorSnapshotCalls);
+ Assert.Equal(0, themeService.GetCurrentCalls);
+ Assert.Equal("Dark", snapshot.ThemeVariant);
+ Assert.Equal(Color.Parse("#FF214365").ToString(), snapshot.AccentColor);
+ Assert.Equal(Color.Parse("#FF123456").ToString(), snapshot.SeedColor);
+ Assert.Equal(MaterialColorSourceKind.CustomSeed.ToString(), snapshot.ColorSource);
+ Assert.Equal(ThemeAppearanceValues.MaterialMica, snapshot.SystemMaterialMode);
+ Assert.Equal(Color.Parse("#FF214365").ToString(), snapshot.ColorRoles?["accent"]);
+ }
+
+ [Fact]
+ public void PluginIsolationJsonContext_RoundTripsAppearancePayloads()
+ {
+ var request = new PluginIsolationAppearanceSnapshotRequest("session-42");
+ var snapshot = new PluginIsolationAppearanceSnapshot(
+ ThemeVariant: "Dark",
+ AccentColor: "#FF214365",
+ CornerRadiusScale: 1.25,
+ CornerRadiusTokens: new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ ["component"] = 24,
+ ["sm"] = 8
+ },
+ ResourceAliases: new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ ["surface-base"] = "DesignSurfaceBase"
+ },
+ SeedColor: "#FF123456",
+ ColorSource: "custom_seed",
+ SystemMaterialMode: ThemeAppearanceValues.MaterialMica,
+ ColorRoles: new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ ["accent"] = "#FF214365",
+ ["primary"] = "#FF315577"
+ },
+ MaterialSurfaces: new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ ["WindowBackground"] = new PluginIsolationMaterialSurfaceSnapshot("#FF101820", "#33557799", 18, 0.92)
+ },
+ WallpaperSeedCandidates: ["#FF123456", "#FF214365"]);
+ var notification = new PluginIsolationAppearanceChangedNotification(snapshot);
+
+ var requestJson = JsonSerializer.Serialize(request, PluginIsolationJsonContext.Default.PluginAppearanceSnapshotRequest);
+ var requestRoundTrip = JsonSerializer.Deserialize(requestJson, PluginIsolationJsonContext.Default.PluginAppearanceSnapshotRequest);
+
+ var snapshotJson = JsonSerializer.Serialize(snapshot, PluginIsolationJsonContext.Default.PluginAppearanceSnapshot);
+ var snapshotRoundTrip = JsonSerializer.Deserialize(snapshotJson, PluginIsolationJsonContext.Default.PluginAppearanceSnapshot);
+
+ var notificationJson = JsonSerializer.Serialize(notification, PluginIsolationJsonContext.Default.PluginAppearanceChangedNotification);
+ var notificationRoundTrip = JsonSerializer.Deserialize(notificationJson, PluginIsolationJsonContext.Default.PluginAppearanceChangedNotification);
+
+ Assert.Equal(request, requestRoundTrip);
+ AssertAppearanceSnapshotEqual(snapshot, snapshotRoundTrip!);
+ AssertAppearanceSnapshotEqual(snapshot, notificationRoundTrip!.Snapshot);
+ }
+
+ private static PluginSdkAppearanceSnapshot InvokeLoaderSnapshotBuilder(IServiceProvider provider)
+ {
+ var method = typeof(PluginLoader).GetMethod(
+ "BuildAppearanceSnapshot",
+ BindingFlags.NonPublic | BindingFlags.Static);
+
+ Assert.NotNull(method);
+
+ var result = method!.Invoke(null, [provider]);
+ return Assert.IsType(result);
+ }
+
+ private static void AssertAppearanceSnapshotEqual(PluginIsolationAppearanceSnapshot expected, PluginIsolationAppearanceSnapshot actual)
+ {
+ Assert.Equal(expected.ThemeVariant, actual.ThemeVariant);
+ Assert.Equal(expected.AccentColor, actual.AccentColor);
+ Assert.Equal(expected.CornerRadiusScale, actual.CornerRadiusScale);
+ Assert.Equal(expected.SeedColor, actual.SeedColor);
+ Assert.Equal(expected.ColorSource, actual.ColorSource);
+ Assert.Equal(expected.SystemMaterialMode, actual.SystemMaterialMode);
+ Assert.Equal(expected.WallpaperSeedCandidates, actual.WallpaperSeedCandidates);
+ AssertDictionaryEqual(expected.CornerRadiusTokens, actual.CornerRadiusTokens);
+ AssertDictionaryEqual(expected.ResourceAliases, actual.ResourceAliases);
+ AssertDictionaryEqual(expected.ColorRoles, actual.ColorRoles);
+
+ Assert.NotNull(actual.MaterialSurfaces);
+ Assert.Equal(expected.MaterialSurfaces!.Count, actual.MaterialSurfaces!.Count);
+ foreach (var pair in expected.MaterialSurfaces)
+ {
+ Assert.True(actual.MaterialSurfaces.TryGetValue(pair.Key, out var actualSurface));
+ Assert.Equal(pair.Value.BackgroundColor, actualSurface.BackgroundColor);
+ Assert.Equal(pair.Value.BorderColor, actualSurface.BorderColor);
+ Assert.Equal(pair.Value.BlurRadius, actualSurface.BlurRadius);
+ Assert.Equal(pair.Value.Opacity, actualSurface.Opacity);
+ }
+ }
+
+ private static void AssertDictionaryEqual(
+ IReadOnlyDictionary? expected,
+ IReadOnlyDictionary? actual)
+ where TKey : notnull
+ {
+ if (expected is null)
+ {
+ Assert.Null(actual);
+ return;
+ }
+
+ Assert.NotNull(actual);
+ Assert.Equal(expected.Count, actual!.Count);
+ foreach (var pair in expected)
+ {
+ Assert.True(actual.TryGetValue(pair.Key, out var actualValue));
+ Assert.Equal(pair.Value, actualValue);
+ }
+ }
+
+ private static MaterialColorSnapshot CreateMaterialSnapshot()
+ {
+ var seed = Color.Parse("#FF123456");
+ var accent = Color.Parse("#FF214365");
+ var palette = new MaterialColorPalette(
+ Color.Parse("#FF315577"),
+ Color.Parse("#FF557799"),
+ accent,
+ Color.Parse("#FFFFFFFF"),
+ Color.Parse("#FF5F7F9F"),
+ Color.Parse("#FF7F9FBF"),
+ Color.Parse("#FF9FBFDF"),
+ Color.Parse("#FF17314B"),
+ Color.Parse("#FF102840"),
+ Color.Parse("#FF082038"),
+ Color.Parse("#FF0B1118"),
+ Color.Parse("#FF141C24"),
+ Color.Parse("#FF1C2630"),
+ Color.Parse("#FFF5F7FA"),
+ Color.Parse("#FFC8D0DA"),
+ Color.Parse("#FF9EA8B4"),
+ Color.Parse("#FF91B8E8"),
+ Color.Parse("#FFF5F7FA"),
+ Color.Parse("#FFFFFFFF"),
+ Color.Parse("#FF9FBFDF"),
+ Color.Parse("#33141C24"),
+ Color.Parse("#441C2630"),
+ Color.Parse("#55315577"),
+ Color.Parse("#FF315577"),
+ Color.Parse("#88557799"),
+ Color.Parse("#667F9FBF"));
+ var monetPalette = new MonetPalette(
+ [seed],
+ seed,
+ palette.Primary,
+ palette.Secondary,
+ Color.Parse("#FF775577"),
+ Color.Parse("#FF202830"),
+ Color.Parse("#FF26313B"));
+ var surfaces = new Dictionary
+ {
+ [MaterialSurfaceRole.WindowBackground] = new(
+ MaterialSurfaceRole.WindowBackground,
+ Color.Parse("#FF101820"),
+ Color.Parse("#33557799"),
+ 18,
+ 0.92),
+ [MaterialSurfaceRole.DesktopComponentHost] = new(
+ MaterialSurfaceRole.DesktopComponentHost,
+ Color.Parse("#FF141C24"),
+ Color.Parse("#44557799"),
+ 20,
+ 0.90),
+ [MaterialSurfaceRole.OverlayPanel] = new(
+ MaterialSurfaceRole.OverlayPanel,
+ Color.Parse("#FF202A34"),
+ Color.Parse("#556688AA"),
+ 24,
+ 0.88)
+ };
+
+ return new MaterialColorSnapshot(
+ IsNightMode: true,
+ ThemeColorMode: ThemeAppearanceValues.ColorModeSeedMonet,
+ ThemeWallpaperColorSource: ThemeAppearanceValues.WallpaperColorSourceAuto,
+ ColorSourceKind: MaterialColorSourceKind.CustomSeed,
+ ResolvedSeedSource: "user_color",
+ CornerRadiusTokens: new AppearanceCornerRadiusTokens(
+ new CornerRadius(2),
+ new CornerRadius(4),
+ new CornerRadius(6),
+ new CornerRadius(8),
+ new CornerRadius(10),
+ new CornerRadius(12),
+ new CornerRadius(14),
+ new CornerRadius(8)),
+ UserThemeColor: seed.ToString(),
+ SelectedWallpaperSeed: seed.ToString(),
+ EffectiveSeedColor: seed,
+ AccentColor: accent,
+ MonetPalette: monetPalette,
+ Palette: palette,
+ WallpaperSeedCandidates: [seed],
+ SystemMaterialMode: ThemeAppearanceValues.MaterialMica,
+ AvailableSystemMaterialModes: [ThemeAppearanceValues.MaterialAuto, ThemeAppearanceValues.MaterialMica],
+ CanChangeSystemMaterial: true,
+ UseSystemChrome: false,
+ ResolvedWallpaperPath: @"C:\wallpaper.png",
+ UseNativeWallpaperChangeEvents: true,
+ NativeWallpaperChangeEventsActive: true,
+ WallpaperPollingActive: true,
+ Surfaces: surfaces);
+ }
+
+ private static AppearanceThemeSnapshot CreateThemeSnapshot()
+ {
+ var seed = Color.Parse("#FF456789");
+ var monetPalette = new MonetPalette(
+ [seed],
+ seed,
+ Color.Parse("#FF667788"),
+ Color.Parse("#FF8899AA"),
+ Color.Parse("#FF112233"),
+ Color.Parse("#FF334455"),
+ Color.Parse("#FF556677"));
+
+ return new AppearanceThemeSnapshot(
+ IsNightMode: false,
+ ThemeColorMode: ThemeAppearanceValues.ColorModeWallpaperMonet,
+ UserThemeColor: "#FF456789",
+ SelectedWallpaperSeed: "#FF456789",
+ CornerRadiusStyle: GlobalAppearanceSettings.CornerRadiusStyleRounded,
+ CornerRadiusTokens: new AppearanceCornerRadiusTokens(
+ new CornerRadius(1),
+ new CornerRadius(2),
+ new CornerRadius(3),
+ new CornerRadius(4),
+ new CornerRadius(5),
+ new CornerRadius(6),
+ new CornerRadius(7),
+ new CornerRadius(8)),
+ ResolvedSeedSource: "theme-source",
+ MonetPalette: monetPalette,
+ AccentColor: Color.Parse("#FF556677"),
+ EffectiveSeedColor: seed,
+ WallpaperSeedCandidates: [seed],
+ SystemMaterialMode: ThemeAppearanceValues.MaterialAcrylic,
+ AvailableSystemMaterialModes: [ThemeAppearanceValues.MaterialAuto, ThemeAppearanceValues.MaterialAcrylic],
+ CanChangeSystemMaterial: true,
+ UseSystemChrome: true,
+ ResolvedWallpaperPath: @"C:\theme-wallpaper.png",
+ ThemeWallpaperColorSource: ThemeAppearanceValues.WallpaperColorSourceSystem,
+ UseNativeWallpaperChangeEvents: false);
+ }
+
+ private sealed class TrackingServiceProvider : IServiceProvider
+ {
+ private readonly Dictionary _services = new();
+
+ public TrackingServiceProvider(IMaterialColorService materialColorService, IAppearanceThemeService appearanceThemeService)
+ {
+ _services[typeof(IMaterialColorService)] = materialColorService;
+ _services[typeof(IAppearanceThemeService)] = appearanceThemeService;
+ }
+
+ public object? GetService(Type serviceType)
+ {
+ return _services.TryGetValue(serviceType, out var service) ? service : null;
+ }
+ }
+
+ private sealed class TrackingMaterialColorService(MaterialColorSnapshot snapshot) : IMaterialColorService
+ {
+ public int GetMaterialColorSnapshotCalls { get; private set; }
+
+ public MaterialColorSnapshot GetMaterialColorSnapshot()
+ {
+ GetMaterialColorSnapshotCalls++;
+ return snapshot;
+ }
+
+ public MaterialColorSnapshot BuildMaterialColorPreview(ThemeAppearanceSettingsState pendingState)
+ {
+ throw new NotSupportedException();
+ }
+
+ public event EventHandler? MaterialColorChanged
+ {
+ add { }
+ remove { }
+ }
+
+ public void ApplyThemeResources(IResourceDictionary resources)
+ {
+ throw new NotSupportedException();
+ }
+
+ public MaterialSurfaceSnapshot GetSurface(MaterialSurfaceRole role)
+ {
+ throw new NotSupportedException();
+ }
+
+ public void ApplyWindowMaterial(Window window, MaterialSurfaceRole role)
+ {
+ throw new NotSupportedException();
+ }
+
+ public void RefreshWallpaperColors()
+ {
+ throw new NotSupportedException();
+ }
+ }
+
+ private sealed class TrackingAppearanceThemeService(AppearanceThemeSnapshot snapshot) : IAppearanceThemeService
+ {
+ public int GetCurrentCalls { get; private set; }
+
+ public AppearanceThemeSnapshot GetCurrent()
+ {
+ GetCurrentCalls++;
+ return snapshot;
+ }
+
+ public AppearanceThemeSnapshot BuildPreview(ThemeAppearanceSettingsState pendingState)
+ {
+ throw new NotSupportedException();
+ }
+
+ public event EventHandler? Changed
+ {
+ add { }
+ remove { }
+ }
+
+ public void ApplyThemeResources(IResourceDictionary resources)
+ {
+ throw new NotSupportedException();
+ }
+
+ public AppearanceMaterialSurface GetMaterialSurface(MaterialSurfaceRole role)
+ {
+ throw new NotSupportedException();
+ }
+
+ public void ApplyWindowMaterial(Window window, MaterialSurfaceRole role)
+ {
+ throw new NotSupportedException();
+ }
+ }
+}
diff --git a/LanMountainDesktop.Tests/SettingsCatalogServiceTests.cs b/LanMountainDesktop.Tests/SettingsCatalogServiceTests.cs
new file mode 100644
index 0000000..17cc2ac
--- /dev/null
+++ b/LanMountainDesktop.Tests/SettingsCatalogServiceTests.cs
@@ -0,0 +1,39 @@
+using System.Linq;
+using LanMountainDesktop.PluginSdk;
+using LanMountainDesktop.Services.Settings;
+using Xunit;
+
+namespace LanMountainDesktop.Tests;
+
+public sealed class SettingsCatalogServiceTests
+{
+ [Fact]
+ public void BuiltInAppSectionsIncludeIndependentMaterialColorAndWallpaperEntries()
+ {
+ var catalog = new SettingsCatalogService();
+
+ var sections = catalog.GetSections(SettingsScope.App).ToList();
+
+ Assert.Equal(
+ [
+ "general",
+ "material-color",
+ "appearance",
+ "wallpaper",
+ "about"
+ ],
+ sections.Select(section => section.Id));
+
+ var materialColor = sections.Single(section => section.Id == "material-color");
+ Assert.Equal(SettingsCategories.Appearance, materialColor.Category);
+ Assert.Equal(SettingsScope.App, materialColor.Scope);
+ Assert.Equal("settings.material_color.title", materialColor.TitleLocalizationKey);
+ Assert.Equal("Color", materialColor.IconKey);
+
+ var wallpaper = sections.Single(section => section.Id == "wallpaper");
+ Assert.Equal(SettingsCategories.Appearance, wallpaper.Category);
+ Assert.Equal(SettingsScope.App, wallpaper.Scope);
+ Assert.Equal("settings.wallpaper.title", wallpaper.TitleLocalizationKey);
+ Assert.Equal("Image", wallpaper.IconKey);
+ }
+}
diff --git a/LanMountainDesktop.Tests/WallpaperSettingsPageViewModelTests.cs b/LanMountainDesktop.Tests/WallpaperSettingsPageViewModelTests.cs
new file mode 100644
index 0000000..f6719f8
--- /dev/null
+++ b/LanMountainDesktop.Tests/WallpaperSettingsPageViewModelTests.cs
@@ -0,0 +1,169 @@
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Avalonia.Media;
+using LanMountainDesktop.Models;
+using LanMountainDesktop.PluginSdk;
+using LanMountainDesktop.Services;
+using LanMountainDesktop.Services.Settings;
+using LanMountainDesktop.Settings.Core;
+using LanMountainDesktop.ViewModels;
+using Xunit;
+
+namespace LanMountainDesktop.Tests;
+
+public sealed class WallpaperSettingsPageViewModelTests
+{
+ [Fact]
+ public void CustomColorRoundTripsThroughWallpaperColorField()
+ {
+ var initialWallpaperState = new WallpaperSettingsState(
+ WallpaperPath: null,
+ Type: "SolidColor",
+ Color: "#FF123456",
+ Placement: "Fill",
+ SystemWallpaperRefreshIntervalSeconds: 900);
+ var initialThemeState = CreateThemeState();
+ var facade = new FakeSettingsFacade(initialWallpaperState, initialThemeState);
+ var viewModel = new WallpaperSettingsPageViewModel(facade);
+
+ Assert.Equal("#FF123456", viewModel.SelectedColor);
+ Assert.Equal(Color.Parse("#FF123456"), viewModel.CustomColor);
+
+ viewModel.CustomColor = Color.Parse("#FFABCDEF");
+
+ Assert.Equal("#FFABCDEF", facade.WallpaperState.Color);
+ Assert.Equal(900, facade.WallpaperState.SystemWallpaperRefreshIntervalSeconds);
+ Assert.Equal(0, facade.ThemeSaveCount);
+ Assert.Equal(ThemeAppearanceValues.MaterialMica, facade.ThemeState.SystemMaterialMode);
+ Assert.Equal("#FF998877", facade.ThemeState.SelectedWallpaperSeed);
+
+ var reloaded = new WallpaperSettingsPageViewModel(facade);
+ Assert.Equal("#FFABCDEF", reloaded.SelectedColor);
+ Assert.Equal(Color.Parse("#FFABCDEF"), reloaded.CustomColor);
+ }
+
+ [Fact]
+ public void SavingWallpaperChanges_DoesNotTouchThemeMaterialFields()
+ {
+ var initialWallpaperState = new WallpaperSettingsState(
+ WallpaperPath: @"C:\\wallpaper\\forest.png",
+ Type: "Image",
+ Color: "#FF123456",
+ Placement: "Fill",
+ SystemWallpaperRefreshIntervalSeconds: 1800);
+ var initialThemeState = CreateThemeState();
+ var facade = new FakeSettingsFacade(initialWallpaperState, initialThemeState);
+ var viewModel = new WallpaperSettingsPageViewModel(facade);
+
+ viewModel.SelectedWallpaperPlacement = viewModel.WallpaperPlacements.Single(option => option.Value == "Tile");
+
+ Assert.Equal(0, facade.ThemeSaveCount);
+ Assert.Equal(ThemeAppearanceValues.MaterialMica, facade.ThemeState.SystemMaterialMode);
+ Assert.Equal("#FF998877", facade.ThemeState.SelectedWallpaperSeed);
+ Assert.Equal(1800, facade.WallpaperState.SystemWallpaperRefreshIntervalSeconds);
+ }
+
+ private static ThemeAppearanceSettingsState CreateThemeState()
+ {
+ return new ThemeAppearanceSettingsState(
+ IsNightMode: false,
+ ThemeColor: "#FF445566",
+ UseSystemChrome: true,
+ CornerRadiusStyle: GlobalAppearanceSettings.CornerRadiusStyleRounded,
+ ThemeColorMode: ThemeAppearanceValues.ColorModeWallpaperMonet,
+ SystemMaterialMode: ThemeAppearanceValues.MaterialMica,
+ SelectedWallpaperSeed: "#FF998877",
+ ThemeMode: ThemeAppearanceValues.ThemeModeLight,
+ ThemeWallpaperColorSource: ThemeAppearanceValues.WallpaperColorSourceAuto,
+ UseNativeWallpaperChangeEvents: true);
+ }
+
+ private sealed class FakeSettingsFacade(
+ WallpaperSettingsState wallpaperState,
+ ThemeAppearanceSettingsState themeState) : ISettingsFacadeService
+ {
+ private readonly FakeWallpaperSettingsService _wallpaper = new(wallpaperState);
+ private readonly FakeThemeAppearanceService _theme = new(themeState);
+ private readonly FakeRegionSettingsService _region = new();
+
+ public WallpaperSettingsState WallpaperState => _wallpaper.State;
+ public ThemeAppearanceSettingsState ThemeState => _theme.State;
+ public int ThemeSaveCount => _theme.SaveCount;
+
+ public ISettingsService Settings => throw new NotSupportedException();
+ public ISettingsCatalog Catalog => throw new NotSupportedException();
+ public IGridSettingsService Grid => throw new NotSupportedException();
+ public IWallpaperSettingsService Wallpaper => _wallpaper;
+ public IWallpaperMediaService WallpaperMedia => new FakeWallpaperMediaService();
+ public IThemeAppearanceService Theme => _theme;
+ public IStatusBarSettingsService StatusBar => throw new NotSupportedException();
+ public ITextCapsuleSettingsService TextCapsule => throw new NotSupportedException();
+ public IWeatherSettingsService Weather => throw new NotSupportedException();
+ public IRegionSettingsService Region => _region;
+ public IPrivacySettingsService Privacy => throw new NotSupportedException();
+ public IUpdateSettingsService Update => throw new NotSupportedException();
+ public ILauncherCatalogService LauncherCatalog => throw new NotSupportedException();
+ public ILauncherPolicyService LauncherPolicy => throw new NotSupportedException();
+ public IPluginManagementSettingsService PluginManagement => throw new NotSupportedException();
+ public IPluginCatalogSettingsService PluginCatalog => throw new NotSupportedException();
+ public IApplicationInfoService ApplicationInfo => throw new NotSupportedException();
+ }
+
+ private sealed class FakeWallpaperSettingsService(WallpaperSettingsState state) : IWallpaperSettingsService
+ {
+ public WallpaperSettingsState State { get; private set; } = state;
+
+ public WallpaperSettingsState Get() => State;
+
+ public void Save(WallpaperSettingsState state)
+ {
+ State = state;
+ }
+ }
+
+ private sealed class FakeThemeAppearanceService(ThemeAppearanceSettingsState state) : IThemeAppearanceService
+ {
+ public ThemeAppearanceSettingsState State { get; private set; } = state;
+
+ public int SaveCount { get; private set; }
+
+ public ThemeAppearanceSettingsState Get() => State;
+
+ public void Save(ThemeAppearanceSettingsState state)
+ {
+ SaveCount++;
+ State = state;
+ }
+
+ public MonetPalette BuildPalette(bool nightMode, string? wallpaperPath, string? preferredSeedColor = null)
+ {
+ var seed = Color.Parse(preferredSeedColor ?? "#FF3B82F6");
+ return new MonetPalette([seed], seed, seed, seed, seed, seed, seed);
+ }
+ }
+
+ private sealed class FakeRegionSettingsService : IRegionSettingsService
+ {
+ public RegionSettingsState Get() => new("en-US", null);
+
+ public void Save(RegionSettingsState state)
+ {
+ _ = state;
+ }
+
+ public TimeZoneService GetTimeZoneService() => new();
+ }
+
+ private sealed class FakeWallpaperMediaService : IWallpaperMediaService
+ {
+ public WallpaperMediaType DetectMediaType(string? path) => WallpaperMediaType.None;
+
+ public Task ImportAssetAsync(string sourcePath, CancellationToken cancellationToken = default)
+ {
+ _ = sourcePath;
+ _ = cancellationToken;
+ return Task.FromResult(null);
+ }
+ }
+}
diff --git a/LanMountainDesktop/ComponentSystem/ComponentColorSchemeHelper.cs b/LanMountainDesktop/ComponentSystem/ComponentColorSchemeHelper.cs
index 2b68ddd..38ff387 100644
--- a/LanMountainDesktop/ComponentSystem/ComponentColorSchemeHelper.cs
+++ b/LanMountainDesktop/ComponentSystem/ComponentColorSchemeHelper.cs
@@ -1,4 +1,5 @@
using System;
+using LanMountainDesktop.Models;
using LanMountainDesktop.Services;
using LanMountainDesktop.Views;
@@ -25,13 +26,18 @@ public static class ComponentColorSchemeHelper
{
try
{
- var service = HostAppearanceThemeProvider.GetOrCreate();
- var appearance = service.GetCurrent();
- return appearance?.ThemeColorMode ?? ThemeAppearanceValues.ColorModeDefaultNeutral;
+ var service = HostMaterialColorProvider.GetOrCreate();
+ return service.GetMaterialColorSnapshot().ThemeColorMode;
}
catch
{
return ThemeAppearanceValues.ColorModeDefaultNeutral;
}
}
+
+ public static string GetCurrentGlobalThemeColorMode(MaterialColorSnapshot materialColorSnapshot)
+ {
+ ArgumentNullException.ThrowIfNull(materialColorSnapshot);
+ return materialColorSnapshot.ThemeColorMode;
+ }
}
diff --git a/LanMountainDesktop/Localization/en-US.json b/LanMountainDesktop/Localization/en-US.json
index 602b869..3148d44 100644
--- a/LanMountainDesktop/Localization/en-US.json
+++ b/LanMountainDesktop/Localization/en-US.json
@@ -38,27 +38,25 @@
"settings.wallpaper.title": "Wallpaper",
"settings.wallpaper.description": "Pick an image or video to apply as the app window wallpaper immediately.",
"settings.wallpaper.current_label": "Current Wallpaper",
- "settings.wallpaper.type_label": "Wallpaper Type",
- "settings.wallpaper.type.image": "Image",
- "settings.wallpaper.type.solid_color": "Solid Color",
- "settings.wallpaper.type.system": "System Wallpaper",
- "settings.wallpaper.system.label": "System Wallpaper",
- "settings.wallpaper.system.unavailable": "Unable to read system wallpaper",
- "settings.wallpaper.refresh_interval": "Refresh Interval",
- "settings.wallpaper.refresh_now": "Refresh Now",
- "settings.wallpaper.refresh.30s": "30 seconds",
- "settings.wallpaper.refresh.1m": "1 minute",
- "settings.wallpaper.refresh.5m": "5 minutes",
- "settings.wallpaper.refresh.10m": "10 minutes",
- "settings.wallpaper.refresh.15m": "15 minutes",
- "settings.wallpaper.refresh.30m": "30 minutes",
- "settings.wallpaper.refresh.1h": "1 hour",
- "settings.wallpaper.refresh.2h": "2 hours",
- "settings.wallpaper.refresh.4h": "4 hours",
- "settings.wallpaper.refresh.8h": "8 hours",
- "settings.wallpaper.refresh.12h": "12 hours",
- "settings.wallpaper.refresh.24h": "24 hours",
- "settings.wallpaper.color_label": "Wallpaper Color",
+"settings.wallpaper.type_label": "Wallpaper Type",
+"settings.wallpaper.type.image": "Image",
+"settings.wallpaper.type.solid_color": "Solid Color",
+"settings.wallpaper.type.system": "System Wallpaper",
+"settings.wallpaper.refresh_interval": "Refresh Interval",
+"settings.wallpaper.refresh_now": "Refresh Now",
+"settings.wallpaper.refresh.30s": "30 seconds",
+"settings.wallpaper.refresh.1m": "1 minute",
+"settings.wallpaper.refresh.5m": "5 minutes",
+"settings.wallpaper.refresh.10m": "10 minutes",
+"settings.wallpaper.refresh.15m": "15 minutes",
+"settings.wallpaper.refresh.30m": "30 minutes",
+"settings.wallpaper.refresh.1h": "1 hour",
+"settings.wallpaper.refresh.2h": "2 hours",
+"settings.wallpaper.refresh.4h": "4 hours",
+"settings.wallpaper.refresh.8h": "8 hours",
+"settings.wallpaper.refresh.12h": "12 hours",
+"settings.wallpaper.refresh.24h": "24 hours",
+"settings.wallpaper.color_label": "Wallpaper Color",
"settings.wallpaper.placement_label": "Placement",
"settings.wallpaper.placement_desc": "Adjust how the image fills the desktop.",
"settings.wallpaper.pick_button": "Browse Files",
@@ -392,6 +390,14 @@
"settings.appearance.preview.apply_seed": "Apply",
"settings.appearance.preview.wallpaper_candidates": "Wallpaper seed candidates",
"settings.appearance.preview.wallpaper_current": "Current",
+ "settings.material_color.preview.wallpaper_current": "Current",
+ "settings.material_color.theme_color_mode.neutral": "Default neutral",
+ "settings.material_color.theme_color_mode.user": "User theme color Monet",
+ "settings.material_color.theme_color_mode.wallpaper": "Wallpaper Monet",
+ "settings.material_color.system_material.auto": "Auto (recommended)",
+ "settings.material_color.system_material.none": "None",
+ "settings.material_color.system_material.mica": "Mica",
+ "settings.material_color.system_material.acrylic": "Acrylic",
"settings.wallpaper.placement.fill": "Fill",
"settings.wallpaper.placement.fit": "Fit",
"settings.wallpaper.placement.stretch": "Stretch",
diff --git a/LanMountainDesktop/Localization/ja-JP.json b/LanMountainDesktop/Localization/ja-JP.json
index eb50f9d..6ea0f11 100644
--- a/LanMountainDesktop/Localization/ja-JP.json
+++ b/LanMountainDesktop/Localization/ja-JP.json
@@ -38,27 +38,25 @@
"settings.wallpaper.title": "壁紙",
"settings.wallpaper.description": "画像または動画を選択して、アプリウィンドウの壁紙としてすぐに適用します。",
"settings.wallpaper.current_label": "現在の壁紙",
- "settings.wallpaper.type_label": "壁紙タイプ",
- "settings.wallpaper.type.image": "画像",
- "settings.wallpaper.type.solid_color": "単色",
- "settings.wallpaper.type.system": "システム壁紙",
- "settings.wallpaper.system.label": "システム壁紙",
- "settings.wallpaper.system.unavailable": "システム壁紙を読み込めません",
- "settings.wallpaper.refresh_interval": "更新間隔",
- "settings.wallpaper.refresh_now": "今すぐ更新",
- "settings.wallpaper.refresh.30s": "30秒",
- "settings.wallpaper.refresh.1m": "1分",
- "settings.wallpaper.refresh.5m": "5分",
- "settings.wallpaper.refresh.10m": "10分",
- "settings.wallpaper.refresh.15m": "15分",
- "settings.wallpaper.refresh.30m": "30分",
- "settings.wallpaper.refresh.1h": "1時間",
- "settings.wallpaper.refresh.2h": "2時間",
- "settings.wallpaper.refresh.4h": "4時間",
- "settings.wallpaper.refresh.8h": "8時間",
- "settings.wallpaper.refresh.12h": "12時間",
- "settings.wallpaper.refresh.24h": "24時間",
- "settings.wallpaper.color_label": "壁紙の色",
+"settings.wallpaper.type_label": "壁紙タイプ",
+"settings.wallpaper.type.image": "画像",
+"settings.wallpaper.type.solid_color": "単色",
+"settings.wallpaper.type.system": "システム壁紙",
+"settings.wallpaper.refresh_interval": "更新間隔",
+"settings.wallpaper.refresh_now": "今すぐ更新",
+"settings.wallpaper.refresh.30s": "30秒",
+"settings.wallpaper.refresh.1m": "1分",
+"settings.wallpaper.refresh.5m": "5分",
+"settings.wallpaper.refresh.10m": "10分",
+"settings.wallpaper.refresh.15m": "15分",
+"settings.wallpaper.refresh.30m": "30分",
+"settings.wallpaper.refresh.1h": "1時間",
+"settings.wallpaper.refresh.2h": "2時間",
+"settings.wallpaper.refresh.4h": "4時間",
+"settings.wallpaper.refresh.8h": "8時間",
+"settings.wallpaper.refresh.12h": "12時間",
+"settings.wallpaper.refresh.24h": "24時間",
+"settings.wallpaper.color_label": "壁紙の色",
"settings.wallpaper.placement_label": "配置",
"settings.wallpaper.placement_desc": "画像がデスクトップにどのように表示されるかを調整します。",
"settings.wallpaper.pick_button": "ファイルを参照",
@@ -328,7 +326,14 @@
"settings.appearance.preview.apply_seed": "適用",
"settings.appearance.preview.wallpaper_candidates": "壁紙シード候補",
"settings.appearance.preview.wallpaper_current": "現在",
- "settings.wallpaper.placement.fill": "フィル",
+ "settings.material_color.preview.wallpaper_current": "Current",
+ "settings.material_color.theme_color_mode.neutral": "Default neutral",
+ "settings.material_color.theme_color_mode.user": "User theme color Monet",
+ "settings.material_color.theme_color_mode.wallpaper": "Wallpaper Monet",
+ "settings.material_color.system_material.auto": "Auto (recommended)",
+ "settings.material_color.system_material.none": "None",
+ "settings.material_color.system_material.mica": "Mica",
+ "settings.material_color.system_material.acrylic": "Acrylic", "settings.wallpaper.placement.fill": "フィル",
"settings.wallpaper.placement.fit": "フィット",
"settings.wallpaper.placement.stretch": "ストレッチ",
"settings.wallpaper.placement.center": "中央",
diff --git a/LanMountainDesktop/Localization/ko-KR.json b/LanMountainDesktop/Localization/ko-KR.json
index e8b9640..f8c71f5 100644
--- a/LanMountainDesktop/Localization/ko-KR.json
+++ b/LanMountainDesktop/Localization/ko-KR.json
@@ -374,7 +374,14 @@
"settings.appearance.preview.apply_seed": "적용",
"settings.appearance.preview.wallpaper_candidates": "배경화면 후보 테마 색상",
"settings.appearance.preview.wallpaper_current": "현재",
- "settings.wallpaper.placement.fill": "채우기",
+ "settings.material_color.preview.wallpaper_current": "Current",
+ "settings.material_color.theme_color_mode.neutral": "Default neutral",
+ "settings.material_color.theme_color_mode.user": "User theme color Monet",
+ "settings.material_color.theme_color_mode.wallpaper": "Wallpaper Monet",
+ "settings.material_color.system_material.auto": "Auto (recommended)",
+ "settings.material_color.system_material.none": "None",
+ "settings.material_color.system_material.mica": "Mica",
+ "settings.material_color.system_material.acrylic": "Acrylic", "settings.wallpaper.placement.fill": "채우기",
"settings.wallpaper.placement.fit": "맞추기",
"settings.wallpaper.placement.stretch": "늘리기",
"settings.wallpaper.placement.center": "가운데",
diff --git a/LanMountainDesktop/Localization/zh-CN.json b/LanMountainDesktop/Localization/zh-CN.json
index c21d16c..6d6ab51 100644
--- a/LanMountainDesktop/Localization/zh-CN.json
+++ b/LanMountainDesktop/Localization/zh-CN.json
@@ -38,27 +38,25 @@
"settings.wallpaper.title": "壁纸",
"settings.wallpaper.description": "选择图片后可立即设为应用窗口壁纸。",
"settings.wallpaper.current_label": "当前壁纸",
- "settings.wallpaper.type_label": "壁纸类型",
- "settings.wallpaper.type.image": "图片",
- "settings.wallpaper.type.solid_color": "纯色",
- "settings.wallpaper.type.system": "系统壁纸",
- "settings.wallpaper.system.label": "系统壁纸",
- "settings.wallpaper.system.unavailable": "无法读取系统壁纸",
- "settings.wallpaper.refresh_interval": "刷新频率",
- "settings.wallpaper.refresh_now": "立即刷新",
- "settings.wallpaper.refresh.30s": "30 秒",
- "settings.wallpaper.refresh.1m": "1 分钟",
- "settings.wallpaper.refresh.5m": "5 分钟",
- "settings.wallpaper.refresh.10m": "10 分钟",
- "settings.wallpaper.refresh.15m": "15 分钟",
- "settings.wallpaper.refresh.30m": "30 分钟",
- "settings.wallpaper.refresh.1h": "1 小时",
- "settings.wallpaper.refresh.2h": "2 小时",
- "settings.wallpaper.refresh.4h": "4 小时",
- "settings.wallpaper.refresh.8h": "8 小时",
- "settings.wallpaper.refresh.12h": "12 小时",
- "settings.wallpaper.refresh.24h": "24 小时",
- "settings.wallpaper.color_label": "壁纸颜色",
+"settings.wallpaper.type_label": "壁纸类型",
+"settings.wallpaper.type.image": "图片",
+"settings.wallpaper.type.solid_color": "纯色",
+"settings.wallpaper.type.system": "系统壁纸",
+"settings.wallpaper.refresh_interval": "刷新频率",
+"settings.wallpaper.refresh_now": "立即刷新",
+"settings.wallpaper.refresh.30s": "30 秒",
+"settings.wallpaper.refresh.1m": "1 分钟",
+"settings.wallpaper.refresh.5m": "5 分钟",
+"settings.wallpaper.refresh.10m": "10 分钟",
+"settings.wallpaper.refresh.15m": "15 分钟",
+"settings.wallpaper.refresh.30m": "30 分钟",
+"settings.wallpaper.refresh.1h": "1 小时",
+"settings.wallpaper.refresh.2h": "2 小时",
+"settings.wallpaper.refresh.4h": "4 小时",
+"settings.wallpaper.refresh.8h": "8 小时",
+"settings.wallpaper.refresh.12h": "12 小时",
+"settings.wallpaper.refresh.24h": "24 小时",
+"settings.wallpaper.color_label": "壁纸颜色",
"settings.wallpaper.custom_color_tooltip": "自定义颜色",
"settings.wallpaper.custom_color_apply": "应用",
"settings.wallpaper.placement_label": "显示方式",
@@ -393,7 +391,14 @@
"settings.appearance.preview.apply_seed": "应用",
"settings.appearance.preview.wallpaper_candidates": "壁纸候选主题色",
"settings.appearance.preview.wallpaper_current": "当前",
- "settings.wallpaper.placement.fill": "填充",
+ "settings.material_color.preview.wallpaper_current": "Current",
+ "settings.material_color.theme_color_mode.neutral": "Default neutral",
+ "settings.material_color.theme_color_mode.user": "User theme color Monet",
+ "settings.material_color.theme_color_mode.wallpaper": "Wallpaper Monet",
+ "settings.material_color.system_material.auto": "Auto (recommended)",
+ "settings.material_color.system_material.none": "None",
+ "settings.material_color.system_material.mica": "Mica",
+ "settings.material_color.system_material.acrylic": "Acrylic", "settings.wallpaper.placement.fill": "填充",
"settings.wallpaper.placement.fit": "适应",
"settings.wallpaper.placement.stretch": "拉伸",
"settings.wallpaper.placement.center": "居中",
diff --git a/LanMountainDesktop/Services/AppearanceThemeService.cs b/LanMountainDesktop/Services/AppearanceThemeService.cs
index 96f5fd7..83f975a 100644
--- a/LanMountainDesktop/Services/AppearanceThemeService.cs
+++ b/LanMountainDesktop/Services/AppearanceThemeService.cs
@@ -75,13 +75,6 @@ public interface IAppearanceThemeService
void ApplyWindowMaterial(Window window, MaterialSurfaceRole role);
}
-internal interface ISystemWallpaperService
-{
- bool IsSupported { get; }
-
- string? GetWallpaperPath();
-}
-
internal interface IWindowMaterialService
{
IReadOnlyList GetAvailableModes();
@@ -96,1320 +89,91 @@ internal interface IMaterialSurfaceService
AppearanceMaterialSurface GetSurface(ThemeColorContext context, MaterialSurfaceRole role);
}
-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 SystemWallpaperService : ISystemWallpaperService
+internal sealed class AppearanceThemeService : IAppearanceThemeService, IDisposable
{
- public bool IsSupported => OperatingSystem.IsWindows();
+ private readonly MaterialColorService _materialColorService;
- public string? GetWallpaperPath()
+ public AppearanceThemeService(MaterialColorService materialColorService)
{
- 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 => GetAvailableModes().Count > 1;
-
- public IReadOnlyList GetAvailableModes()
- {
- return GetSupportProfile() switch
- {
- WindowMaterialSupportProfile.FullSwitching =>
- [
- ThemeAppearanceValues.MaterialAuto,
- ThemeAppearanceValues.MaterialNone,
- ThemeAppearanceValues.MaterialMica,
- ThemeAppearanceValues.MaterialAcrylic
- ],
- WindowMaterialSupportProfile.FixedMica =>
- [
- ThemeAppearanceValues.MaterialAuto,
- ThemeAppearanceValues.MaterialNone,
- ThemeAppearanceValues.MaterialMica
- ],
- WindowMaterialSupportProfile.FixedAcrylic =>
- [
- ThemeAppearanceValues.MaterialAuto,
- ThemeAppearanceValues.MaterialNone,
- ThemeAppearanceValues.MaterialAcrylic
- ],
- _ =>
- [
- ThemeAppearanceValues.MaterialAuto,
- ThemeAppearanceValues.MaterialNone
- ]
- };
- }
-
- public void Apply(Window window, string materialMode)
- {
- ArgumentNullException.ThrowIfNull(window);
-
- var normalizedMode = ThemeAppearanceValues.NormalizeSystemMaterialMode(materialMode);
- var supportProfile = GetSupportProfile();
- var effectiveMode = normalizedMode == ThemeAppearanceValues.MaterialAuto
- ? ResolveAutoMaterialMode(supportProfile)
- : normalizedMode;
-
- if (effectiveMode == ThemeAppearanceValues.MaterialNone)
- {
- window.Background = Brushes.White;
- window.TransparencyLevelHint = [WindowTransparencyLevel.None];
- return;
- }
-
- window.Background = Brushes.Transparent;
-
- if (supportProfile == WindowMaterialSupportProfile.NoneOnly)
- {
- window.TransparencyLevelHint =
- [
- WindowTransparencyLevel.None
- ];
- return;
- }
-
- window.TransparencyLevelHint = normalizedMode == ThemeAppearanceValues.MaterialAuto
- ? ResolveAutoTransparencyLevels(supportProfile)
- : effectiveMode switch
- {
- ThemeAppearanceValues.MaterialMica =>
- [
- WindowTransparencyLevel.Mica,
- WindowTransparencyLevel.Blur,
- WindowTransparencyLevel.None
- ],
- ThemeAppearanceValues.MaterialAcrylic =>
- [
- WindowTransparencyLevel.AcrylicBlur,
- WindowTransparencyLevel.Blur,
- WindowTransparencyLevel.None
- ],
- _ =>
- [
- WindowTransparencyLevel.None
- ]
- };
- }
-
- private static string ResolveAutoMaterialMode(WindowMaterialSupportProfile supportProfile)
- {
- return supportProfile switch
- {
- WindowMaterialSupportProfile.FullSwitching or WindowMaterialSupportProfile.FixedMica =>
- ThemeAppearanceValues.MaterialMica,
- WindowMaterialSupportProfile.FixedAcrylic =>
- ThemeAppearanceValues.MaterialAcrylic,
- _ => ThemeAppearanceValues.MaterialNone
- };
- }
-
- private static IReadOnlyList ResolveAutoTransparencyLevels(WindowMaterialSupportProfile supportProfile)
- {
- return supportProfile switch
- {
- WindowMaterialSupportProfile.FullSwitching or WindowMaterialSupportProfile.FixedMica =>
- [
- WindowTransparencyLevel.Mica,
- WindowTransparencyLevel.AcrylicBlur,
- WindowTransparencyLevel.Blur,
- WindowTransparencyLevel.None
- ],
- WindowMaterialSupportProfile.FixedAcrylic =>
- [
- 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.ResolveEffectiveSystemMaterialMode(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)
- {
- // Settings 根层(如 RootGrid)叠在 Transparent + Mica/Acrylic 上:过高 alpha 会完全盖住系统 backdrop。
- // 保持非 None 下较低 alpha;None 仍用不透明白底等价。BlurRadius=0(由 DWM 提供模糊)。
- if (role == MaterialSurfaceRole.SettingsWindowBackground)
- {
- return materialMode switch
- {
- ThemeAppearanceValues.MaterialAcrylic => (
- 0.20,
- 0.14,
- isNightMode ? (byte)0x8E : (byte)0x96,
- 0),
- ThemeAppearanceValues.MaterialMica => (
- 0.14,
- 0.08,
- isNightMode ? (byte)0x9E : (byte)0xA6,
- 0),
- _ => (0.08, 0.05, (byte)0xFF, 0)
- };
- }
-
- 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, IMaterialColorService, 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 MonetColorService _monetColorService = new();
- private string _liveThemeColorMode;
- private string _liveSystemMaterialMode;
- private string? _liveSelectedWallpaperSeed;
- private string _liveThemeWallpaperColorSource;
- private bool _liveUseNativeWallpaperChangeEvents;
- private readonly object _paletteGate = new();
- private readonly Dictionary _wallpaperSeedCache = new(StringComparer.OrdinalIgnoreCase);
- private readonly HashSet _pendingWallpaperSeedKeys = new(StringComparer.OrdinalIgnoreCase);
- private Timer? _systemWallpaperPollTimer;
- private string? _lastObservedWallpaperSourceKey;
- private bool _nativeWallpaperEventsActive;
- private bool _wallpaperPollingActive;
-
- public AppearanceThemeService(
- ISettingsFacadeService settingsFacade,
- ISystemWallpaperService systemWallpaperService,
- IWindowMaterialService windowMaterialService,
- IMaterialSurfaceService materialSurfaceService)
- {
- _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));
- var initialThemeState = _settingsFacade.Theme.Get();
- _liveThemeColorMode = ThemeAppearanceValues.NormalizeThemeColorMode(
- initialThemeState.ThemeColorMode,
- initialThemeState.ThemeColor);
- _liveSystemMaterialMode = ResolveSupportedMaterialMode(initialThemeState.SystemMaterialMode);
- _liveSelectedWallpaperSeed = initialThemeState.SelectedWallpaperSeed;
- _liveThemeWallpaperColorSource = ThemeAppearanceValues.NormalizeWallpaperColorSource(initialThemeState.ThemeWallpaperColorSource);
- _liveUseNativeWallpaperChangeEvents = initialThemeState.UseNativeWallpaperChangeEvents;
- _settingsFacade.Settings.Changed += OnSettingsChanged;
- ConfigureSystemWallpaperMonitoring(initialThemeState);
+ _materialColorService = materialColorService ?? throw new ArgumentNullException(nameof(materialColorService));
+ _materialColorService.AppearanceThemeChanged += OnAppearanceThemeChanged;
}
public event EventHandler? Changed;
- public event EventHandler? MaterialColorChanged;
-
public AppearanceThemeSnapshot GetCurrent()
{
- return BuildCurrentSnapshot(queueWallpaperPaletteBuild: true);
+ return _materialColorService.GetCurrent();
}
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 MaterialColorSnapshot GetMaterialColorSnapshot()
- {
- return CreateMaterialColorSnapshot(GetCurrent());
- }
-
- public MaterialColorSnapshot BuildMaterialColorPreview(ThemeAppearanceSettingsState pendingState)
- {
- return CreateMaterialColorSnapshot(BuildPreview(pendingState));
- }
-
- public MaterialSurfaceSnapshot GetSurface(MaterialSurfaceRole role)
- {
- var surface = GetMaterialSurface(role);
- return new MaterialSurfaceSnapshot(
- role,
- surface.BackgroundColor,
- surface.BorderColor,
- surface.BlurRadius,
- surface.Opacity);
- }
-
- public void RefreshWallpaperColors()
- {
- lock (_paletteGate)
- {
- _wallpaperSeedCache.Clear();
- _pendingWallpaperSeedKeys.Clear();
- _lastObservedWallpaperSourceKey = null;
- }
-
- RaiseChanged(queueWallpaperPaletteBuild: true);
+ return _materialColorService.BuildPreview(pendingState);
}
public void ApplyThemeResources(IResourceDictionary resources)
{
- ArgumentNullException.ThrowIfNull(resources);
-
- var snapshot = GetCurrent();
- var context = CreateThemeContext(snapshot);
- ThemeColorSystemService.ApplyThemeResources(resources, context);
- GlassEffectService.ApplyGlassResources(resources, context);
- resources["DesignCornerRadiusMicro"] = snapshot.CornerRadiusTokens.Micro;
- resources["DesignCornerRadiusXs"] = snapshot.CornerRadiusTokens.Xs;
- resources["DesignCornerRadiusSm"] = snapshot.CornerRadiusTokens.Sm;
- resources["DesignCornerRadiusMd"] = snapshot.CornerRadiusTokens.Md;
- resources["DesignCornerRadiusLg"] = snapshot.CornerRadiusTokens.Lg;
- resources["DesignCornerRadiusXl"] = snapshot.CornerRadiusTokens.Xl;
- resources["DesignCornerRadiusIsland"] = snapshot.CornerRadiusTokens.Island;
- resources["DesignCornerRadiusComponent"] = snapshot.CornerRadiusTokens.Component;
+ _materialColorService.ApplyThemeResources(resources);
}
public AppearanceMaterialSurface GetMaterialSurface(MaterialSurfaceRole role)
{
- var snapshot = GetCurrent();
- return _materialSurfaceService.GetSurface(CreateThemeContext(snapshot), role);
+ return _materialColorService.GetMaterialSurface(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.
- // SettingsWindowBackground 是唯一需要材质与资源同步热切换的宿主角色;其它窗口仍保持「仅创建时」应用以降低风险。
- if (window.IsVisible && role != MaterialSurfaceRole.SettingsWindowBackground)
- {
- 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);
- }
+ _materialColorService.ApplyWindowMaterial(window, role);
}
public void Dispose()
{
- _settingsFacade.Settings.Changed -= OnSettingsChanged;
- StopSystemWallpaperMonitoring();
- _systemWallpaperPollTimer?.Dispose();
- _systemWallpaperPollTimer = null;
+ _materialColorService.AppearanceThemeChanged -= OnAppearanceThemeChanged;
}
- private AppearanceThemeSnapshot BuildCurrentSnapshot(bool queueWallpaperPaletteBuild)
- {
- var themeState = _settingsFacade.Theme.Get();
- return BuildSnapshot(
- themeState,
- _liveThemeColorMode,
- _liveSystemMaterialMode,
- _liveSelectedWallpaperSeed,
- queueWallpaperPaletteBuild);
- }
-
- private void OnSettingsChanged(object? sender, SettingsChangedEvent e)
+ private void OnAppearanceThemeChanged(object? sender, AppearanceThemeSnapshot snapshot)
{
_ = 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) &&
- !changedKeys.Contains(nameof(AppSettingsSnapshot.CornerRadiusStyle), StringComparer.OrdinalIgnoreCase) &&
- !changedKeys.Contains(nameof(AppSettingsSnapshot.ThemeColorMode), StringComparer.OrdinalIgnoreCase) &&
- !changedKeys.Contains(nameof(AppSettingsSnapshot.SystemMaterialMode), StringComparer.OrdinalIgnoreCase) &&
- !changedKeys.Contains(nameof(AppSettingsSnapshot.SelectedWallpaperSeed), StringComparer.OrdinalIgnoreCase) &&
- !changedKeys.Contains(nameof(AppSettingsSnapshot.ThemeWallpaperColorSource), StringComparer.OrdinalIgnoreCase) &&
- !changedKeys.Contains(nameof(AppSettingsSnapshot.UseNativeWallpaperChangeEvents), StringComparer.OrdinalIgnoreCase) &&
- !changedKeys.Contains(nameof(AppSettingsSnapshot.SystemWallpaperRefreshIntervalSeconds), 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;
- }
-
- var latestThemeState = _settingsFacade.Theme.Get();
- _liveThemeColorMode = ThemeAppearanceValues.NormalizeThemeColorMode(
- latestThemeState.ThemeColorMode,
- latestThemeState.ThemeColor);
- _liveSystemMaterialMode = ResolveSupportedMaterialMode(latestThemeState.SystemMaterialMode);
- _liveSelectedWallpaperSeed = latestThemeState.SelectedWallpaperSeed;
- _liveThemeWallpaperColorSource = ThemeAppearanceValues.NormalizeWallpaperColorSource(latestThemeState.ThemeWallpaperColorSource);
- _liveUseNativeWallpaperChangeEvents = latestThemeState.UseNativeWallpaperChangeEvents;
- ConfigureSystemWallpaperMonitoring(latestThemeState);
- RaiseChanged(queueWallpaperPaletteBuild: true);
- }
-
- private AppearanceThemeSnapshot BuildSnapshot(
- ThemeAppearanceSettingsState themeState,
- string themeColorMode,
- string systemMaterialMode,
- string? selectedWallpaperSeed,
- bool queueWallpaperPaletteBuild)
- {
- var availableModes = _windowMaterialService.GetAvailableModes();
- var cornerRadiusStyle = GlobalAppearanceSettings.NormalizeCornerRadiusStyle(themeState.CornerRadiusStyle);
- var cornerRadiusTokens = AppearanceCornerRadiusTokenFactory.Create(cornerRadiusStyle);
- 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,
- ThemeAppearanceValues.NormalizeWallpaperColorSource(themeState.ThemeWallpaperColorSource),
- 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,
- cornerRadiusStyle,
- cornerRadiusTokens,
- resolvedSeedSource,
- palette,
- ResolveAccentColor(themeColorMode, themeState.ThemeColor, palette),
- effectiveSeedColor,
- wallpaperSeedCandidates,
- systemMaterialMode,
- availableModes,
- _windowMaterialService.CanChangeMode,
- themeState.UseSystemChrome,
- resolvedWallpaperPath,
- ThemeAppearanceValues.NormalizeWallpaperColorSource(themeState.ThemeWallpaperColorSource),
- themeState.UseNativeWallpaperChangeEvents);
- }
-
- 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 wallpaperColorSource,
- string? selectedWallpaperSeed,
- bool queueWallpaperPaletteBuild)
- {
- var source = ResolveWallpaperSeedSource(wallpaperState, wallpaperColorSource);
- 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_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 WallpaperSeedSourceDescriptor ResolveWallpaperSeedSource(
- WallpaperSettingsState wallpaperState,
- string wallpaperColorSource)
- {
- var normalizedWallpaperColorSource = ThemeAppearanceValues.NormalizeWallpaperColorSource(wallpaperColorSource);
-
- if (normalizedWallpaperColorSource != ThemeAppearanceValues.WallpaperColorSourceSystem &&
- 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 (normalizedWallpaperColorSource != ThemeAppearanceValues.WallpaperColorSourceSystem &&
- !string.IsNullOrWhiteSpace(wallpaperPath) &&
- File.Exists(wallpaperPath))
- {
- if (appWallpaperMediaType == WallpaperMediaType.Image)
- {
- return new WallpaperSeedSourceDescriptor(
- "app_wallpaper",
- CreateWallpaperSourceKey("app_wallpaper", wallpaperPath),
- wallpaperPath,
- wallpaperPath,
- null);
- }
- }
-
- if (normalizedWallpaperColorSource == ThemeAppearanceValues.WallpaperColorSourceApp)
- {
- return new WallpaperSeedSourceDescriptor(
- "fallback",
- "fallback",
- null,
- null,
- null);
- }
-
- var systemWallpaper = _systemWallpaperService.GetWallpaperPath();
- if (normalizedWallpaperColorSource != ThemeAppearanceValues.WallpaperColorSourceApp &&
- !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);
- var materialSnapshot = CreateMaterialColorSnapshot(snapshot);
- if (Dispatcher.UIThread.CheckAccess())
- {
- Changed?.Invoke(this, snapshot);
- MaterialColorChanged?.Invoke(this, materialSnapshot);
- return;
- }
-
- Dispatcher.UIThread.Post(() =>
- {
- Changed?.Invoke(this, snapshot);
- MaterialColorChanged?.Invoke(this, materialSnapshot);
- }, DispatcherPriority.Background);
- }
-
- private MaterialColorSnapshot CreateMaterialColorSnapshot(AppearanceThemeSnapshot snapshot)
- {
- var context = CreateThemeContext(snapshot);
- var appPalette = ThemeColorSystemService.BuildPalette(context);
- var palette = new LanMountainDesktop.Models.MaterialColorPalette(
- appPalette.Primary,
- appPalette.Secondary,
- appPalette.Accent,
- appPalette.OnAccent,
- appPalette.AccentLight1,
- appPalette.AccentLight2,
- appPalette.AccentLight3,
- appPalette.AccentDark1,
- appPalette.AccentDark2,
- appPalette.AccentDark3,
- appPalette.SurfaceBase,
- appPalette.SurfaceRaised,
- appPalette.SurfaceOverlay,
- appPalette.TextPrimary,
- appPalette.TextSecondary,
- appPalette.TextMuted,
- appPalette.TextAccent,
- appPalette.NavText,
- appPalette.NavSelectedText,
- appPalette.NavSelectionIndicator,
- appPalette.NavItemBackground,
- appPalette.NavItemHoverBackground,
- appPalette.NavItemSelectedBackground,
- appPalette.ToggleOn,
- appPalette.ToggleOff,
- appPalette.ToggleBorder);
- var surfaces = Enum.GetValues()
- .Select(role =>
- {
- var surface = _materialSurfaceService.GetSurface(context, role);
- return new MaterialSurfaceSnapshot(
- role,
- surface.BackgroundColor,
- surface.BorderColor,
- surface.BlurRadius,
- surface.Opacity);
- })
- .ToDictionary(surface => surface.Role);
-
- return new MaterialColorSnapshot(
- snapshot.IsNightMode,
- snapshot.ThemeColorMode,
- snapshot.ThemeWallpaperColorSource,
- ResolveMaterialColorSourceKind(snapshot),
- snapshot.ResolvedSeedSource,
- snapshot.CornerRadiusTokens,
- snapshot.UserThemeColor,
- snapshot.SelectedWallpaperSeed,
- snapshot.EffectiveSeedColor,
- snapshot.AccentColor,
- snapshot.MonetPalette,
- palette,
- snapshot.WallpaperSeedCandidates,
- snapshot.SystemMaterialMode,
- snapshot.AvailableSystemMaterialModes,
- snapshot.CanChangeSystemMaterial,
- snapshot.UseSystemChrome,
- snapshot.ResolvedWallpaperPath,
- snapshot.UseNativeWallpaperChangeEvents,
- _nativeWallpaperEventsActive,
- _wallpaperPollingActive,
- surfaces);
- }
-
- private static MaterialColorSourceKind ResolveMaterialColorSourceKind(AppearanceThemeSnapshot snapshot)
- {
- if (string.Equals(snapshot.ThemeColorMode, ThemeAppearanceValues.ColorModeDefaultNeutral, StringComparison.OrdinalIgnoreCase))
- {
- return MaterialColorSourceKind.Neutral;
- }
-
- if (string.Equals(snapshot.ThemeColorMode, ThemeAppearanceValues.ColorModeSeedMonet, StringComparison.OrdinalIgnoreCase))
- {
- return MaterialColorSourceKind.CustomSeed;
- }
-
- if (!string.Equals(snapshot.ThemeColorMode, ThemeAppearanceValues.ColorModeWallpaperMonet, StringComparison.OrdinalIgnoreCase))
- {
- return MaterialColorSourceKind.Fallback;
- }
-
- if (string.Equals(snapshot.ResolvedSeedSource, "app_wallpaper", StringComparison.OrdinalIgnoreCase) ||
- string.Equals(snapshot.ResolvedSeedSource, "app_solid", StringComparison.OrdinalIgnoreCase))
- {
- return string.Equals(snapshot.ThemeWallpaperColorSource, ThemeAppearanceValues.WallpaperColorSourceApp, StringComparison.OrdinalIgnoreCase)
- ? MaterialColorSourceKind.AppWallpaper
- : MaterialColorSourceKind.WallpaperAuto;
- }
-
- if (string.Equals(snapshot.ResolvedSeedSource, "system_wallpaper", StringComparison.OrdinalIgnoreCase))
- {
- return string.Equals(snapshot.ThemeWallpaperColorSource, ThemeAppearanceValues.WallpaperColorSourceSystem, StringComparison.OrdinalIgnoreCase)
- ? MaterialColorSourceKind.SystemWallpaper
- : MaterialColorSourceKind.WallpaperAuto;
- }
-
- return MaterialColorSourceKind.Fallback;
- }
-
- private void ConfigureSystemWallpaperMonitoring(ThemeAppearanceSettingsState themeState)
- {
- var colorMode = ThemeAppearanceValues.NormalizeThemeColorMode(themeState.ThemeColorMode, themeState.ThemeColor);
- var wallpaperColorSource = ThemeAppearanceValues.NormalizeWallpaperColorSource(themeState.ThemeWallpaperColorSource);
- var shouldMonitor =
- string.Equals(colorMode, ThemeAppearanceValues.ColorModeWallpaperMonet, StringComparison.OrdinalIgnoreCase) &&
- !string.Equals(wallpaperColorSource, ThemeAppearanceValues.WallpaperColorSourceApp, StringComparison.OrdinalIgnoreCase);
-
- if (!shouldMonitor)
- {
- StopSystemWallpaperMonitoring();
- return;
- }
-
- ConfigureNativeWallpaperEvents(themeState.UseNativeWallpaperChangeEvents);
- ConfigureWallpaperPolling(_settingsFacade.Wallpaper.Get().SystemWallpaperRefreshIntervalSeconds);
- UpdateObservedWallpaperSourceKey();
- }
-
- private void ConfigureNativeWallpaperEvents(bool enabled)
- {
- if (!enabled || !OperatingSystem.IsWindows())
- {
- UnregisterNativeWallpaperEvents();
- return;
- }
-
- if (_nativeWallpaperEventsActive)
- {
- return;
- }
-
- RegisterNativeWallpaperEvents();
- }
-
- private void UnregisterNativeWallpaperEvents()
- {
- if (!_nativeWallpaperEventsActive)
- {
- return;
- }
-
- if (OperatingSystem.IsWindows())
- {
- UnregisterNativeWallpaperEventsCore();
- }
-
- _nativeWallpaperEventsActive = false;
- }
-
- [SupportedOSPlatform("windows")]
- private void RegisterNativeWallpaperEvents()
- {
- try
- {
- SystemEvents.UserPreferenceChanged += OnNativeWallpaperPreferenceChanged;
- _nativeWallpaperEventsActive = true;
- }
- catch (Exception ex)
- {
- _nativeWallpaperEventsActive = false;
- AppLogger.Warn("Appearance.WallpaperMonitor", "Failed to subscribe to native wallpaper change events; polling will remain active.", ex);
- }
- }
-
- [SupportedOSPlatform("windows")]
- private void UnregisterNativeWallpaperEventsCore()
- {
- try
- {
- SystemEvents.UserPreferenceChanged -= OnNativeWallpaperPreferenceChanged;
- }
- catch
- {
- // Ignore shutdown-time native event cleanup failures.
- }
- }
-
- private void ConfigureWallpaperPolling(int intervalSeconds)
- {
- var normalizedInterval = Math.Clamp(intervalSeconds <= 0 ? 300 : intervalSeconds, 30, 86400);
- var interval = TimeSpan.FromSeconds(normalizedInterval);
- _systemWallpaperPollTimer ??= new Timer(OnSystemWallpaperPollTimer);
- _systemWallpaperPollTimer.Change(interval, interval);
- _wallpaperPollingActive = true;
- }
-
- private void StopSystemWallpaperMonitoring()
- {
- UnregisterNativeWallpaperEvents();
- _systemWallpaperPollTimer?.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
- _wallpaperPollingActive = false;
- _lastObservedWallpaperSourceKey = null;
- }
-
- private void OnNativeWallpaperPreferenceChanged(object? sender, UserPreferenceChangedEventArgs e)
- {
- _ = sender;
-
- if (!OperatingSystem.IsWindows())
- {
- return;
- }
-
- if (e.Category is UserPreferenceCategory.Desktop or UserPreferenceCategory.General)
- {
- RefreshWallpaperColors();
- }
- }
-
- private void OnSystemWallpaperPollTimer(object? state)
- {
- _ = state;
-
- try
- {
- var source = ResolveWallpaperSeedSource(_settingsFacade.Wallpaper.Get(), _liveThemeWallpaperColorSource);
- var sourceKey = source.SourceKey;
- if (string.Equals(_lastObservedWallpaperSourceKey, sourceKey, StringComparison.OrdinalIgnoreCase))
- {
- return;
- }
-
- _lastObservedWallpaperSourceKey = sourceKey;
- RefreshWallpaperColors();
- }
- catch (Exception ex)
- {
- AppLogger.Warn("Appearance.WallpaperMonitor", "Failed to poll wallpaper color source.", ex);
- }
- }
-
- private void UpdateObservedWallpaperSourceKey()
- {
- try
- {
- _lastObservedWallpaperSourceKey = ResolveWallpaperSeedSource(
- _settingsFacade.Wallpaper.Get(),
- _liveThemeWallpaperColorSource).SourceKey;
- }
- catch
- {
- _lastObservedWallpaperSourceKey = null;
- }
- }
-
- 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());
+ Changed?.Invoke(this, snapshot);
}
}
internal static class HostAppearanceThemeProvider
{
private static readonly object Gate = new();
- private static AppearanceThemeService? _instance;
+ private static MaterialColorService? _materialColorService;
+ private static AppearanceThemeService? _appearanceThemeService;
public static IAppearanceThemeService GetOrCreate()
{
lock (Gate)
{
- return _instance ??= new AppearanceThemeService(
- HostSettingsFacadeProvider.GetOrCreate(),
- new SystemWallpaperService(),
- new WindowMaterialService(),
- new MaterialSurfaceService());
+ return _appearanceThemeService ??= new AppearanceThemeService(GetMaterialColorServiceCore());
}
}
+
+ internal static MaterialColorService GetMaterialColorService()
+ {
+ lock (Gate)
+ {
+ return GetMaterialColorServiceCore();
+ }
+ }
+
+ private static MaterialColorService GetMaterialColorServiceCore()
+ {
+ return _materialColorService ??= new MaterialColorService(
+ HostSettingsFacadeProvider.GetOrCreate(),
+ HostSystemWallpaperProvider.GetOrCreate(),
+ new WindowMaterialService(),
+ new MaterialSurfaceService());
+ }
}
internal static class HostMaterialColorProvider
{
public static IMaterialColorService GetOrCreate()
{
- return (IMaterialColorService)HostAppearanceThemeProvider.GetOrCreate();
+ return HostAppearanceThemeProvider.GetMaterialColorService();
}
}
diff --git a/LanMountainDesktop/Services/DesktopComponentRegistryFactory.cs b/LanMountainDesktop/Services/DesktopComponentRegistryFactory.cs
index 047df80..ad15fb6 100644
--- a/LanMountainDesktop/Services/DesktopComponentRegistryFactory.cs
+++ b/LanMountainDesktop/Services/DesktopComponentRegistryFactory.cs
@@ -35,12 +35,14 @@ public static class DesktopComponentRegistryFactory
public static DesktopComponentRuntimeRegistry CreateRuntimeRegistry(
ComponentRegistry componentRegistry,
PluginRuntimeService? pluginRuntimeService,
- ISettingsFacadeService settingsFacade)
+ ISettingsFacadeService settingsFacade,
+ IMaterialColorService? materialColorService = null)
{
var registrations = DesktopComponentRuntimeRegistry.GetDefaultRegistrations().ToList();
var registeredIds = new HashSet(
registrations.Select(registration => registration.ComponentId),
StringComparer.OrdinalIgnoreCase);
+ var resolvedMaterialColorService = materialColorService ?? HostMaterialColorProvider.GetOrCreate();
if (pluginRuntimeService is not null)
{
@@ -62,7 +64,7 @@ public static class DesktopComponentRegistryFactory
registrations.Add(new DesktopComponentRuntimeRegistration(
registration.ComponentId,
registration.DisplayNameLocalizationKey,
- factoryContext => CreatePluginControl(contribution, factoryContext),
+ factoryContext => CreatePluginControl(contribution, factoryContext, resolvedMaterialColorService),
chromeContext =>
{
var appearanceContext = CreatePluginAppearanceContext(chromeContext);
@@ -118,7 +120,8 @@ public static class DesktopComponentRegistryFactory
private static Control CreatePluginControl(
PluginDesktopComponentContribution contribution,
- DesktopComponentControlFactoryContext context)
+ DesktopComponentControlFactoryContext context,
+ IMaterialColorService materialColorService)
{
try
{
@@ -129,7 +132,7 @@ public static class DesktopComponentRegistryFactory
settingsService);
var pluginAppearance = new PluginAppearanceContext(
PluginAppearanceSnapshotMapper.FromMaterialColorSnapshot(
- HostMaterialColorProvider.GetOrCreate().GetMaterialColorSnapshot()));
+ materialColorService.GetMaterialColorSnapshot()));
var pluginContext = new PluginDesktopComponentContext(
contribution.Plugin.Manifest,
contribution.Plugin.Context.PluginDirectory,
diff --git a/LanMountainDesktop/Services/GlassEffectService.cs b/LanMountainDesktop/Services/GlassEffectService.cs
index 0dd03d7..7ee9fb5 100644
--- a/LanMountainDesktop/Services/GlassEffectService.cs
+++ b/LanMountainDesktop/Services/GlassEffectService.cs
@@ -7,9 +7,10 @@ namespace LanMountainDesktop.Services;
public static class GlassEffectService
{
+ private static readonly IMaterialSurfaceService MaterialSurfaceService = new MaterialSurfaceService();
+
public static void ApplyGlassResources(IResourceDictionary resources, ThemeColorContext context)
{
- var materialSurfaceService = new MaterialSurfaceService();
var monetPalette = context.MonetPalette;
var monetColors = context.MonetColors?.Where(color => color.A > 0).ToArray() ?? [];
var primary = context.UseNeutralSurfaces
@@ -48,13 +49,13 @@ public static class GlassEffectService
ColorMath.Blend(buttonBackground, primary, context.IsNightMode ? 0.24 : 0.16),
context.IsNightMode ? (byte)0xF8 : (byte)0xFF));
- var windowSurface = materialSurfaceService.GetSurface(context, MaterialSurfaceRole.WindowBackground);
- var settingsWindowSurface = materialSurfaceService.GetSurface(context, MaterialSurfaceRole.SettingsWindowBackground);
- var dockSurface = materialSurfaceService.GetSurface(context, MaterialSurfaceRole.DockBackground);
- var statusBarSurface = materialSurfaceService.GetSurface(context, MaterialSurfaceRole.StatusBarBackground);
- var desktopComponentSurface = materialSurfaceService.GetSurface(context, MaterialSurfaceRole.DesktopComponentHost);
- var statusBarComponentSurface = materialSurfaceService.GetSurface(context, MaterialSurfaceRole.StatusBarComponentHost);
- var overlaySurface = materialSurfaceService.GetSurface(context, MaterialSurfaceRole.OverlayPanel);
+ var windowSurface = MaterialSurfaceService.GetSurface(context, MaterialSurfaceRole.WindowBackground);
+ var settingsWindowSurface = MaterialSurfaceService.GetSurface(context, MaterialSurfaceRole.SettingsWindowBackground);
+ var dockSurface = MaterialSurfaceService.GetSurface(context, MaterialSurfaceRole.DockBackground);
+ var statusBarSurface = MaterialSurfaceService.GetSurface(context, MaterialSurfaceRole.StatusBarBackground);
+ var desktopComponentSurface = MaterialSurfaceService.GetSurface(context, MaterialSurfaceRole.DesktopComponentHost);
+ var statusBarComponentSurface = MaterialSurfaceService.GetSurface(context, MaterialSurfaceRole.StatusBarComponentHost);
+ var overlaySurface = MaterialSurfaceService.GetSurface(context, MaterialSurfaceRole.OverlayPanel);
var strongSurfaceColor = ColorMath.Blend(
desktopComponentSurface.BackgroundColor,
overlaySurface.BackgroundColor,
diff --git a/LanMountainDesktop/Services/MaterialColorService.cs b/LanMountainDesktop/Services/MaterialColorService.cs
new file mode 100644
index 0000000..46ae6ac
--- /dev/null
+++ b/LanMountainDesktop/Services/MaterialColorService.cs
@@ -0,0 +1,645 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.Versioning;
+using System.Threading;
+using Avalonia.Controls;
+using Avalonia.Media;
+using Avalonia.Styling;
+using Avalonia.Threading;
+using LanMountainDesktop.Appearance;
+using LanMountainDesktop.Models;
+using LanMountainDesktop.PluginSdk;
+using LanMountainDesktop.Services.Settings;
+using LanMountainDesktop.Settings.Core;
+using LanMountainDesktop.Shared.Contracts;
+using LanMountainDesktop.Theme;
+using Microsoft.Win32;
+
+namespace LanMountainDesktop.Services;
+
+internal sealed class MaterialColorService : IMaterialColorService, IDisposable
+{
+ private static readonly Color DefaultAccentColor = Color.Parse("#FF3B82F6");
+ private readonly ISettingsFacadeService _settingsFacade;
+ private readonly IWindowMaterialService _windowMaterialService;
+ private readonly IMaterialSurfaceService _materialSurfaceService;
+ private readonly MonetColorService _monetColorService = new();
+ private readonly WallpaperColorPipeline _wallpaperColorPipeline;
+ private string _liveThemeColorMode;
+ private string _liveSystemMaterialMode;
+ private string? _liveSelectedWallpaperSeed;
+ private string _liveThemeWallpaperColorSource;
+ private bool _liveUseNativeWallpaperChangeEvents;
+ private Timer? _systemWallpaperPollTimer;
+ private string? _lastObservedWallpaperSourceKey;
+ private bool _nativeWallpaperEventsActive;
+ private bool _wallpaperPollingActive;
+
+ public MaterialColorService(
+ ISettingsFacadeService settingsFacade,
+ ISystemWallpaperProvider systemWallpaperProvider,
+ IWindowMaterialService windowMaterialService,
+ IMaterialSurfaceService materialSurfaceService)
+ {
+ _settingsFacade = settingsFacade ?? throw new ArgumentNullException(nameof(settingsFacade));
+ _windowMaterialService = windowMaterialService ?? throw new ArgumentNullException(nameof(windowMaterialService));
+ _materialSurfaceService = materialSurfaceService ?? throw new ArgumentNullException(nameof(materialSurfaceService));
+ _wallpaperColorPipeline = new WallpaperColorPipeline(
+ _settingsFacade,
+ systemWallpaperProvider ?? throw new ArgumentNullException(nameof(systemWallpaperProvider)),
+ _monetColorService,
+ RaiseChanged);
+ var initialThemeState = _settingsFacade.Theme.Get();
+ _liveThemeColorMode = ThemeAppearanceValues.NormalizeThemeColorMode(
+ initialThemeState.ThemeColorMode,
+ initialThemeState.ThemeColor);
+ _liveSystemMaterialMode = ResolveSupportedMaterialMode(initialThemeState.SystemMaterialMode);
+ _liveSelectedWallpaperSeed = initialThemeState.SelectedWallpaperSeed;
+ _liveThemeWallpaperColorSource = ThemeAppearanceValues.NormalizeWallpaperColorSource(initialThemeState.ThemeWallpaperColorSource);
+ _liveUseNativeWallpaperChangeEvents = initialThemeState.UseNativeWallpaperChangeEvents;
+ _settingsFacade.Settings.Changed += OnSettingsChanged;
+ ConfigureSystemWallpaperMonitoring(initialThemeState);
+ }
+
+ internal event EventHandler? AppearanceThemeChanged;
+
+ public event EventHandler? MaterialColorChanged;
+
+ 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 MaterialColorSnapshot GetMaterialColorSnapshot()
+ {
+ return CreateMaterialColorSnapshot(GetCurrent());
+ }
+
+ public MaterialColorSnapshot BuildMaterialColorPreview(ThemeAppearanceSettingsState pendingState)
+ {
+ return CreateMaterialColorSnapshot(BuildPreview(pendingState));
+ }
+
+ public MaterialSurfaceSnapshot GetSurface(MaterialSurfaceRole role)
+ {
+ var surface = GetMaterialSurface(role);
+ return new MaterialSurfaceSnapshot(
+ role,
+ surface.BackgroundColor,
+ surface.BorderColor,
+ surface.BlurRadius,
+ surface.Opacity);
+ }
+
+ public void RefreshWallpaperColors()
+ {
+ _wallpaperColorPipeline.Clear();
+ _lastObservedWallpaperSourceKey = null;
+ RaiseChanged(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);
+ resources["DesignCornerRadiusMicro"] = snapshot.CornerRadiusTokens.Micro;
+ resources["DesignCornerRadiusXs"] = snapshot.CornerRadiusTokens.Xs;
+ resources["DesignCornerRadiusSm"] = snapshot.CornerRadiusTokens.Sm;
+ resources["DesignCornerRadiusMd"] = snapshot.CornerRadiusTokens.Md;
+ resources["DesignCornerRadiusLg"] = snapshot.CornerRadiusTokens.Lg;
+ resources["DesignCornerRadiusXl"] = snapshot.CornerRadiusTokens.Xl;
+ resources["DesignCornerRadiusIsland"] = snapshot.CornerRadiusTokens.Island;
+ resources["DesignCornerRadiusComponent"] = snapshot.CornerRadiusTokens.Component;
+ }
+
+ 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.
+ // SettingsWindowBackground 是唯一需要材质与资源同步热切换的宿主角色;其它窗口仍保持「仅创建时」应用以降低风险。
+ if (window.IsVisible && role != MaterialSurfaceRole.SettingsWindowBackground)
+ {
+ 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;
+ StopSystemWallpaperMonitoring();
+ _systemWallpaperPollTimer?.Dispose();
+ _systemWallpaperPollTimer = null;
+ }
+
+ 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) &&
+ !changedKeys.Contains(nameof(AppSettingsSnapshot.CornerRadiusStyle), StringComparer.OrdinalIgnoreCase) &&
+ !changedKeys.Contains(nameof(AppSettingsSnapshot.ThemeColorMode), StringComparer.OrdinalIgnoreCase) &&
+ !changedKeys.Contains(nameof(AppSettingsSnapshot.SystemMaterialMode), StringComparer.OrdinalIgnoreCase) &&
+ !changedKeys.Contains(nameof(AppSettingsSnapshot.SelectedWallpaperSeed), StringComparer.OrdinalIgnoreCase) &&
+ !changedKeys.Contains(nameof(AppSettingsSnapshot.ThemeWallpaperColorSource), StringComparer.OrdinalIgnoreCase) &&
+ !changedKeys.Contains(nameof(AppSettingsSnapshot.UseNativeWallpaperChangeEvents), StringComparer.OrdinalIgnoreCase) &&
+ !changedKeys.Contains(nameof(AppSettingsSnapshot.SystemWallpaperRefreshIntervalSeconds), 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;
+ }
+
+ var latestThemeState = _settingsFacade.Theme.Get();
+ _liveThemeColorMode = ThemeAppearanceValues.NormalizeThemeColorMode(
+ latestThemeState.ThemeColorMode,
+ latestThemeState.ThemeColor);
+ _liveSystemMaterialMode = ResolveSupportedMaterialMode(latestThemeState.SystemMaterialMode);
+ _liveSelectedWallpaperSeed = latestThemeState.SelectedWallpaperSeed;
+ _liveThemeWallpaperColorSource = ThemeAppearanceValues.NormalizeWallpaperColorSource(latestThemeState.ThemeWallpaperColorSource);
+ _liveUseNativeWallpaperChangeEvents = latestThemeState.UseNativeWallpaperChangeEvents;
+ ConfigureSystemWallpaperMonitoring(latestThemeState);
+ RaiseChanged(queueWallpaperPaletteBuild: true);
+ }
+
+ private AppearanceThemeSnapshot BuildSnapshot(
+ ThemeAppearanceSettingsState themeState,
+ string themeColorMode,
+ string systemMaterialMode,
+ string? selectedWallpaperSeed,
+ bool queueWallpaperPaletteBuild)
+ {
+ var availableModes = _windowMaterialService.GetAvailableModes();
+ var cornerRadiusStyle = GlobalAppearanceSettings.NormalizeCornerRadiusStyle(themeState.CornerRadiusStyle);
+ var cornerRadiusTokens = AppearanceCornerRadiusTokenFactory.Create(cornerRadiusStyle);
+ 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 = _wallpaperColorPipeline.Resolve(
+ themeState.IsNightMode,
+ wallpaperState,
+ ThemeAppearanceValues.NormalizeWallpaperColorSource(themeState.ThemeWallpaperColorSource),
+ 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,
+ cornerRadiusStyle,
+ cornerRadiusTokens,
+ resolvedSeedSource,
+ palette,
+ ResolveAccentColor(themeColorMode, themeState.ThemeColor, palette),
+ effectiveSeedColor,
+ wallpaperSeedCandidates,
+ systemMaterialMode,
+ availableModes,
+ _windowMaterialService.CanChangeMode,
+ themeState.UseSystemChrome,
+ resolvedWallpaperPath,
+ ThemeAppearanceValues.NormalizeWallpaperColorSource(themeState.ThemeWallpaperColorSource),
+ themeState.UseNativeWallpaperChangeEvents);
+ }
+
+ 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 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 void RaiseChanged(bool queueWallpaperPaletteBuild)
+ {
+ var snapshot = BuildCurrentSnapshot(queueWallpaperPaletteBuild);
+ var materialSnapshot = CreateMaterialColorSnapshot(snapshot);
+ if (Dispatcher.UIThread.CheckAccess())
+ {
+ AppearanceThemeChanged?.Invoke(this, snapshot);
+ MaterialColorChanged?.Invoke(this, materialSnapshot);
+ return;
+ }
+
+ Dispatcher.UIThread.Post(() =>
+ {
+ AppearanceThemeChanged?.Invoke(this, snapshot);
+ MaterialColorChanged?.Invoke(this, materialSnapshot);
+ }, DispatcherPriority.Background);
+ }
+
+ private MaterialColorSnapshot CreateMaterialColorSnapshot(AppearanceThemeSnapshot snapshot)
+ {
+ var context = CreateThemeContext(snapshot);
+ var appPalette = ThemeColorSystemService.BuildPalette(context);
+ var palette = new LanMountainDesktop.Models.MaterialColorPalette(
+ appPalette.Primary,
+ appPalette.Secondary,
+ appPalette.Accent,
+ appPalette.OnAccent,
+ appPalette.AccentLight1,
+ appPalette.AccentLight2,
+ appPalette.AccentLight3,
+ appPalette.AccentDark1,
+ appPalette.AccentDark2,
+ appPalette.AccentDark3,
+ appPalette.SurfaceBase,
+ appPalette.SurfaceRaised,
+ appPalette.SurfaceOverlay,
+ appPalette.TextPrimary,
+ appPalette.TextSecondary,
+ appPalette.TextMuted,
+ appPalette.TextAccent,
+ appPalette.NavText,
+ appPalette.NavSelectedText,
+ appPalette.NavSelectionIndicator,
+ appPalette.NavItemBackground,
+ appPalette.NavItemHoverBackground,
+ appPalette.NavItemSelectedBackground,
+ appPalette.ToggleOn,
+ appPalette.ToggleOff,
+ appPalette.ToggleBorder);
+ var surfaces = Enum.GetValues()
+ .Select(role =>
+ {
+ var surface = _materialSurfaceService.GetSurface(context, role);
+ return new MaterialSurfaceSnapshot(
+ role,
+ surface.BackgroundColor,
+ surface.BorderColor,
+ surface.BlurRadius,
+ surface.Opacity);
+ })
+ .ToDictionary(surface => surface.Role);
+
+ return new MaterialColorSnapshot(
+ snapshot.IsNightMode,
+ snapshot.ThemeColorMode,
+ snapshot.ThemeWallpaperColorSource,
+ ResolveMaterialColorSourceKind(snapshot),
+ snapshot.ResolvedSeedSource,
+ snapshot.CornerRadiusTokens,
+ snapshot.UserThemeColor,
+ snapshot.SelectedWallpaperSeed,
+ snapshot.EffectiveSeedColor,
+ snapshot.AccentColor,
+ snapshot.MonetPalette,
+ palette,
+ snapshot.WallpaperSeedCandidates,
+ snapshot.SystemMaterialMode,
+ snapshot.AvailableSystemMaterialModes,
+ snapshot.CanChangeSystemMaterial,
+ snapshot.UseSystemChrome,
+ snapshot.ResolvedWallpaperPath,
+ snapshot.UseNativeWallpaperChangeEvents,
+ _nativeWallpaperEventsActive,
+ _wallpaperPollingActive,
+ surfaces);
+ }
+
+ private static MaterialColorSourceKind ResolveMaterialColorSourceKind(AppearanceThemeSnapshot snapshot)
+ {
+ if (string.Equals(snapshot.ThemeColorMode, ThemeAppearanceValues.ColorModeDefaultNeutral, StringComparison.OrdinalIgnoreCase))
+ {
+ return MaterialColorSourceKind.Neutral;
+ }
+
+ if (string.Equals(snapshot.ThemeColorMode, ThemeAppearanceValues.ColorModeSeedMonet, StringComparison.OrdinalIgnoreCase))
+ {
+ return MaterialColorSourceKind.CustomSeed;
+ }
+
+ if (!string.Equals(snapshot.ThemeColorMode, ThemeAppearanceValues.ColorModeWallpaperMonet, StringComparison.OrdinalIgnoreCase))
+ {
+ return MaterialColorSourceKind.Fallback;
+ }
+
+ if (string.Equals(snapshot.ResolvedSeedSource, "app_wallpaper", StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(snapshot.ResolvedSeedSource, "app_solid", StringComparison.OrdinalIgnoreCase))
+ {
+ return string.Equals(snapshot.ThemeWallpaperColorSource, ThemeAppearanceValues.WallpaperColorSourceApp, StringComparison.OrdinalIgnoreCase)
+ ? MaterialColorSourceKind.AppWallpaper
+ : MaterialColorSourceKind.WallpaperAuto;
+ }
+
+ if (string.Equals(snapshot.ResolvedSeedSource, "system_wallpaper", StringComparison.OrdinalIgnoreCase))
+ {
+ return string.Equals(snapshot.ThemeWallpaperColorSource, ThemeAppearanceValues.WallpaperColorSourceSystem, StringComparison.OrdinalIgnoreCase)
+ ? MaterialColorSourceKind.SystemWallpaper
+ : MaterialColorSourceKind.WallpaperAuto;
+ }
+
+ return MaterialColorSourceKind.Fallback;
+ }
+
+ private void ConfigureSystemWallpaperMonitoring(ThemeAppearanceSettingsState themeState)
+ {
+ var colorMode = ThemeAppearanceValues.NormalizeThemeColorMode(themeState.ThemeColorMode, themeState.ThemeColor);
+ var wallpaperColorSource = ThemeAppearanceValues.NormalizeWallpaperColorSource(themeState.ThemeWallpaperColorSource);
+ var shouldMonitor =
+ string.Equals(colorMode, ThemeAppearanceValues.ColorModeWallpaperMonet, StringComparison.OrdinalIgnoreCase) &&
+ !string.Equals(wallpaperColorSource, ThemeAppearanceValues.WallpaperColorSourceApp, StringComparison.OrdinalIgnoreCase);
+
+ if (!shouldMonitor)
+ {
+ StopSystemWallpaperMonitoring();
+ return;
+ }
+
+ ConfigureNativeWallpaperEvents(themeState.UseNativeWallpaperChangeEvents);
+ ConfigureWallpaperPolling(_settingsFacade.Wallpaper.Get().SystemWallpaperRefreshIntervalSeconds);
+ UpdateObservedWallpaperSourceKey();
+ }
+
+ private void ConfigureNativeWallpaperEvents(bool enabled)
+ {
+ if (!enabled || !OperatingSystem.IsWindows())
+ {
+ UnregisterNativeWallpaperEvents();
+ return;
+ }
+
+ if (_nativeWallpaperEventsActive)
+ {
+ return;
+ }
+
+ RegisterNativeWallpaperEvents();
+ }
+
+ private void UnregisterNativeWallpaperEvents()
+ {
+ if (!_nativeWallpaperEventsActive)
+ {
+ return;
+ }
+
+ if (OperatingSystem.IsWindows())
+ {
+ UnregisterNativeWallpaperEventsCore();
+ }
+
+ _nativeWallpaperEventsActive = false;
+ }
+
+ [SupportedOSPlatform("windows")]
+ private void RegisterNativeWallpaperEvents()
+ {
+ try
+ {
+ SystemEvents.UserPreferenceChanged += OnNativeWallpaperPreferenceChanged;
+ _nativeWallpaperEventsActive = true;
+ }
+ catch (Exception ex)
+ {
+ _nativeWallpaperEventsActive = false;
+ AppLogger.Warn("Appearance.WallpaperMonitor", "Failed to subscribe to native wallpaper change events; polling will remain active.", ex);
+ }
+ }
+
+ [SupportedOSPlatform("windows")]
+ private void UnregisterNativeWallpaperEventsCore()
+ {
+ try
+ {
+ SystemEvents.UserPreferenceChanged -= OnNativeWallpaperPreferenceChanged;
+ }
+ catch
+ {
+ // Ignore shutdown-time native event cleanup failures.
+ }
+ }
+
+ private void ConfigureWallpaperPolling(int intervalSeconds)
+ {
+ var normalizedInterval = Math.Clamp(intervalSeconds <= 0 ? 300 : intervalSeconds, 30, 86400);
+ var interval = TimeSpan.FromSeconds(normalizedInterval);
+ _systemWallpaperPollTimer ??= new Timer(OnSystemWallpaperPollTimer);
+ _systemWallpaperPollTimer.Change(interval, interval);
+ _wallpaperPollingActive = true;
+ }
+
+ private void StopSystemWallpaperMonitoring()
+ {
+ UnregisterNativeWallpaperEvents();
+ _systemWallpaperPollTimer?.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
+ _wallpaperPollingActive = false;
+ _lastObservedWallpaperSourceKey = null;
+ }
+
+ private void OnNativeWallpaperPreferenceChanged(object? sender, UserPreferenceChangedEventArgs e)
+ {
+ _ = sender;
+
+ if (!OperatingSystem.IsWindows())
+ {
+ return;
+ }
+
+ if (e.Category is UserPreferenceCategory.Desktop or UserPreferenceCategory.General)
+ {
+ RefreshWallpaperColors();
+ }
+ }
+
+ private void OnSystemWallpaperPollTimer(object? state)
+ {
+ _ = state;
+
+ try
+ {
+ var source = _wallpaperColorPipeline.ResolveSource(_settingsFacade.Wallpaper.Get(), _liveThemeWallpaperColorSource);
+ var sourceKey = source.SourceKey;
+ if (string.Equals(_lastObservedWallpaperSourceKey, sourceKey, StringComparison.OrdinalIgnoreCase))
+ {
+ return;
+ }
+
+ _lastObservedWallpaperSourceKey = sourceKey;
+ RefreshWallpaperColors();
+ }
+ catch (Exception ex)
+ {
+ AppLogger.Warn("Appearance.WallpaperMonitor", "Failed to poll wallpaper color source.", ex);
+ }
+ }
+
+ private void UpdateObservedWallpaperSourceKey()
+ {
+ try
+ {
+ _lastObservedWallpaperSourceKey = _wallpaperColorPipeline.ResolveSource(
+ _settingsFacade.Wallpaper.Get(),
+ _liveThemeWallpaperColorSource).SourceKey;
+ }
+ catch
+ {
+ _lastObservedWallpaperSourceKey = null;
+ }
+ }
+
+
+}
diff --git a/LanMountainDesktop/Services/MaterialSurfaceService.cs b/LanMountainDesktop/Services/MaterialSurfaceService.cs
new file mode 100644
index 0000000..814ad94
--- /dev/null
+++ b/LanMountainDesktop/Services/MaterialSurfaceService.cs
@@ -0,0 +1,144 @@
+using System.Linq;
+using Avalonia.Media;
+using LanMountainDesktop.Services.Settings;
+using LanMountainDesktop.Theme;
+
+namespace LanMountainDesktop.Services;
+
+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.ResolveEffectiveSystemMaterialMode(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)
+ {
+ if (role == MaterialSurfaceRole.SettingsWindowBackground)
+ {
+ return materialMode switch
+ {
+ ThemeAppearanceValues.MaterialAcrylic => (
+ 0.20,
+ 0.14,
+ isNightMode ? (byte)0x8E : (byte)0x96,
+ 0),
+ ThemeAppearanceValues.MaterialMica => (
+ 0.14,
+ 0.08,
+ isNightMode ? (byte)0x9E : (byte)0xA6,
+ 0),
+ _ => (0.08, 0.05, (byte)0xFF, 0)
+ };
+ }
+
+ 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")
+ };
+ }
+}
diff --git a/LanMountainDesktop/Services/NotificationService.cs b/LanMountainDesktop/Services/NotificationService.cs
index 500c861..1fdb324 100644
--- a/LanMountainDesktop/Services/NotificationService.cs
+++ b/LanMountainDesktop/Services/NotificationService.cs
@@ -94,13 +94,18 @@ public interface INotificationService
internal sealed class NotificationService : INotificationService
{
- private readonly IAppearanceThemeService? _appearanceThemeService;
+ private readonly IMaterialColorService _materialColorService;
private readonly NotificationWindowManager _windowManager;
- public NotificationService(IAppearanceThemeService? appearanceThemeService = null)
+ public NotificationService(
+ IAppearanceThemeService? appearanceThemeService = null,
+ IMaterialColorService? materialColorService = null)
{
- _appearanceThemeService = appearanceThemeService;
+ _materialColorService = materialColorService
+ ?? appearanceThemeService as IMaterialColorService
+ ?? HostMaterialColorProvider.GetOrCreate();
_windowManager = NotificationWindowManager.Instance;
+ _materialColorService.MaterialColorChanged += OnMaterialColorChanged;
}
public void Show(NotificationContent content)
@@ -122,7 +127,7 @@ internal sealed class NotificationService : INotificationService
private void ShowDialogWindow(NotificationContent content)
{
var window = new NotificationDialogWindow();
- window.Initialize(content, _appearanceThemeService);
+ window.Initialize(content, _materialColorService.GetMaterialColorSnapshot());
Screen? screen = null;
if (Avalonia.Application.Current?.ApplicationLifetime is Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime desktop)
@@ -223,6 +228,7 @@ internal sealed class NotificationService : INotificationService
private void ShowCore(NotificationContent content)
{
+ var materialColorSnapshot = _materialColorService.GetMaterialColorSnapshot();
var viewModel = new NotificationViewModel
{
Title = content.Title,
@@ -253,7 +259,13 @@ internal sealed class NotificationService : INotificationService
}
}
- _windowManager.ShowNotification(viewModel, _appearanceThemeService);
+ _windowManager.ShowNotification(viewModel, materialColorSnapshot);
+ }
+
+ private void OnMaterialColorChanged(object? sender, MaterialColorSnapshot snapshot)
+ {
+ _ = sender;
+ _windowManager.ApplyMaterialColorToAllWindows(snapshot);
}
public void ShowInfo(string title, string? message = null,
@@ -346,7 +358,7 @@ internal sealed class NotificationWindowManager
}
}
- public void ShowNotification(NotificationViewModel viewModel, IAppearanceThemeService? themeService)
+ public void ShowNotification(NotificationViewModel viewModel, MaterialColorSnapshot materialColorSnapshot)
{
var position = viewModel.Position;
var windows = _windowsByPosition[position];
@@ -362,7 +374,7 @@ internal sealed class NotificationWindowManager
}
var window = new NotificationWindow();
- window.Initialize(viewModel, themeService);
+ window.Initialize(viewModel, materialColorSnapshot);
window.Closed += OnWindowClosed;
windows.Add(window);
@@ -371,6 +383,23 @@ internal sealed class NotificationWindowManager
window.ShowWithAnimationAsync();
}
+ public void ApplyMaterialColorToAllWindows(MaterialColorSnapshot snapshot)
+ {
+ foreach (var windows in _windowsByPosition.Values)
+ {
+ foreach (var window in windows.ToList())
+ {
+ try
+ {
+ window.ApplyMaterialSnapshot(snapshot);
+ }
+ catch
+ {
+ }
+ }
+ }
+ }
+
private void OnWindowClosed(object? sender, EventArgs e)
{
if (sender is not NotificationWindow window) return;
@@ -484,20 +513,4 @@ internal sealed class NotificationWindowManager
return null;
}
- public void ApplyThemeToAllWindows(AppearanceThemeSnapshot snapshot)
- {
- foreach (var windows in _windowsByPosition.Values)
- {
- foreach (var window in windows.ToList())
- {
- try
- {
- window.RequestedThemeVariant = snapshot.IsNightMode ? Avalonia.Styling.ThemeVariant.Dark : Avalonia.Styling.ThemeVariant.Light;
- }
- catch
- {
- }
- }
- }
- }
}
diff --git a/LanMountainDesktop/Services/PluginAppearanceSnapshotMapper.cs b/LanMountainDesktop/Services/PluginAppearanceSnapshotMapper.cs
index d19397b..3936c01 100644
--- a/LanMountainDesktop/Services/PluginAppearanceSnapshotMapper.cs
+++ b/LanMountainDesktop/Services/PluginAppearanceSnapshotMapper.cs
@@ -9,6 +9,9 @@ namespace LanMountainDesktop.Services;
internal static class PluginAppearanceSnapshotMapper
{
+ ///
+ /// Normal host-to-plugin appearance mapping for the live material color pipeline.
+ ///
public static PluginAppearanceSnapshot FromMaterialColorSnapshot(MaterialColorSnapshot snapshot)
{
ArgumentNullException.ThrowIfNull(snapshot);
@@ -32,7 +35,11 @@ internal static class PluginAppearanceSnapshotMapper
snapshot.WallpaperSeedCandidates.Select(ToText).ToArray());
}
- public static PluginAppearanceSnapshot FromAppearanceSnapshot(AppearanceThemeSnapshot snapshot)
+ ///
+ /// Compatibility-only mapper for older hosts that still expose
+ /// instead of the material color pipeline.
+ ///
+ public static PluginAppearanceSnapshot FromCompatibilityAppearanceSnapshot(AppearanceThemeSnapshot snapshot)
{
ArgumentNullException.ThrowIfNull(snapshot);
@@ -56,6 +63,15 @@ internal static class PluginAppearanceSnapshotMapper
snapshot.WallpaperSeedCandidates.Select(ToText).ToArray());
}
+ ///
+ /// Backward-compatible alias for older call sites. Prefer .
+ ///
+ [Obsolete("Use FromCompatibilityAppearanceSnapshot instead.")]
+ public static PluginAppearanceSnapshot FromAppearanceSnapshot(AppearanceThemeSnapshot snapshot)
+ {
+ return FromCompatibilityAppearanceSnapshot(snapshot);
+ }
+
private static IReadOnlyDictionary BuildColorRoles(MaterialColorSnapshot snapshot)
{
return new Dictionary(StringComparer.OrdinalIgnoreCase)
diff --git a/LanMountainDesktop/Services/Settings/SettingsCatalogService.cs b/LanMountainDesktop/Services/Settings/SettingsCatalogService.cs
index 2e2dada..3d0e289 100644
--- a/LanMountainDesktop/Services/Settings/SettingsCatalogService.cs
+++ b/LanMountainDesktop/Services/Settings/SettingsCatalogService.cs
@@ -16,7 +16,9 @@ internal sealed class SettingsCatalogService : ISettingsCatalog
_sections.AddRange(
[
new SettingsSectionDefinition("general", SettingsCategories.General, SettingsScope.App, "settings.general.title", iconKey: "Settings", sortOrder: 0),
+ new SettingsSectionDefinition("material-color", SettingsCategories.Appearance, SettingsScope.App, "settings.material_color.title", iconKey: "Color", sortOrder: 8),
new SettingsSectionDefinition("appearance", SettingsCategories.Appearance, SettingsScope.App, "settings.appearance.title", iconKey: "DesignIdeas", sortOrder: 10),
+ new SettingsSectionDefinition("wallpaper", SettingsCategories.Appearance, SettingsScope.App, "settings.wallpaper.title", iconKey: "Image", sortOrder: 15),
new SettingsSectionDefinition("components", SettingsCategories.Components, SettingsScope.ComponentInstance, "settings.components.title", iconKey: "Apps", sortOrder: 20),
new SettingsSectionDefinition("plugins", SettingsCategories.Plugins, SettingsScope.Plugin, "settings.plugins.title", iconKey: "PuzzlePiece", sortOrder: 30),
new SettingsSectionDefinition("about", SettingsCategories.About, SettingsScope.App, "settings.about.title", iconKey: "Info", sortOrder: 40)
diff --git a/LanMountainDesktop/Services/Settings/SettingsContracts.cs b/LanMountainDesktop/Services/Settings/SettingsContracts.cs
index 97c4bb3..4374912 100644
--- a/LanMountainDesktop/Services/Settings/SettingsContracts.cs
+++ b/LanMountainDesktop/Services/Settings/SettingsContracts.cs
@@ -20,11 +20,10 @@ public enum WallpaperMediaType
public sealed record GridSettingsState(int ShortSideCells, string SpacingPreset, int EdgeInsetPercent);
public sealed record WallpaperSettingsState(
- string? WallpaperPath,
- string Type,
- string? Color,
- string Placement,
- string? CustomColor = null,
+ string? WallpaperPath,
+ string Type,
+ string? Color,
+ string Placement,
int SystemWallpaperRefreshIntervalSeconds = 300);
public sealed record ThemeAppearanceSettingsState(
bool IsNightMode,
diff --git a/LanMountainDesktop/Services/Settings/SettingsDomainServices.cs b/LanMountainDesktop/Services/Settings/SettingsDomainServices.cs
index f5916ea..7aeba68 100644
--- a/LanMountainDesktop/Services/Settings/SettingsDomainServices.cs
+++ b/LanMountainDesktop/Services/Settings/SettingsDomainServices.cs
@@ -102,7 +102,6 @@ internal sealed class WallpaperSettingsService : IWallpaperSettingsService
normalizedType,
snapshot.WallpaperColor,
snapshot.WallpaperPlacement,
- CustomColor: null,
SystemWallpaperRefreshIntervalSeconds: NormalizeRefreshInterval(snapshot.SystemWallpaperRefreshIntervalSeconds));
}
diff --git a/LanMountainDesktop/Services/WallpaperColorPipeline.cs b/LanMountainDesktop/Services/WallpaperColorPipeline.cs
new file mode 100644
index 0000000..08df2eb
--- /dev/null
+++ b/LanMountainDesktop/Services/WallpaperColorPipeline.cs
@@ -0,0 +1,353 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading.Tasks;
+using Avalonia.Media;
+using Avalonia.Media.Imaging;
+using LanMountainDesktop.Models;
+using LanMountainDesktop.Services.Settings;
+
+namespace LanMountainDesktop.Services;
+
+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 WallpaperColorPipeline
+{
+ private static readonly Color NeutralFallbackSeedColor = Color.Parse("#FF8A8A8A");
+
+ private readonly ISettingsFacadeService _settingsFacade;
+ private readonly ISystemWallpaperProvider _systemWallpaperProvider;
+ private readonly MonetColorService _monetColorService;
+ private readonly Action _notifyChanged;
+ private readonly object _gate = new();
+ private readonly Dictionary _seedCache = new(StringComparer.OrdinalIgnoreCase);
+ private readonly HashSet _pendingSeedKeys = new(StringComparer.OrdinalIgnoreCase);
+
+ public WallpaperColorPipeline(
+ ISettingsFacadeService settingsFacade,
+ ISystemWallpaperProvider systemWallpaperProvider,
+ MonetColorService monetColorService,
+ Action notifyChanged)
+ {
+ _settingsFacade = settingsFacade ?? throw new ArgumentNullException(nameof(settingsFacade));
+ _systemWallpaperProvider = systemWallpaperProvider ?? throw new ArgumentNullException(nameof(systemWallpaperProvider));
+ _monetColorService = monetColorService ?? throw new ArgumentNullException(nameof(monetColorService));
+ _notifyChanged = notifyChanged ?? throw new ArgumentNullException(nameof(notifyChanged));
+ }
+
+ public void Clear()
+ {
+ lock (_gate)
+ {
+ _seedCache.Clear();
+ _pendingSeedKeys.Clear();
+ }
+ }
+
+ public WallpaperPaletteResolution Resolve(
+ bool nightMode,
+ WallpaperSettingsState wallpaperState,
+ string wallpaperColorSource,
+ string? selectedWallpaperSeed,
+ bool queueWallpaperPaletteBuild)
+ {
+ var source = ResolveSource(wallpaperState, wallpaperColorSource);
+ if (string.Equals(source.SourceKind, "fallback", StringComparison.OrdinalIgnoreCase))
+ {
+ return BuildFallbackResolution(nightMode, source.ResolvedWallpaperPath);
+ }
+
+ if (string.Equals(source.SourceKind, "app_solid", StringComparison.OrdinalIgnoreCase))
+ {
+ var candidates = source.SolidColor is { } solidColor
+ ? new[] { solidColor }
+ : [];
+ return BuildResolution(nightMode, source, candidates, selectedWallpaperSeed);
+ }
+
+ lock (_gate)
+ {
+ if (_seedCache.TryGetValue(source.SourceKey, out var cachedSeedResult))
+ {
+ if (cachedSeedResult.SeedCandidates.Count > 0)
+ {
+ return BuildResolution(
+ nightMode,
+ source with
+ {
+ SourceKind = cachedSeedResult.SourceKind,
+ ResolvedWallpaperPath = cachedSeedResult.ResolvedWallpaperPath
+ },
+ cachedSeedResult.SeedCandidates,
+ selectedWallpaperSeed);
+ }
+
+ return BuildFallbackResolution(nightMode, cachedSeedResult.ResolvedWallpaperPath);
+ }
+ }
+
+ if (queueWallpaperPaletteBuild)
+ {
+ QueueSeedExtraction(source);
+ }
+
+ return BuildFallbackResolution(nightMode, source.ResolvedWallpaperPath);
+ }
+
+ public WallpaperSeedSourceDescriptor ResolveSource(
+ WallpaperSettingsState wallpaperState,
+ string wallpaperColorSource)
+ {
+ var normalizedWallpaperColorSource = ThemeAppearanceValues.NormalizeWallpaperColorSource(wallpaperColorSource);
+
+ if (normalizedWallpaperColorSource != ThemeAppearanceValues.WallpaperColorSourceSystem &&
+ 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 (normalizedWallpaperColorSource != ThemeAppearanceValues.WallpaperColorSourceSystem &&
+ !string.IsNullOrWhiteSpace(wallpaperPath) &&
+ File.Exists(wallpaperPath) &&
+ appWallpaperMediaType == WallpaperMediaType.Image)
+ {
+ return new WallpaperSeedSourceDescriptor(
+ "app_wallpaper",
+ CreateWallpaperSourceKey("app_wallpaper", wallpaperPath),
+ wallpaperPath,
+ wallpaperPath,
+ null);
+ }
+
+ if (normalizedWallpaperColorSource == ThemeAppearanceValues.WallpaperColorSourceApp)
+ {
+ return new WallpaperSeedSourceDescriptor(
+ "fallback",
+ "fallback",
+ null,
+ null,
+ null);
+ }
+
+ var systemWallpaper = _systemWallpaperProvider.GetWallpaperPath();
+ if (normalizedWallpaperColorSource != ThemeAppearanceValues.WallpaperColorSourceApp &&
+ !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 QueueSeedExtraction(WallpaperSeedSourceDescriptor source)
+ {
+ if (string.Equals(source.SourceKind, "fallback", StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(source.SourceKind, "app_solid", StringComparison.OrdinalIgnoreCase))
+ {
+ return;
+ }
+
+ lock (_gate)
+ {
+ if (_pendingSeedKeys.Contains(source.SourceKey))
+ {
+ return;
+ }
+
+ _pendingSeedKeys.Add(source.SourceKey);
+ }
+
+ _ = Task.Run(() =>
+ {
+ WallpaperSeedExtractionResult? extractionResult = null;
+
+ try
+ {
+ extractionResult = ExtractSeedCandidates(source);
+ }
+ catch (Exception ex)
+ {
+ AppLogger.Warn(
+ "Appearance.WallpaperSeed",
+ $"Failed to build wallpaper seed candidates asynchronously. Source='{source.SourceKind}'; Path='{source.FilePath}'.",
+ ex);
+ }
+ finally
+ {
+ lock (_gate)
+ {
+ _pendingSeedKeys.Remove(source.SourceKey);
+ if (extractionResult is not null)
+ {
+ _seedCache[source.SourceKey] = extractionResult;
+ }
+ }
+ }
+
+ if (extractionResult is not null)
+ {
+ _notifyChanged(false);
+ }
+ });
+ }
+
+ private WallpaperSeedExtractionResult ExtractSeedCandidates(WallpaperSeedSourceDescriptor source)
+ {
+ IReadOnlyList seedCandidates = source.SourceKind switch
+ {
+ "app_wallpaper" or "system_wallpaper" => ExtractImageSeedCandidates(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 WallpaperPaletteResolution BuildResolution(
+ 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 BuildFallbackResolution(bool nightMode, string? resolvedWallpaperPath)
+ {
+ var palette = _monetColorService.BuildPaletteFromSeedCandidates([], nightMode, NeutralFallbackSeedColor);
+ return new WallpaperPaletteResolution(
+ palette,
+ [],
+ "fallback",
+ palette.Seed,
+ resolvedWallpaperPath);
+ }
+
+ 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());
+ }
+}
diff --git a/LanMountainDesktop/Services/WindowMaterialService.cs b/LanMountainDesktop/Services/WindowMaterialService.cs
new file mode 100644
index 0000000..b98fe91
--- /dev/null
+++ b/LanMountainDesktop/Services/WindowMaterialService.cs
@@ -0,0 +1,193 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Controls;
+using Avalonia.Media;
+using LanMountainDesktop.Services.Settings;
+using Microsoft.Win32;
+
+namespace LanMountainDesktop.Services;
+
+internal sealed class WindowMaterialService : IWindowMaterialService
+{
+ private const int Windows11Build = 22000;
+ private const int Windows11_24H2Build = 26100;
+
+ public bool CanChangeMode => GetAvailableModes().Count > 1;
+
+ public IReadOnlyList GetAvailableModes()
+ {
+ return GetSupportProfile() switch
+ {
+ WindowMaterialSupportProfile.FullSwitching =>
+ [
+ ThemeAppearanceValues.MaterialAuto,
+ ThemeAppearanceValues.MaterialNone,
+ ThemeAppearanceValues.MaterialMica,
+ ThemeAppearanceValues.MaterialAcrylic
+ ],
+ WindowMaterialSupportProfile.FixedMica =>
+ [
+ ThemeAppearanceValues.MaterialAuto,
+ ThemeAppearanceValues.MaterialNone,
+ ThemeAppearanceValues.MaterialMica
+ ],
+ WindowMaterialSupportProfile.FixedAcrylic =>
+ [
+ ThemeAppearanceValues.MaterialAuto,
+ ThemeAppearanceValues.MaterialNone,
+ ThemeAppearanceValues.MaterialAcrylic
+ ],
+ _ =>
+ [
+ ThemeAppearanceValues.MaterialAuto,
+ ThemeAppearanceValues.MaterialNone
+ ]
+ };
+ }
+
+ public void Apply(Window window, string materialMode)
+ {
+ ArgumentNullException.ThrowIfNull(window);
+
+ var normalizedMode = ThemeAppearanceValues.NormalizeSystemMaterialMode(materialMode);
+ var supportProfile = GetSupportProfile();
+ var effectiveMode = normalizedMode == ThemeAppearanceValues.MaterialAuto
+ ? ResolveAutoMaterialMode(supportProfile)
+ : normalizedMode;
+
+ if (effectiveMode == ThemeAppearanceValues.MaterialNone)
+ {
+ window.Background = Brushes.White;
+ window.TransparencyLevelHint = [WindowTransparencyLevel.None];
+ return;
+ }
+
+ window.Background = Brushes.Transparent;
+
+ if (supportProfile == WindowMaterialSupportProfile.NoneOnly)
+ {
+ window.TransparencyLevelHint =
+ [
+ WindowTransparencyLevel.None
+ ];
+ return;
+ }
+
+ window.TransparencyLevelHint = normalizedMode == ThemeAppearanceValues.MaterialAuto
+ ? ResolveAutoTransparencyLevels(supportProfile)
+ : effectiveMode switch
+ {
+ ThemeAppearanceValues.MaterialMica =>
+ [
+ WindowTransparencyLevel.Mica,
+ WindowTransparencyLevel.Blur,
+ WindowTransparencyLevel.None
+ ],
+ ThemeAppearanceValues.MaterialAcrylic =>
+ [
+ WindowTransparencyLevel.AcrylicBlur,
+ WindowTransparencyLevel.Blur,
+ WindowTransparencyLevel.None
+ ],
+ _ =>
+ [
+ WindowTransparencyLevel.None
+ ]
+ };
+ }
+
+ private static string ResolveAutoMaterialMode(WindowMaterialSupportProfile supportProfile)
+ {
+ return supportProfile switch
+ {
+ WindowMaterialSupportProfile.FullSwitching or WindowMaterialSupportProfile.FixedMica =>
+ ThemeAppearanceValues.MaterialMica,
+ WindowMaterialSupportProfile.FixedAcrylic =>
+ ThemeAppearanceValues.MaterialAcrylic,
+ _ => ThemeAppearanceValues.MaterialNone
+ };
+ }
+
+ private static IReadOnlyList ResolveAutoTransparencyLevels(WindowMaterialSupportProfile supportProfile)
+ {
+ return supportProfile switch
+ {
+ WindowMaterialSupportProfile.FullSwitching or WindowMaterialSupportProfile.FixedMica =>
+ [
+ WindowTransparencyLevel.Mica,
+ WindowTransparencyLevel.AcrylicBlur,
+ WindowTransparencyLevel.Blur,
+ WindowTransparencyLevel.None
+ ],
+ WindowMaterialSupportProfile.FixedAcrylic =>
+ [
+ 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
+ }
+}
diff --git a/LanMountainDesktop/ViewModels/MaterialColorSettingsPageViewModel.cs b/LanMountainDesktop/ViewModels/MaterialColorSettingsPageViewModel.cs
index c214cb0..1a45ec7 100644
--- a/LanMountainDesktop/ViewModels/MaterialColorSettingsPageViewModel.cs
+++ b/LanMountainDesktop/ViewModels/MaterialColorSettingsPageViewModel.cs
@@ -542,17 +542,17 @@ public sealed partial class MaterialColorSettingsPageViewModel : ViewModelBase
SourceStatusHeader = L("settings.material_color.source_status.header", "Resolved source");
SemanticColorsHeader = L("settings.material_color.semantic.header", "Semantic colors");
SurfacesHeader = L("settings.material_color.surfaces.header", "Material surfaces");
- WallpaperSeedCurrentText = L("settings.appearance.preview.wallpaper_current", "Current");
- ModeNeutralText = L("settings.appearance.theme_color_mode.neutral", "Default neutral");
- ModeCustomText = L("settings.appearance.theme_color_mode.user", "User theme color Monet");
- ModeWallpaperText = L("settings.appearance.theme_color_mode.wallpaper", "Wallpaper Monet");
+ WallpaperSeedCurrentText = L("settings.material_color.preview.wallpaper_current", "Current");
+ ModeNeutralText = L("settings.material_color.theme_color_mode.neutral", "Default neutral");
+ ModeCustomText = L("settings.material_color.theme_color_mode.user", "User theme color Monet");
+ ModeWallpaperText = L("settings.material_color.theme_color_mode.wallpaper", "Wallpaper Monet");
WallpaperSourceAutoText = L("settings.material_color.wallpaper_source.auto", "Auto");
WallpaperSourceAppText = L("settings.material_color.wallpaper_source.app", "App wallpaper");
WallpaperSourceSystemText = L("settings.material_color.wallpaper_source.system", "System wallpaper");
- MaterialNoneText = L("settings.appearance.system_material.none", "None");
- MaterialAutoText = L("settings.appearance.system_material.auto", "Auto");
- MaterialMicaText = L("settings.appearance.system_material.mica", "Mica");
- MaterialAcrylicText = L("settings.appearance.system_material.acrylic", "Acrylic");
+ MaterialNoneText = L("settings.material_color.system_material.none", "None");
+ MaterialAutoText = L("settings.material_color.system_material.auto", "Auto");
+ MaterialMicaText = L("settings.material_color.system_material.mica", "Mica");
+ MaterialAcrylicText = L("settings.material_color.system_material.acrylic", "Acrylic");
}
private string L(string key, string fallback)
diff --git a/LanMountainDesktop/ViewModels/WallpaperSettingsPageViewModel.cs b/LanMountainDesktop/ViewModels/WallpaperSettingsPageViewModel.cs
index d4a122f..96a7047 100644
--- a/LanMountainDesktop/ViewModels/WallpaperSettingsPageViewModel.cs
+++ b/LanMountainDesktop/ViewModels/WallpaperSettingsPageViewModel.cs
@@ -20,6 +20,7 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
private readonly LocalizationService _localizationService = new();
private readonly string _languageCode;
private bool _isInitializing;
+ private int _systemWallpaperRefreshIntervalSeconds = 300;
public WallpaperSettingsPageViewModel(ISettingsFacadeService settingsFacade)
{
@@ -28,7 +29,6 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
_languageCode = _localizationService.NormalizeLanguageCode(_settingsFacade.Region.Get().LanguageCode);
WallpaperPlacements = CreateWallpaperPlacements();
WallpaperTypes = CreateWallpaperTypes();
- RefreshIntervals = CreateRefreshIntervals();
PresetColors = CreatePresetColors();
RefreshLocalizedText();
@@ -39,7 +39,6 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
public IReadOnlyList WallpaperPlacements { get; }
public IReadOnlyList WallpaperTypes { get; }
- public IReadOnlyList RefreshIntervals { get; }
public IReadOnlyList PresetColors { get; }
public bool IsSystemWallpaperSupported => RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
@@ -56,9 +55,6 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
[ObservableProperty]
private SelectionOption _selectedWallpaperPlacement = null!;
- [ObservableProperty]
- private SelectionOption _selectedRefreshInterval = null!;
-
[ObservableProperty]
private string _wallpaperHeader = string.Empty;
@@ -83,18 +79,6 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
[ObservableProperty]
private string _filePickerTitle = string.Empty;
- [ObservableProperty]
- private string _systemWallpaperLabel = string.Empty;
-
- [ObservableProperty]
- private string _refreshIntervalLabel = string.Empty;
-
- [ObservableProperty]
- private string _refreshButtonTooltip = string.Empty;
-
- [ObservableProperty]
- private string _systemWallpaperStatus = string.Empty;
-
[ObservableProperty]
private bool _isImageOrVideo;
@@ -126,6 +110,7 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
SelectedWallpaperType = WallpaperTypes.FirstOrDefault(t => t.Value == wallpaper.Type) ?? WallpaperTypes[0];
SelectedColor = wallpaper.Color ?? PresetColors[0];
+ UpdateCustomColorPreview(SelectedColor);
var wallpaperPlacement = string.IsNullOrWhiteSpace(wallpaper.Placement)
? "Fill"
@@ -134,20 +119,10 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
string.Equals(option.Value, wallpaperPlacement, StringComparison.OrdinalIgnoreCase))
?? WallpaperPlacements[0];
- var refreshIntervalSeconds = wallpaper.SystemWallpaperRefreshIntervalSeconds;
- SelectedRefreshInterval = RefreshIntervals.FirstOrDefault(option =>
- GetIntervalSeconds(option.Value) == refreshIntervalSeconds)
- ?? RefreshIntervals[2];
-
- if (!string.IsNullOrWhiteSpace(wallpaper.CustomColor) && Color.TryParse(wallpaper.CustomColor, out var customColor))
- {
- CustomColor = customColor;
- CustomColorBrush = new SolidColorBrush(customColor);
- }
+ _systemWallpaperRefreshIntervalSeconds = wallpaper.SystemWallpaperRefreshIntervalSeconds;
UpdateVisibility();
UpdatePreviewFromCurrentSelection();
- UpdateSystemWallpaperStatus();
}
partial void OnSelectedWallpaperTypeChanged(SelectionOption value)
@@ -168,6 +143,7 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
partial void OnSelectedColorChanged(string? value)
{
+ UpdateCustomColorPreview(value);
if (_isInitializing) return;
SaveWallpaper();
}
@@ -181,12 +157,6 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
SaveWallpaper();
}
- partial void OnSelectedRefreshIntervalChanged(SelectionOption value)
- {
- if (_isInitializing) return;
- SaveWallpaper();
- }
-
public async Task ImportWallpaperAsync(string sourcePath)
{
var importedPath = await _settingsFacade.WallpaperMedia.ImportAssetAsync(sourcePath);
@@ -227,11 +197,9 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
if (string.IsNullOrWhiteSpace(systemPath))
{
ClearPreviewImage();
- SystemWallpaperStatus = L("settings.wallpaper.system.unavailable", "Unable to read system wallpaper");
return;
}
- SystemWallpaperStatus = systemPath;
UpdatePreviewImage(systemPath);
}
@@ -270,16 +238,13 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
previousPreview?.Dispose();
}
- private void UpdateSystemWallpaperStatus()
+ private void UpdateCustomColorPreview(string? value)
{
- if (!IsSystemWallpaper) return;
- UpdateSystemWallpaperPreview();
- }
-
- [RelayCommand]
- private void RefreshSystemWallpaper()
- {
- UpdateSystemWallpaperPreview();
+ if (!string.IsNullOrWhiteSpace(value) && Color.TryParse(value, out var parsed))
+ {
+ CustomColor = parsed;
+ CustomColorBrush = new SolidColorBrush(parsed);
+ }
}
partial void OnSelectedWallpaperPlacementChanged(SelectionOption value)
@@ -303,8 +268,10 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
{
var selectedType = SelectedWallpaperType?.Value ?? "Image";
var selectedPlacement = SelectedWallpaperPlacement?.Value ?? WallpaperImageBrushFactory.Fill;
- var refreshIntervalSeconds = GetIntervalSeconds(SelectedRefreshInterval?.Value);
-
+ var selectedColor = string.IsNullOrWhiteSpace(SelectedColor)
+ ? PresetColors[0]
+ : SelectedColor.Trim();
+
string? normalizedPath;
if (selectedType == "SolidColor" || selectedType == "SystemWallpaper")
{
@@ -315,34 +282,12 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
normalizedPath = string.IsNullOrWhiteSpace(WallpaperPath) ? null : WallpaperPath;
}
- var customColorHex = $"#{CustomColor.A:X2}{CustomColor.R:X2}{CustomColor.G:X2}{CustomColor.B:X2}";
_settingsFacade.Wallpaper.Save(new WallpaperSettingsState(
normalizedPath,
selectedType,
- SelectedColor,
+ selectedColor,
selectedPlacement,
- customColorHex,
- refreshIntervalSeconds));
- }
-
- private static int GetIntervalSeconds(string? value)
- {
- return value switch
- {
- "30s" => 30,
- "1m" => 60,
- "5m" => 300,
- "10m" => 600,
- "15m" => 900,
- "30m" => 1800,
- "1h" => 3600,
- "2h" => 7200,
- "4h" => 14400,
- "8h" => 28800,
- "12h" => 43200,
- "24h" => 86400,
- _ => 300
- };
+ _systemWallpaperRefreshIntervalSeconds));
}
private IReadOnlyList CreateWallpaperPlacements()
@@ -373,25 +318,6 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
return types;
}
- private IReadOnlyList CreateRefreshIntervals()
- {
- return
- [
- new SelectionOption("30s", L("settings.wallpaper.refresh.30s", "30 seconds")),
- new SelectionOption("1m", L("settings.wallpaper.refresh.1m", "1 minute")),
- new SelectionOption("5m", L("settings.wallpaper.refresh.5m", "5 minutes")),
- new SelectionOption("10m", L("settings.wallpaper.refresh.10m", "10 minutes")),
- new SelectionOption("15m", L("settings.wallpaper.refresh.15m", "15 minutes")),
- new SelectionOption("30m", L("settings.wallpaper.refresh.30m", "30 minutes")),
- new SelectionOption("1h", L("settings.wallpaper.refresh.1h", "1 hour")),
- new SelectionOption("2h", L("settings.wallpaper.refresh.2h", "2 hours")),
- new SelectionOption("4h", L("settings.wallpaper.refresh.4h", "4 hours")),
- new SelectionOption("8h", L("settings.wallpaper.refresh.8h", "8 hours")),
- new SelectionOption("12h", L("settings.wallpaper.refresh.12h", "12 hours")),
- new SelectionOption("24h", L("settings.wallpaper.refresh.24h", "24 hours"))
- ];
- }
-
private IReadOnlyList CreatePresetColors()
{
return
@@ -412,9 +338,6 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
WallpaperPlacementDescription = L("settings.wallpaper.placement_desc", "Adjust how the image fills the desktop.");
ImportWallpaperButtonText = L("settings.wallpaper.pick_button", "Import Wallpaper");
FilePickerTitle = L("filepicker.title", "Select wallpaper");
- SystemWallpaperLabel = L("settings.wallpaper.system.label", "System Wallpaper");
- RefreshIntervalLabel = L("settings.wallpaper.refresh_interval", "Refresh Interval");
- RefreshButtonTooltip = L("settings.wallpaper.refresh_now", "Refresh Now");
}
private string L(string key, string fallback)
diff --git a/LanMountainDesktop/Views/NotificationDialogWindow.axaml.cs b/LanMountainDesktop/Views/NotificationDialogWindow.axaml.cs
index 62471ea..fa86bd8 100644
--- a/LanMountainDesktop/Views/NotificationDialogWindow.axaml.cs
+++ b/LanMountainDesktop/Views/NotificationDialogWindow.axaml.cs
@@ -9,6 +9,7 @@ using Avalonia.Styling;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using FluentIcons.Avalonia;
+using LanMountainDesktop.Models;
using LanMountainDesktop.Services;
namespace LanMountainDesktop.Views;
@@ -27,25 +28,32 @@ public partial class NotificationDialogWindow : Window
}
public void Initialize(NotificationContent content, IAppearanceThemeService? themeService = null)
+ {
+ Initialize(content, themeService is IMaterialColorService materialColorService
+ ? materialColorService.GetMaterialColorSnapshot()
+ : themeService is null
+ ? null
+ : HostMaterialColorProvider.GetOrCreate().GetMaterialColorSnapshot());
+ }
+
+ public void Initialize(NotificationContent content, MaterialColorSnapshot? materialColorSnapshot)
{
_viewModel = new NotificationDialogViewModel(content, this);
DataContext = _viewModel;
CompletionSource = new TaskCompletionSource();
- bool isNightMode = false;
- if (themeService is not null)
+ if (materialColorSnapshot is not null)
{
- var snapshot = themeService.GetCurrent();
- isNightMode = snapshot.IsNightMode;
- RequestedThemeVariant = isNightMode ? ThemeVariant.Dark : ThemeVariant.Light;
+ RequestedThemeVariant = materialColorSnapshot.IsNightMode ? ThemeVariant.Dark : ThemeVariant.Light;
}
- if (DialogCard is not null)
+ if (DialogCard is not null && materialColorSnapshot is not null)
{
- DialogCard.Background = isNightMode
- ? new SolidColorBrush(Color.Parse("#FF2D2D2D"))
- : new SolidColorBrush(Color.Parse("#FFF8F9FA"));
+ var cardSurface = GetDialogSurface(materialColorSnapshot);
+ DialogCard.Background = new SolidColorBrush(cardSurface.BackgroundColor);
+ DialogCard.BorderBrush = new SolidColorBrush(cardSurface.BorderColor);
+ DialogCard.BorderThickness = new Thickness(1);
}
if (!HasButtons(content) && content.Duration.HasValue)
@@ -59,6 +67,20 @@ public partial class NotificationDialogWindow : Window
}
}
+ private static MaterialSurfaceSnapshot GetDialogSurface(MaterialColorSnapshot materialColorSnapshot)
+ {
+ return materialColorSnapshot.Surfaces.TryGetValue(MaterialSurfaceRole.OverlayPanel, out var overlaySurface)
+ ? overlaySurface
+ : materialColorSnapshot.Surfaces.TryGetValue(MaterialSurfaceRole.WindowBackground, out var windowSurface)
+ ? windowSurface
+ : new MaterialSurfaceSnapshot(
+ MaterialSurfaceRole.WindowBackground,
+ Color.Parse("#FFF8F9FA"),
+ Color.Parse("#22000000"),
+ 0,
+ 1);
+ }
+
private static bool HasButtons(NotificationContent content)
{
return !string.IsNullOrEmpty(content.PrimaryButtonText) ||
diff --git a/LanMountainDesktop/Views/NotificationWindow.axaml.cs b/LanMountainDesktop/Views/NotificationWindow.axaml.cs
index 2e0ec64..ab8badd 100644
--- a/LanMountainDesktop/Views/NotificationWindow.axaml.cs
+++ b/LanMountainDesktop/Views/NotificationWindow.axaml.cs
@@ -8,6 +8,7 @@ using Avalonia.Input;
using Avalonia.Media;
using Avalonia.Styling;
using Avalonia.Threading;
+using LanMountainDesktop.Models;
using LanMountainDesktop.Services;
using LanMountainDesktop.Theme;
using LanMountainDesktop.ViewModels;
@@ -31,42 +32,58 @@ public partial class NotificationWindow : Window
}
public void Initialize(NotificationViewModel viewModel, IAppearanceThemeService? themeService = null)
+ {
+ Initialize(viewModel, themeService is IMaterialColorService materialColorService
+ ? materialColorService.GetMaterialColorSnapshot()
+ : themeService is null
+ ? null
+ : HostMaterialColorProvider.GetOrCreate().GetMaterialColorSnapshot());
+ }
+
+ public void Initialize(NotificationViewModel viewModel, MaterialColorSnapshot? materialColorSnapshot)
{
_viewModel = viewModel;
DataContext = viewModel;
-
+
_remainingDuration = viewModel.Duration;
-
- ApplyTheme(themeService);
+
+ ApplyTheme(materialColorSnapshot);
ApplySeverityColor();
}
- private void ApplyTheme(IAppearanceThemeService? themeService)
+ public void ApplyMaterialSnapshot(MaterialColorSnapshot materialColorSnapshot)
{
- if (themeService is null) return;
+ ApplyTheme(materialColorSnapshot);
+ ApplySeverityColor();
+ }
- var snapshot = themeService.GetCurrent();
- RequestedThemeVariant = snapshot.IsNightMode ? ThemeVariant.Dark : ThemeVariant.Light;
+ private void ApplyTheme(MaterialColorSnapshot? materialColorSnapshot)
+ {
+ // Notification windows must always stay transparent, regardless of whether
+ // we have a live material snapshot.
+ Background = Brushes.Transparent;
+ TransparencyLevelHint = [WindowTransparencyLevel.Transparent];
+
+ if (materialColorSnapshot is null) return;
+
+ RequestedThemeVariant = materialColorSnapshot.IsNightMode ? ThemeVariant.Dark : ThemeVariant.Light;
// Apply glass effect resources directly to window resources
// This ensures the notification card has proper background/border colors
- var context = CreateThemeContext(snapshot);
+ var context = CreateThemeContext(materialColorSnapshot);
GlassEffectService.ApplyGlassResources(Resources, context);
// IMPORTANT: Do NOT call ApplyWindowMaterial for notification windows!
// ApplyWindowMaterial sets Background to White when MaterialMode is "None",
// which causes the white border around the notification card.
- // Notification windows must always have transparent background.
- Background = Brushes.Transparent;
- TransparencyLevelHint = [WindowTransparencyLevel.Transparent];
}
- private ThemeColorContext CreateThemeContext(AppearanceThemeSnapshot snapshot)
+ private ThemeColorContext CreateThemeContext(MaterialColorSnapshot snapshot)
{
// Create theme context for glass effect resources
// Note: IsLightBackground and IsLightNavBackground are derived from IsNightMode
// UseNeutralSurfaces is determined by ThemeColorMode
- var useNeutralSurfaces = snapshot.ThemeColorMode == "Neutral";
+ var useNeutralSurfaces = snapshot.ThemeColorMode == ThemeAppearanceValues.ColorModeDefaultNeutral;
var monetColors = snapshot.WallpaperSeedCandidates;
return new ThemeColorContext(
diff --git a/LanMountainDesktop/Views/SettingsPages/WallpaperSettingsPage.axaml b/LanMountainDesktop/Views/SettingsPages/WallpaperSettingsPage.axaml
index f76fe93..a3d0723 100644
--- a/LanMountainDesktop/Views/SettingsPages/WallpaperSettingsPage.axaml
+++ b/LanMountainDesktop/Views/SettingsPages/WallpaperSettingsPage.axaml
@@ -3,7 +3,6 @@
xmlns:vm="using:LanMountainDesktop.ViewModels"
xmlns:controls="using:LanMountainDesktop.Controls"
xmlns:ui="using:FluentAvalonia.UI.Controls"
- xmlns:fi="using:FluentIcons.Avalonia"
x:Class="LanMountainDesktop.Views.SettingsPages.WallpaperSettingsPage"
x:DataType="vm:WallpaperSettingsPageViewModel">
@@ -180,21 +179,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/LanMountainDesktop/plugins/PluginLoader.cs b/LanMountainDesktop/plugins/PluginLoader.cs
index 74bf1de..503cbed 100644
--- a/LanMountainDesktop/plugins/PluginLoader.cs
+++ b/LanMountainDesktop/plugins/PluginLoader.cs
@@ -335,6 +335,7 @@ public sealed class PluginLoader
RegisterHostService(services, hostServices);
RegisterHostService(services, hostServices);
RegisterHostService(services, hostServices);
+ // Legacy compatibility only. Normal plugin appearance snapshots come from IMaterialColorService.
RegisterHostService(services, hostServices);
RegisterHostService(services, hostServices);
RegisterHostService(services, hostServices);
@@ -344,20 +345,18 @@ public sealed class PluginLoader
private static PluginAppearanceSnapshot BuildAppearanceSnapshot(IServiceProvider? hostServices)
{
- var defaultSnapshot = new PluginAppearanceSnapshot(
- CornerRadiusTokens: new PluginCornerRadiusTokens(6, 12, 14, 20, 28, 32, 36, 24),
- ThemeVariant: "Unknown");
+ var defaultSnapshot = CreateDefaultAppearanceSnapshot();
try
{
- if (hostServices?.GetService(typeof(IMaterialColorService)) is IMaterialColorService materialColorService)
+ if (TryBuildAppearanceSnapshotFromMaterialColorService(hostServices, out var snapshot))
{
- return PluginAppearanceSnapshotMapper.FromMaterialColorSnapshot(materialColorService.GetMaterialColorSnapshot());
+ return snapshot;
}
- if (hostServices?.GetService(typeof(IAppearanceThemeService)) is IAppearanceThemeService appearanceThemeService)
+ if (TryBuildCompatibilityAppearanceSnapshotFromAppearanceThemeService(hostServices, out snapshot))
{
- return PluginAppearanceSnapshotMapper.FromAppearanceSnapshot(appearanceThemeService.GetCurrent());
+ return snapshot;
}
return defaultSnapshot;
@@ -369,6 +368,41 @@ public sealed class PluginLoader
}
}
+ private static bool TryBuildAppearanceSnapshotFromMaterialColorService(
+ IServiceProvider? hostServices,
+ out PluginAppearanceSnapshot snapshot)
+ {
+ snapshot = default!;
+ if (hostServices?.GetService(typeof(IMaterialColorService)) is not IMaterialColorService materialColorService)
+ {
+ return false;
+ }
+
+ snapshot = PluginAppearanceSnapshotMapper.FromMaterialColorSnapshot(materialColorService.GetMaterialColorSnapshot());
+ return true;
+ }
+
+ private static bool TryBuildCompatibilityAppearanceSnapshotFromAppearanceThemeService(
+ IServiceProvider? hostServices,
+ out PluginAppearanceSnapshot snapshot)
+ {
+ snapshot = default!;
+ if (hostServices?.GetService(typeof(IAppearanceThemeService)) is not IAppearanceThemeService appearanceThemeService)
+ {
+ return false;
+ }
+
+ snapshot = PluginAppearanceSnapshotMapper.FromCompatibilityAppearanceSnapshot(appearanceThemeService.GetCurrent());
+ return true;
+ }
+
+ private static PluginAppearanceSnapshot CreateDefaultAppearanceSnapshot()
+ {
+ return new PluginAppearanceSnapshot(
+ CornerRadiusTokens: new PluginCornerRadiusTokens(6, 12, 14, 20, 28, 32, 36, 24),
+ ThemeVariant: "Unknown");
+ }
+
private static void RegisterHostService(IServiceCollection services, IServiceProvider? hostServices)
where TService : class
{
diff --git a/LanMountainDesktop/plugins/PluginRuntimeService.cs b/LanMountainDesktop/plugins/PluginRuntimeService.cs
index d73c867..397fa74 100644
--- a/LanMountainDesktop/plugins/PluginRuntimeService.cs
+++ b/LanMountainDesktop/plugins/PluginRuntimeService.cs
@@ -1103,6 +1103,7 @@ public sealed class PluginRuntimeService : IDisposable
if (serviceType == typeof(IAppearanceThemeService))
{
+ // Compatibility-only. Plugin appearance snapshots are still sourced from the material pipeline.
return _appearanceThemeService;
}