diff --git a/.trae/specs/material-color-service/checklist.md b/.trae/specs/material-color-service/checklist.md
new file mode 100644
index 0000000..d56410f
--- /dev/null
+++ b/.trae/specs/material-color-service/checklist.md
@@ -0,0 +1,12 @@
+# Material Color Service Acceptance Checklist
+
+- [x] `dotnet build LanMountainDesktop.slnx -c Debug` succeeds.
+- [x] `dotnet test LanMountainDesktop.slnx -c Debug` succeeds.
+- [x] Material & Color page exposes color source, wallpaper source, system material, native event preference, polling interval, manual refresh, semantic color preview, and surface preview.
+- [x] Appearance page no longer owns duplicate visible color/material controls.
+- [x] Appearance page view model preserves Material & Color settings instead of rewriting them.
+- [x] Component corner-radius settings preserve Material & Color fields instead of resetting them through old positional constructors.
+- [x] Component editor receives colors from `MaterialColorSnapshot`.
+- [x] Plugin SDK snapshot includes read-only color/material fields without breaking the existing constructor shape.
+- [x] Wallpaper source selection supports auto, app, and system modes.
+- [x] Native wallpaper event monitoring can be disabled and polling remains available.
diff --git a/.trae/specs/material-color-service/spec.md b/.trae/specs/material-color-service/spec.md
new file mode 100644
index 0000000..91f3d7e
--- /dev/null
+++ b/.trae/specs/material-color-service/spec.md
@@ -0,0 +1,62 @@
+# Material Color Service
+
+## Goal
+
+Unify Monet seed extraction, wallpaper color extraction, semantic color roles, host material surfaces, and plugin appearance snapshots behind one host-owned material/color source of truth.
+
+## Scope
+
+- Host service: `IMaterialColorService`
+- Compatibility facade: `IAppearanceThemeService`
+- Settings page: `MaterialColorSettingsPage`
+- Persisted settings:
+ - `ThemeColorMode`
+ - `ThemeColor`
+ - `SelectedWallpaperSeed`
+ - `SystemMaterialMode`
+ - `ThemeWallpaperColorSource`
+ - `UseNativeWallpaperChangeEvents`
+ - `SystemWallpaperRefreshIntervalSeconds`
+- Plugin read-only appearance snapshot fields:
+ - accent color
+ - seed color
+ - color source
+ - system material mode
+ - semantic color roles
+ - material surfaces
+ - wallpaper seed candidates
+
+## Behavior
+
+`IMaterialColorService` owns the live `MaterialColorSnapshot`. Consumers should derive colors and material values from this snapshot instead of recalculating from raw theme settings, wallpaper settings, or `MonetPalette`.
+
+Supported color sources:
+
+- `default_neutral`: stable neutral surfaces with the default accent.
+- `seed_monet`: user-selected seed color processed through Monet.
+- `wallpaper_monet`: wallpaper colors processed through Monet.
+
+Wallpaper color source selection:
+
+- `auto`: app wallpaper or app solid color first, then system wallpaper, then fallback.
+- `app`: app wallpaper or app solid color only, then fallback.
+- `system`: system wallpaper only, then fallback.
+
+System wallpaper monitoring:
+
+- Native Windows user preference events are preferred when enabled and available.
+- Polling remains active as the fallback path.
+- Manual refresh clears cached wallpaper candidates and rebuilds the snapshot.
+
+## Refactor Rules
+
+- New consumers must depend on `IMaterialColorService`, not on parallel combinations of theme settings, wallpaper settings, and `MonetColorService`.
+- `MonetColorService` remains the extraction/palette utility, not the application-wide coordinator.
+- Component/editor/plugin appearance code must consume `MaterialColorSnapshot` or a mapper produced from it.
+- Existing `IAppearanceThemeService` remains available for compatibility, but it must not become a second source of truth.
+
+## Out Of Scope
+
+- Plugin write access to global host appearance settings.
+- Market metadata or sample plugin changes.
+- Replacing the wallpaper picker page. It remains the asset/source management page.
diff --git a/.trae/specs/material-color-service/tasks.md b/.trae/specs/material-color-service/tasks.md
new file mode 100644
index 0000000..34f8121
--- /dev/null
+++ b/.trae/specs/material-color-service/tasks.md
@@ -0,0 +1,13 @@
+# Material Color Service Tasks
+
+- [x] Add unified material/color snapshot models and `IMaterialColorService`.
+- [x] Persist wallpaper color source and native wallpaper event preference.
+- [x] Add the Material & Color settings page.
+- [x] Keep Appearance focused on theme mode, window chrome, and corner radius.
+- [x] Route plugin appearance snapshots through the material/color snapshot.
+- [x] Route component editor theming through the material/color snapshot.
+- [x] Remove legacy color/material preview and save logic from the Appearance page view model.
+- [x] Replace legacy positional `ThemeAppearanceSettingsState` writes with preserving `with` updates where found.
+- [x] Keep native wallpaper events optional with polling/manual refresh fallback.
+- [x] Add regression tests for normalization, plugin mapping, and component editor palette mapping.
+- [ ] Continue retiring legacy direct consumers of raw theme/wallpaper/Monet tuples when they are touched.
diff --git a/LanMountainDesktop.PluginSdk/AppearanceChangedEvent.cs b/LanMountainDesktop.PluginSdk/AppearanceChangedEvent.cs
index 6cb8bf5..244bb55 100644
--- a/LanMountainDesktop.PluginSdk/AppearanceChangedEvent.cs
+++ b/LanMountainDesktop.PluginSdk/AppearanceChangedEvent.cs
@@ -1,15 +1,10 @@
namespace LanMountainDesktop.PluginSdk;
///
-/// 外观变更事件参数,当主题、圆角或其他外观属性变化时触发。
+/// Provides the latest read-only appearance snapshot when host appearance values change.
///
public sealed class AppearanceChangedEvent : EventArgs
{
- ///
- /// 创建外观变更事件实例。
- ///
- /// 当前外观快照
- /// 变更的属性集合
public AppearanceChangedEvent(
PluginAppearanceSnapshot snapshot,
IReadOnlyCollection changedProperties)
@@ -21,89 +16,50 @@ public sealed class AppearanceChangedEvent : EventArgs
ChangedProperties = changedProperties;
}
- ///
- /// 当前外观快照。
- ///
public PluginAppearanceSnapshot Snapshot { get; }
- ///
- /// 变更的属性集合。
- ///
public IReadOnlyCollection ChangedProperties { get; }
- ///
- /// 圆角是否发生变化。
- ///
- public bool CornerRadiusChanged => ChangedProperties.Contains(AppearanceProperty.CornerRadius);
+ public bool CornerRadiusChanged => HasChanged(AppearanceProperty.CornerRadius);
- ///
- /// 主题变体(亮色/暗色)是否发生变化。
- ///
- public bool ThemeVariantChanged => ChangedProperties.Contains(AppearanceProperty.ThemeVariant);
+ public bool ThemeVariantChanged => HasChanged(AppearanceProperty.ThemeVariant);
- ///
- /// 强调色是否发生变化。
- ///
- public bool AccentColorChanged => ChangedProperties.Contains(AppearanceProperty.AccentColor);
+ public bool AccentColorChanged => HasChanged(AppearanceProperty.AccentColor);
- ///
- /// 圆角风格是否发生变化。
- ///
- public bool CornerRadiusStyleChanged => ChangedProperties.Contains(AppearanceProperty.CornerRadiusStyle);
+ public bool CornerRadiusStyleChanged => HasChanged(AppearanceProperty.CornerRadiusStyle);
+
+ public bool WallpaperChanged => HasChanged(AppearanceProperty.Wallpaper);
+
+ public bool SystemMaterialModeChanged => HasChanged(AppearanceProperty.SystemMaterialMode);
+
+ public bool ColorSourceChanged => HasChanged(AppearanceProperty.ColorSource);
+
+ public bool ColorRolesChanged => HasChanged(AppearanceProperty.ColorRoles);
+
+ public bool MaterialSurfacesChanged => HasChanged(AppearanceProperty.MaterialSurfaces);
+
+ public bool WallpaperSeedCandidatesChanged => HasChanged(AppearanceProperty.WallpaperSeedCandidates);
- ///
- /// 检查指定属性是否发生变化。
- ///
- /// 要检查的属性
- /// 如果属性发生变化则返回 true
public bool HasChanged(AppearanceProperty property)
{
- return ChangedProperties.Contains(property);
+ return ChangedProperties.Contains(AppearanceProperty.All) ||
+ ChangedProperties.Contains(property);
}
- ///
- /// 检查是否有任何外观属性发生变化。
- ///
public bool HasAnyChanges => ChangedProperties.Count > 0;
}
-///
-/// 可变更的外观属性枚举。
-///
public enum AppearanceProperty
{
- ///
- /// 圆角Token值发生变化。
- ///
CornerRadius,
-
- ///
- /// 主题变体(亮色/暗色)发生变化。
- ///
ThemeVariant,
-
- ///
- /// 强调色发生变化。
- ///
AccentColor,
-
- ///
- /// 圆角风格(Sharp/Balanced/Rounded/Open)发生变化。
- ///
CornerRadiusStyle,
-
- ///
- /// 壁纸发生变化。
- ///
Wallpaper,
-
- ///
- /// 系统材质模式发生变化。
- ///
SystemMaterialMode,
-
- ///
- /// 所有外观属性(用于批量更新)。
- ///
+ ColorSource,
+ ColorRoles,
+ MaterialSurfaces,
+ WallpaperSeedCandidates,
All
}
diff --git a/LanMountainDesktop.Tests/AppearanceSettingsPageViewModelTests.cs b/LanMountainDesktop.Tests/AppearanceSettingsPageViewModelTests.cs
new file mode 100644
index 0000000..ef5c245
--- /dev/null
+++ b/LanMountainDesktop.Tests/AppearanceSettingsPageViewModelTests.cs
@@ -0,0 +1,177 @@
+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 AppearanceSettingsPageViewModelTests
+{
+ [Fact]
+ public void ChangingThemeMode_PreservesMaterialColorSettings()
+ {
+ var initialState = new ThemeAppearanceSettingsState(
+ IsNightMode: false,
+ ThemeColor: "#ff123456",
+ UseSystemChrome: false,
+ CornerRadiusStyle: GlobalAppearanceSettings.CornerRadiusStyleRounded,
+ ThemeColorMode: ThemeAppearanceValues.ColorModeWallpaperMonet,
+ SystemMaterialMode: ThemeAppearanceValues.MaterialMica,
+ SelectedWallpaperSeed: "#ff654321",
+ ThemeMode: ThemeAppearanceValues.ThemeModeLight,
+ ThemeWallpaperColorSource: ThemeAppearanceValues.WallpaperColorSourceSystem,
+ UseNativeWallpaperChangeEvents: false);
+ var facade = new FakeSettingsFacade(initialState);
+ var viewModel = new AppearanceSettingsPageViewModel(facade);
+
+ viewModel.SelectedThemeMode = viewModel.ThemeModeOptions.Single(option =>
+ option.Value == ThemeAppearanceValues.ThemeModeDark);
+
+ var saved = facade.ThemeState;
+ Assert.True(saved.IsNightMode);
+ Assert.Equal(ThemeAppearanceValues.ThemeModeDark, saved.ThemeMode);
+ Assert.Equal("#ff123456", saved.ThemeColor);
+ Assert.Equal(ThemeAppearanceValues.ColorModeWallpaperMonet, saved.ThemeColorMode);
+ Assert.Equal(ThemeAppearanceValues.MaterialMica, saved.SystemMaterialMode);
+ Assert.Equal("#ff654321", saved.SelectedWallpaperSeed);
+ Assert.Equal(ThemeAppearanceValues.WallpaperColorSourceSystem, saved.ThemeWallpaperColorSource);
+ Assert.False(saved.UseNativeWallpaperChangeEvents);
+ }
+
+ [Fact]
+ public void ChangingComponentCornerRadius_PreservesMaterialColorSettings()
+ {
+ var initialState = new ThemeAppearanceSettingsState(
+ IsNightMode: true,
+ ThemeColor: "#ffabcdef",
+ UseSystemChrome: true,
+ CornerRadiusStyle: GlobalAppearanceSettings.CornerRadiusStyleBalanced,
+ ThemeColorMode: ThemeAppearanceValues.ColorModeWallpaperMonet,
+ SystemMaterialMode: ThemeAppearanceValues.MaterialAcrylic,
+ SelectedWallpaperSeed: "#ff111111",
+ ThemeMode: ThemeAppearanceValues.ThemeModeDark,
+ ThemeWallpaperColorSource: ThemeAppearanceValues.WallpaperColorSourceApp,
+ UseNativeWallpaperChangeEvents: false);
+ var facade = new FakeSettingsFacade(initialState);
+ var viewModel = new ComponentsSettingsPageViewModel(facade);
+
+ viewModel.SelectedCornerRadiusStyle = viewModel.CornerRadiusStyleOptions.Single(option =>
+ option.Value == GlobalAppearanceSettings.CornerRadiusStyleOpen);
+
+ var saved = facade.ThemeState;
+ Assert.Equal(GlobalAppearanceSettings.CornerRadiusStyleOpen, saved.CornerRadiusStyle);
+ Assert.True(saved.IsNightMode);
+ Assert.Equal("#ffabcdef", saved.ThemeColor);
+ Assert.True(saved.UseSystemChrome);
+ Assert.Equal(ThemeAppearanceValues.ColorModeWallpaperMonet, saved.ThemeColorMode);
+ Assert.Equal(ThemeAppearanceValues.MaterialAcrylic, saved.SystemMaterialMode);
+ Assert.Equal("#ff111111", saved.SelectedWallpaperSeed);
+ Assert.Equal(ThemeAppearanceValues.ThemeModeDark, saved.ThemeMode);
+ Assert.Equal(ThemeAppearanceValues.WallpaperColorSourceApp, saved.ThemeWallpaperColorSource);
+ Assert.False(saved.UseNativeWallpaperChangeEvents);
+ }
+
+ private sealed class FakeSettingsFacade(ThemeAppearanceSettingsState themeState) : ISettingsFacadeService
+ {
+ private readonly FakeThemeAppearanceService _theme = new(themeState);
+ private readonly FakeRegionSettingsService _region = new();
+ private readonly FakeGridSettingsService _grid = new();
+
+ public ThemeAppearanceSettingsState ThemeState => _theme.State;
+
+ public IThemeAppearanceService Theme => _theme;
+
+ public IRegionSettingsService Region => _region;
+
+ public ISettingsService Settings => throw new NotSupportedException();
+ public ISettingsCatalog Catalog => throw new NotSupportedException();
+ public IGridSettingsService Grid => _grid;
+ public IWallpaperSettingsService Wallpaper => throw new NotSupportedException();
+ public IWallpaperMediaService WallpaperMedia => throw new NotSupportedException();
+ public IStatusBarSettingsService StatusBar => throw new NotSupportedException();
+ public ITextCapsuleSettingsService TextCapsule => throw new NotSupportedException();
+ public IWeatherSettingsService Weather => throw new NotSupportedException();
+ 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 FakeThemeAppearanceService(ThemeAppearanceSettingsState state) : IThemeAppearanceService
+ {
+ public ThemeAppearanceSettingsState State { get; private set; } = state;
+
+ public ThemeAppearanceSettingsState Get() => State;
+
+ public void Save(ThemeAppearanceSettingsState state)
+ {
+ 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 FakeGridSettingsService : IGridSettingsService
+ {
+ public GridSettingsState State { get; private set; } = new(12, "Relaxed", 18);
+
+ public GridSettingsState Get() => State;
+
+ public void Save(GridSettingsState state)
+ {
+ State = state;
+ }
+
+ public string NormalizeSpacingPreset(string? value)
+ {
+ return string.IsNullOrWhiteSpace(value) ? "Relaxed" : value;
+ }
+
+ public double ResolveGapRatio(string? preset)
+ {
+ _ = preset;
+ return 0.08;
+ }
+
+ public double CalculateEdgeInset(double hostWidth, double hostHeight, int shortSideCells, int insetPercent)
+ {
+ _ = hostWidth;
+ _ = hostHeight;
+ _ = shortSideCells;
+ return insetPercent;
+ }
+
+ public DesktopGridMetrics CalculateGridMetrics(
+ double hostWidth,
+ double hostHeight,
+ int shortSideCells,
+ double gapRatio,
+ double edgeInsetPx)
+ {
+ throw new NotSupportedException();
+ }
+ }
+}
diff --git a/LanMountainDesktop.Tests/MaterialColorIntegrationTests.cs b/LanMountainDesktop.Tests/MaterialColorIntegrationTests.cs
new file mode 100644
index 0000000..79716bf
--- /dev/null
+++ b/LanMountainDesktop.Tests/MaterialColorIntegrationTests.cs
@@ -0,0 +1,136 @@
+using Avalonia;
+using Avalonia.Media;
+using LanMountainDesktop.Models;
+using LanMountainDesktop.Services;
+using LanMountainDesktop.Shared.Contracts;
+using Xunit;
+
+namespace LanMountainDesktop.Tests;
+
+public sealed class MaterialColorIntegrationTests
+{
+ [Fact]
+ public void PluginMapper_ExposesUnifiedMaterialColorSnapshot()
+ {
+ var snapshot = CreateSnapshot();
+
+ var pluginSnapshot = PluginAppearanceSnapshotMapper.FromMaterialColorSnapshot(snapshot);
+
+ Assert.Equal("Dark", pluginSnapshot.ThemeVariant);
+ Assert.Equal(Color.Parse("#FF214365").ToString(), pluginSnapshot.AccentColor);
+ Assert.Equal(Color.Parse("#FF123456").ToString(), pluginSnapshot.SeedColor);
+ Assert.Equal(MaterialColorSourceKind.CustomSeed.ToString(), pluginSnapshot.ColorSource);
+ Assert.Equal(ThemeAppearanceValues.MaterialMica, pluginSnapshot.SystemMaterialMode);
+ Assert.Equal(Color.Parse("#FF214365").ToString(), pluginSnapshot.ColorRoles?["accent"]);
+ Assert.Equal(Color.Parse("#FF101820").ToString(), pluginSnapshot.MaterialSurfaces?["WindowBackground"].BackgroundColor);
+ Assert.Equal(Color.Parse("#FF123456").ToString(), Assert.Single(pluginSnapshot.WallpaperSeedCandidates ?? []));
+ }
+
+ [Fact]
+ public void ComponentEditorAdapter_UsesMaterialColorSnapshotAsSource()
+ {
+ var snapshot = CreateSnapshot();
+
+ var palette = ComponentEditorMaterialThemeAdapter.Build(snapshot);
+
+ Assert.Equal(snapshot.Palette.Primary, palette.PrimaryColor);
+ Assert.Equal(snapshot.Palette.Secondary, palette.SecondaryColor);
+ Assert.Equal(snapshot.Palette.OnAccent, palette.OnPrimaryColor);
+ Assert.Equal(snapshot.Surfaces[MaterialSurfaceRole.WindowBackground].BackgroundColor, palette.WindowBackgroundColor);
+ Assert.Equal(snapshot.Surfaces[MaterialSurfaceRole.OverlayPanel].BackgroundColor, palette.SurfaceContainerHighColor);
+ }
+
+ private static MaterialColorSnapshot CreateSnapshot()
+ {
+ 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);
+ }
+}
diff --git a/LanMountainDesktop.Tests/ThemeAppearanceValuesTests.cs b/LanMountainDesktop.Tests/ThemeAppearanceValuesTests.cs
index 0800682..1b194b6 100644
--- a/LanMountainDesktop.Tests/ThemeAppearanceValuesTests.cs
+++ b/LanMountainDesktop.Tests/ThemeAppearanceValuesTests.cs
@@ -26,4 +26,15 @@ public sealed class ThemeAppearanceValuesTests
Assert.Equal(ThemeAppearanceValues.MaterialNone, result[1]);
Assert.Contains(ThemeAppearanceValues.MaterialMica, result);
}
+
+ [Theory]
+ [InlineData("auto", ThemeAppearanceValues.WallpaperColorSourceAuto)]
+ [InlineData("APP", ThemeAppearanceValues.WallpaperColorSourceApp)]
+ [InlineData("system", ThemeAppearanceValues.WallpaperColorSourceSystem)]
+ [InlineData("unknown", ThemeAppearanceValues.WallpaperColorSourceAuto)]
+ [InlineData(null, ThemeAppearanceValues.WallpaperColorSourceAuto)]
+ public void NormalizeWallpaperColorSource_ReturnsKnownValue(string? input, string expected)
+ {
+ Assert.Equal(expected, ThemeAppearanceValues.NormalizeWallpaperColorSource(input));
+ }
}
diff --git a/LanMountainDesktop/Services/ComponentEditorMaterialThemeAdapter.cs b/LanMountainDesktop/Services/ComponentEditorMaterialThemeAdapter.cs
index 47a9006..c7bba52 100644
--- a/LanMountainDesktop/Services/ComponentEditorMaterialThemeAdapter.cs
+++ b/LanMountainDesktop/Services/ComponentEditorMaterialThemeAdapter.cs
@@ -1,9 +1,5 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
using Avalonia.Media;
using LanMountainDesktop.Models;
-using LanMountainDesktop.Services.Settings;
using LanMountainDesktop.Theme;
namespace LanMountainDesktop.Services;
@@ -28,89 +24,46 @@ internal sealed record ComponentEditorThemePalette(
internal static class ComponentEditorMaterialThemeAdapter
{
- private static readonly Color DefaultPrimary = Color.Parse("#FF6750A4");
- private static readonly Color DarkBackgroundBase = Color.Parse("#FF0B0F14");
- private static readonly Color DarkSurfaceBase = Color.Parse("#FF10161D");
- private static readonly Color DarkSurfaceContainerBase = Color.Parse("#FF151C24");
- private static readonly Color DarkSurfaceContainerHighBase = Color.Parse("#FF1A232D");
- private static readonly Color LightBackgroundBase = Color.Parse("#FFFCFCFF");
- private static readonly Color LightSurfaceBase = Color.Parse("#FFFFFFFF");
- private static readonly Color LightSurfaceContainerBase = Color.Parse("#FFF6F8FD");
- private static readonly Color LightSurfaceContainerHighBase = Color.Parse("#FFF0F4FA");
- private static readonly Color LightOnSurfaceBase = Color.Parse("#FF101316");
- private static readonly Color DarkOnSurfaceBase = Color.Parse("#FFF6F8FC");
+ private static readonly Color FallbackPrimary = Color.Parse("#FF6750A4");
- public static ComponentEditorThemePalette Build(
- ThemeAppearanceSettingsState themeState,
- WallpaperSettingsState wallpaperState,
- MonetPalette monetPalette,
- WallpaperMediaType wallpaperMediaType)
+ public static ComponentEditorThemePalette Build(MaterialColorSnapshot snapshot)
{
- ArgumentNullException.ThrowIfNull(monetPalette);
+ ArgumentNullException.ThrowIfNull(snapshot);
- var isNightMode = themeState.IsNightMode;
- var fallbackThemeColor = TryParseColor(themeState.ThemeColor);
- var useWallpaperPalette = wallpaperMediaType == WallpaperMediaType.Image && monetPalette.Primary.A > 0;
+ var palette = snapshot.Palette;
+ var isNightMode = snapshot.IsNightMode;
+ var primary = FirstUsable(palette.Primary, palette.Accent, snapshot.AccentColor, FallbackPrimary);
+ var secondary = FirstUsable(
+ palette.Secondary,
+ snapshot.MonetPalette.Secondary,
+ ColorMath.Blend(primary, isNightMode ? Colors.White : Color.Parse("#FF1F1B24"), isNightMode ? 0.18 : 0.16));
+ var tertiary = FirstUsable(
+ snapshot.MonetPalette.Tertiary,
+ ColorMath.Blend(ColorMath.Blend(primary, secondary, 0.5), isNightMode ? Colors.White : Color.Parse("#FF2A2230"), isNightMode ? 0.12 : 0.14));
- var primary = useWallpaperPalette
- ? monetPalette.Primary
- : fallbackThemeColor ?? monetPalette.Primary;
- if (primary == default)
- {
- primary = DefaultPrimary;
- }
-
- var secondary = ResolveSecondaryColor(primary, monetPalette, isNightMode);
- var tertiary = ResolveTertiaryColor(primary, secondary, monetPalette, isNightMode);
-
- var backgroundBase = isNightMode ? DarkBackgroundBase : LightBackgroundBase;
- var surfaceBase = isNightMode ? DarkSurfaceBase : LightSurfaceBase;
- var surfaceContainerBase = isNightMode ? DarkSurfaceContainerBase : LightSurfaceContainerBase;
- var surfaceContainerHighBase = isNightMode ? DarkSurfaceContainerHighBase : LightSurfaceContainerHighBase;
-
- var background = ColorMath.Blend(backgroundBase, primary, isNightMode ? 0.10 : 0.025);
- var surface = ColorMath.Blend(surfaceBase, primary, isNightMode ? 0.12 : 0.035);
- var surfaceContainer = ColorMath.Blend(surfaceContainerBase, primary, isNightMode ? 0.18 : 0.065);
- var surfaceContainerHigh = ColorMath.Blend(surfaceContainerHighBase, primary, isNightMode ? 0.24 : 0.09);
+ var windowBackground = GetSurfaceColor(snapshot, MaterialSurfaceRole.WindowBackground, palette.SurfaceBase);
+ var surface = FirstUsable(palette.SurfaceRaised, GetSurfaceColor(snapshot, MaterialSurfaceRole.SettingsWindowBackground, palette.SurfaceBase));
+ var surfaceContainer = FirstUsable(palette.SurfaceOverlay, GetSurfaceColor(snapshot, MaterialSurfaceRole.DesktopComponentHost, surface));
+ var surfaceContainerHigh = GetSurfaceColor(snapshot, MaterialSurfaceRole.OverlayPanel, surfaceContainer);
var topAppBar = ColorMath.Blend(surfaceContainerHigh, primary, isNightMode ? 0.10 : 0.06);
- var onSurfaceBase = isNightMode ? DarkOnSurfaceBase : LightOnSurfaceBase;
- var onSurface = ColorMath.EnsureContrast(onSurfaceBase, background, 7.0);
- var onSurfaceVariantBase = ColorMath.Blend(
- onSurface,
- surfaceContainer,
- isNightMode ? 0.30 : 0.42);
- var onSurfaceVariant = ColorMath.EnsureContrast(onSurfaceVariantBase, surfaceContainer, 4.5);
- var outlineBase = ColorMath.Blend(onSurface, surfaceContainer, isNightMode ? 0.74 : 0.82);
- var outline = Color.FromArgb(
- isNightMode ? (byte)0x66 : (byte)0x42,
- outlineBase.R,
- outlineBase.G,
- outlineBase.B);
- var divider = Color.FromArgb(
- isNightMode ? (byte)0x52 : (byte)0x26,
- outlineBase.R,
- outlineBase.G,
- outlineBase.B);
- var headerIconBackground = Color.FromArgb(
- isNightMode ? (byte)0x36 : (byte)0x1F,
- primary.R,
- primary.G,
- primary.B);
- var titleBarButtonHover = Color.FromArgb(
- isNightMode ? (byte)0x24 : (byte)0x12,
- onSurface.R,
- onSurface.G,
- onSurface.B);
- var onPrimaryBase = isNightMode ? Color.Parse("#FF111318") : Color.Parse("#FFFFFFFF");
- var onPrimary = ColorMath.EnsureContrast(onPrimaryBase, primary, 4.5);
+ var textPrimary = FirstUsable(palette.TextPrimary, isNightMode ? Colors.White : Color.Parse("#FF101316"));
+ var textSecondary = FirstUsable(palette.TextSecondary, palette.TextMuted, ColorMath.Blend(textPrimary, surfaceContainer, isNightMode ? 0.30 : 0.42));
+ var outline = FirstUsable(
+ GetSurfaceBorder(snapshot, MaterialSurfaceRole.DesktopComponentHost),
+ palette.ToggleBorder,
+ ColorMath.WithAlpha(ColorMath.Blend(textPrimary, surfaceContainer, isNightMode ? 0.74 : 0.82), isNightMode ? (byte)0x66 : (byte)0x42));
+ var divider = ColorMath.WithAlpha(outline, isNightMode ? (byte)0x52 : (byte)0x26);
+ var headerIconBackground = Color.FromArgb(isNightMode ? (byte)0x36 : (byte)0x1F, primary.R, primary.G, primary.B);
+ var titleBarButtonHover = Color.FromArgb(isNightMode ? (byte)0x24 : (byte)0x12, textPrimary.R, textPrimary.G, textPrimary.B);
+ var onPrimary = FirstUsable(palette.OnAccent, ColorMath.EnsureContrast(Colors.White, primary, 4.5));
return new ComponentEditorThemePalette(
isNightMode,
primary,
secondary,
tertiary,
- background,
+ windowBackground,
surface,
surfaceContainer,
surfaceContainerHigh,
@@ -119,43 +72,35 @@ internal static class ComponentEditorMaterialThemeAdapter
titleBarButtonHover,
outline,
divider,
- onSurface,
- onSurfaceVariant,
+ textPrimary,
+ textSecondary,
onPrimary);
}
- private static Color ResolveSecondaryColor(Color primary, MonetPalette monetPalette, bool isNightMode)
+ private static Color GetSurfaceColor(MaterialColorSnapshot snapshot, MaterialSurfaceRole role, Color fallback)
{
- if (monetPalette.Secondary != default)
- {
- return monetPalette.Secondary;
- }
-
- return ColorMath.Blend(
- primary,
- isNightMode ? Color.Parse("#FFFFFFFF") : Color.Parse("#FF1F1B24"),
- isNightMode ? 0.18 : 0.16);
+ return snapshot.Surfaces.TryGetValue(role, out var surface) && surface.BackgroundColor.A > 0
+ ? surface.BackgroundColor
+ : fallback;
}
- private static Color ResolveTertiaryColor(
- Color primary,
- Color secondary,
- MonetPalette monetPalette,
- bool isNightMode)
+ private static Color GetSurfaceBorder(MaterialColorSnapshot snapshot, MaterialSurfaceRole role)
{
- if (monetPalette.Tertiary != default)
- {
- return monetPalette.Tertiary;
- }
-
- var blendTarget = isNightMode ? Color.Parse("#FFFFFFFF") : Color.Parse("#FF2A2230");
- return ColorMath.Blend(ColorMath.Blend(primary, secondary, 0.5), blendTarget, isNightMode ? 0.12 : 0.14);
+ return snapshot.Surfaces.TryGetValue(role, out var surface)
+ ? surface.BorderColor
+ : default;
}
- private static Color? TryParseColor(string? value)
+ private static Color FirstUsable(params Color[] colors)
{
- return !string.IsNullOrWhiteSpace(value) && Color.TryParse(value, out var parsed)
- ? parsed
- : null;
+ foreach (var color in colors)
+ {
+ if (color.A > 0)
+ {
+ return color;
+ }
+ }
+
+ return default;
}
}
diff --git a/LanMountainDesktop/ViewModels/SettingsViewModels.cs b/LanMountainDesktop/ViewModels/SettingsViewModels.cs
index 7bbaa2d..302950b 100644
--- a/LanMountainDesktop/ViewModels/SettingsViewModels.cs
+++ b/LanMountainDesktop/ViewModels/SettingsViewModels.cs
@@ -630,70 +630,34 @@ public sealed partial class GeneralSettingsPageViewModel : ViewModelBase, IDispo
public sealed partial class AppearanceSettingsPageViewModel : ViewModelBase
{
- private static readonly Color DefaultSeedColor = Color.Parse("#FF3B82F6");
- private static readonly SolidColorBrush NeutralLightBrushValue = new(Color.Parse("#FFFFFFFF"));
- private static readonly SolidColorBrush NeutralDarkBrushValue = new(Color.Parse("#FF000000"));
private readonly ISettingsFacadeService _settingsFacade;
- private readonly IAppearanceThemeService _appearanceThemeService;
private readonly LocalizationService _localizationService = new();
private readonly string _languageCode;
private bool _isInitializing;
- private string? _selectedWallpaperSeed;
- public AppearanceSettingsPageViewModel(
- ISettingsFacadeService settingsFacade,
- IAppearanceThemeService appearanceThemeService)
+ public AppearanceSettingsPageViewModel(ISettingsFacadeService settingsFacade)
{
- _settingsFacade = settingsFacade;
- _appearanceThemeService = appearanceThemeService;
+ _settingsFacade = settingsFacade ?? throw new ArgumentNullException(nameof(settingsFacade));
_languageCode = _localizationService.NormalizeLanguageCode(_settingsFacade.Region.Get().LanguageCode);
RefreshLocalizedText();
- ThemeColorModes = CreateThemeColorModes();
ThemeModeOptions = CreateThemeModeOptions();
_isInitializing = true;
- Load();
- _isInitializing = false;
-
- }
-
- partial void OnSelectedThemeModeChanged(SelectionOption value)
- {
- if (_isInitializing || value is null)
+ try
{
- return;
+ Load();
}
-
- // 根据选择的主题模式更新夜间模式状态
- var newIsNightMode = value.Value switch
+ finally
{
- ThemeAppearanceValues.ThemeModeDark => true,
- ThemeAppearanceValues.ThemeModeLight => false,
- ThemeAppearanceValues.ThemeModeFollowSystem => Application.Current?.ActualThemeVariant == ThemeVariant.Dark,
- _ => IsNightMode
- };
-
- if (IsNightMode != newIsNightMode)
- {
- IsNightMode = newIsNightMode;
+ _isInitializing = false;
}
-
- PersistCurrentState(restartRequired: false);
}
public event Action? RestartRequested;
- public IReadOnlyList ThemeColorModes { get; }
-
- [ObservableProperty]
- private IReadOnlyList _systemMaterialModes = [];
-
[ObservableProperty]
private bool _isNightMode;
- [ObservableProperty]
- private string _themeColor = string.Empty;
-
[ObservableProperty]
private IReadOnlyList _themeModeOptions = [];
@@ -715,92 +679,12 @@ public sealed partial class AppearanceSettingsPageViewModel : ViewModelBase
[ObservableProperty]
private string _themeModeFollowSystemText = string.Empty;
- [ObservableProperty]
- private Color _customSeedPickerValue = DefaultSeedColor;
-
- partial void OnCustomSeedPickerValueChanged(Color value)
- {
- if (_isInitializing ||
- !string.Equals(SelectedThemeColorMode?.Value, ThemeAppearanceValues.ColorModeSeedMonet, StringComparison.OrdinalIgnoreCase))
- {
- return;
- }
-
- UpdatePreview(BuildPendingState(usePickerSeed: true));
- }
-
[ObservableProperty]
private bool _useSystemChrome;
- [ObservableProperty]
- private double _globalCornerRadiusScale = GlobalAppearanceSettings.DefaultCornerRadiusScale;
-
- [ObservableProperty]
- private SelectionOption _selectedThemeColorMode = new(ThemeAppearanceValues.ColorModeSeedMonet, "User theme color Monet");
-
- [ObservableProperty]
- private SelectionOption _selectedSystemMaterialMode = new(ThemeAppearanceValues.MaterialAuto, "Auto");
-
- [ObservableProperty]
- private bool _isThemeColorEditable;
-
- [ObservableProperty]
- private bool _isWallpaperMode;
-
- [ObservableProperty]
- private bool _showNeutralPreview;
-
- [ObservableProperty]
- private bool _showMonetPreview;
-
- [ObservableProperty]
- private bool _isWallpaperSeedSelectable;
-
- [ObservableProperty]
- private string _themeColorSourceDescription = string.Empty;
-
- [ObservableProperty]
- private string _systemMaterialDescription = string.Empty;
-
- [ObservableProperty]
- private IBrush _primarySwatchBrush = new SolidColorBrush(DefaultSeedColor);
-
- [ObservableProperty]
- private IBrush _secondarySwatchBrush = new SolidColorBrush(DefaultSeedColor);
-
- [ObservableProperty]
- private IBrush _tertiarySwatchBrush = new SolidColorBrush(DefaultSeedColor);
-
- [ObservableProperty]
- private IBrush _neutralSwatchBrush = new SolidColorBrush(Color.Parse("#FFF2F4F7"));
-
- [ObservableProperty]
- private IBrush _seedSwatchBrush = new SolidColorBrush(DefaultSeedColor);
-
- [ObservableProperty]
- private IReadOnlyList _wallpaperSeedCandidates = [];
-
- [ObservableProperty]
- private string _pageTitle = string.Empty;
-
- [ObservableProperty]
- private string _pageDescription = string.Empty;
-
- [ObservableProperty]
- private string _nightModeLabel = string.Empty;
-
[ObservableProperty]
private string _useSystemChromeLabel = string.Empty;
- [ObservableProperty]
- private string _themeColorLabel = string.Empty;
-
- [ObservableProperty]
- private string _themeColorModeLabel = string.Empty;
-
- [ObservableProperty]
- private string _systemMaterialLabel = string.Empty;
-
[ObservableProperty]
private string _cornerRadiusStyleLabel = string.Empty;
@@ -810,91 +694,9 @@ public sealed partial class AppearanceSettingsPageViewModel : ViewModelBase
[ObservableProperty]
private string _themeHeader = string.Empty;
- [ObservableProperty]
- private string _themeSourceNeutralText = string.Empty;
-
- [ObservableProperty]
- private string _themeSourceUserColorText = string.Empty;
-
- [ObservableProperty]
- private string _themeSourceWallpaperText = string.Empty;
-
- [ObservableProperty]
- private string _themeSourceDefaultDescription = string.Empty;
-
- [ObservableProperty]
- private string _themeSourceUserColorDescription = string.Empty;
-
- [ObservableProperty]
- private string _themeSourceWallpaperDescription = string.Empty;
-
- [ObservableProperty]
- private string _themeSourceWallpaperAppDescription = string.Empty;
-
- [ObservableProperty]
- private string _themeSourceWallpaperSystemDescription = string.Empty;
-
- [ObservableProperty]
- private string _themeSourceWallpaperFallbackDescription = string.Empty;
-
- [ObservableProperty]
- private string _systemMaterialNoneText = string.Empty;
-
- [ObservableProperty]
- private string _systemMaterialAutoText = string.Empty;
-
- [ObservableProperty]
- private string _systemMaterialMicaText = string.Empty;
-
- [ObservableProperty]
- private string _systemMaterialAcrylicText = string.Empty;
-
- [ObservableProperty]
- private string _systemMaterialSwitchableDescription = string.Empty;
-
- [ObservableProperty]
- private string _systemMaterialFixedDescription = string.Empty;
-
- [ObservableProperty]
- private string _systemMaterialAutoDescription = string.Empty;
-
[ObservableProperty]
private string _appearanceRestartMessage = string.Empty;
- [ObservableProperty]
- private string _previewPrimaryLabel = string.Empty;
-
- [ObservableProperty]
- private string _previewSecondaryLabel = string.Empty;
-
- [ObservableProperty]
- private string _previewTertiaryLabel = string.Empty;
-
- [ObservableProperty]
- private string _previewNeutralLabel = string.Empty;
-
- [ObservableProperty]
- private string _previewSeedLabel = string.Empty;
-
- [ObservableProperty]
- private string _previewNeutralLightLabel = string.Empty;
-
- [ObservableProperty]
- private string _previewNeutralDarkLabel = string.Empty;
-
- [ObservableProperty]
- private string _seedApplyButtonText = string.Empty;
-
- [ObservableProperty]
- private string _wallpaperSeedFlyoutTitle = string.Empty;
-
- [ObservableProperty]
- private string _wallpaperSeedCurrentText = string.Empty;
-
- public IBrush NeutralLightPreviewBrush => NeutralLightBrushValue;
-
- public IBrush NeutralDarkPreviewBrush => NeutralDarkBrushValue;
-
[ObservableProperty]
private string _cornerRadiusStyle = GlobalAppearanceSettings.DefaultCornerRadiusStyle;
@@ -907,8 +709,6 @@ public sealed partial class AppearanceSettingsPageViewModel : ViewModelBase
public void Load()
{
var theme = _settingsFacade.Theme.Get();
- var liveSnapshot = _appearanceThemeService.GetCurrent();
- RefreshMaterialModeOptions(liveSnapshot);
_isInitializing = true;
try
@@ -919,8 +719,29 @@ public sealed partial class AppearanceSettingsPageViewModel : ViewModelBase
{
_isInitializing = false;
}
+ }
- UpdatePreview(theme);
+ partial void OnSelectedThemeModeChanged(SelectionOption value)
+ {
+ if (_isInitializing || value is null)
+ {
+ return;
+ }
+
+ var newIsNightMode = value.Value switch
+ {
+ ThemeAppearanceValues.ThemeModeDark => true,
+ ThemeAppearanceValues.ThemeModeLight => false,
+ ThemeAppearanceValues.ThemeModeFollowSystem => Application.Current?.ActualThemeVariant == ThemeVariant.Dark,
+ _ => IsNightMode
+ };
+
+ if (IsNightMode != newIsNightMode)
+ {
+ IsNightMode = newIsNightMode;
+ }
+
+ PersistCurrentState(restartRequired: false);
}
partial void OnUseSystemChromeChanged(bool value)
@@ -944,64 +765,8 @@ public sealed partial class AppearanceSettingsPageViewModel : ViewModelBase
PersistCurrentState(restartRequired: false);
}
- partial void OnSelectedThemeColorModeChanged(SelectionOption value)
- {
- if (_isInitializing || value is null)
- {
- return;
- }
-
- PersistCurrentState(restartRequired: true);
- }
-
- partial void OnSelectedSystemMaterialModeChanged(SelectionOption value)
- {
- if (_isInitializing || value is null)
- {
- return;
- }
-
- PersistCurrentState(restartRequired: true);
- }
-
- [RelayCommand]
- private void ApplyCustomSeed()
- {
- if (!IsThemeColorEditable)
- {
- return;
- }
-
- ThemeColor = CustomSeedPickerValue.ToString();
- PersistCurrentState(restartRequired: false);
- }
-
- public void CancelCustomSeedPreview()
- {
- if (_isInitializing)
- {
- return;
- }
-
- SyncCustomSeedPickerWithSavedThemeColor();
- UpdatePreview(BuildPendingState(usePickerSeed: false));
- }
-
- public void SelectWallpaperSeed(string value)
- {
- if (!IsWallpaperMode || string.IsNullOrWhiteSpace(value))
- {
- return;
- }
-
- _selectedWallpaperSeed = value;
- PersistCurrentState(restartRequired: true);
- }
-
private void RefreshLocalizedText()
{
- PageTitle = L("settings.appearance.title", "Appearance");
- PageDescription = L("settings.appearance.description", "Adjust theme source, material background, and window chrome.");
ThemeHeader = L("settings.appearance.theme_header", "Theme");
ThemeModeLabel = L("settings.appearance.theme_mode_label", "Theme mode");
ThemeModeDescription = L("settings.appearance.theme_mode_desc", "Choose light, dark, or follow system preference.");
@@ -1009,78 +774,26 @@ public sealed partial class AppearanceSettingsPageViewModel : ViewModelBase
ThemeModeDarkText = L("settings.appearance.theme_mode.dark", "Dark");
ThemeModeFollowSystemText = L("settings.appearance.theme_mode.follow_system", "Follow system");
UseSystemChromeLabel = L("settings.color.use_system_chrome_toggle", "Use system window chrome");
- ThemeColorLabel = L("settings.color.theme_color_label", "Theme Accent Color");
- ThemeColorModeLabel = L("settings.appearance.theme_color_mode_label", "Theme color source");
- SystemMaterialLabel = L("settings.appearance.system_material_label", "System material");
CornerRadiusStyleLabel = L("settings.appearance.corner_radius.label", "Global corner radius style");
CornerRadiusStyleDescription = L("settings.appearance.corner_radius.description", "Select a fixed corner radius style inspired by Xiaomi HyperOS.");
-
+ AppearanceRestartMessage = L(
+ "settings.appearance.restart_message",
+ "Window chrome changes require restarting the app.");
+
CornerRadiusStyleOptions = GlobalAppearanceSettings.AllCornerRadiusStyles
.Select(style => new SelectionOption(style, L($"settings.appearance.corner_radius.style_{style.ToLower()}", style)))
.ToList();
- ThemeSourceNeutralText = L("settings.appearance.theme_color_mode.neutral", "Default neutral");
- ThemeSourceUserColorText = L("settings.appearance.theme_color_mode.user", "User theme color Monet");
- ThemeSourceWallpaperText = L("settings.appearance.theme_color_mode.wallpaper", "Wallpaper Monet");
- ThemeSourceDefaultDescription = L("settings.appearance.theme_color_mode_desc.neutral", "Use the standard light and dark neutral surfaces.");
- ThemeSourceUserColorDescription = L("settings.appearance.theme_color_mode_desc.user", "Use the selected theme color as the Monet seed.");
- ThemeSourceWallpaperDescription = L("settings.appearance.theme_color_mode_desc.wallpaper", "Use the current wallpaper palette. App wallpaper is preferred, then system wallpaper.");
- ThemeSourceWallpaperAppDescription = L("settings.appearance.theme_color_preview.app", "Currently previewing colors extracted from the app wallpaper.");
- ThemeSourceWallpaperSystemDescription = L("settings.appearance.theme_color_preview.system", "Currently previewing colors extracted from the system wallpaper.");
- ThemeSourceWallpaperFallbackDescription = L("settings.appearance.theme_color_preview.fallback", "No usable wallpaper was found. The app is using a fallback accent.");
- SystemMaterialNoneText = L("settings.appearance.system_material.none", "None");
- SystemMaterialAutoText = L("settings.appearance.system_material.auto", "Auto (recommended)");
- SystemMaterialMicaText = L("settings.appearance.system_material.mica", "Mica");
- SystemMaterialAcrylicText = L("settings.appearance.system_material.acrylic", "Acrylic");
- SystemMaterialSwitchableDescription = L("settings.appearance.system_material_desc.switchable", "Apply the selected material to windows, Dock, status bar, and component hosts.");
- SystemMaterialFixedDescription = L("settings.appearance.system_material_desc.fixed", "Your current system only exposes the available material modes listed here.");
- SystemMaterialAutoDescription = L("settings.appearance.system_material_desc.auto", "Auto prefers Mica on Windows 11, Acrylic on Windows 10, and falls back to no material when unavailable.");
- AppearanceRestartMessage = L(
- "settings.appearance.restart_message",
- "Theme source and system material changes require restarting the app.");
- PreviewPrimaryLabel = L("settings.appearance.preview.primary", "Primary");
- PreviewSecondaryLabel = L("settings.appearance.preview.secondary", "Secondary");
- PreviewTertiaryLabel = L("settings.appearance.preview.tertiary", "Tertiary");
- PreviewNeutralLabel = L("settings.appearance.preview.neutral", "Neutral");
- PreviewSeedLabel = L("settings.appearance.preview.seed", "Seed");
- PreviewNeutralLightLabel = L("settings.appearance.preview.neutral_light", "White");
- PreviewNeutralDarkLabel = L("settings.appearance.preview.neutral_dark", "Black");
- SeedApplyButtonText = L("settings.appearance.preview.apply_seed", "Apply");
- WallpaperSeedFlyoutTitle = L("settings.appearance.preview.wallpaper_candidates", "Wallpaper seed candidates");
- WallpaperSeedCurrentText = L("settings.appearance.preview.wallpaper_current", "Current");
- }
-
- private void RefreshMaterialModeOptions(AppearanceThemeSnapshot snapshot)
- {
- SystemMaterialModes = snapshot.AvailableSystemMaterialModes
- .Select(value => new SelectionOption(value, ResolveMaterialModeLabel(value)))
- .ToList();
- SystemMaterialDescription = snapshot.CanChangeSystemMaterial
- ? SystemMaterialAutoDescription
- : SystemMaterialFixedDescription;
}
private void ApplySavedState(ThemeAppearanceSettingsState theme)
{
IsNightMode = theme.IsNightMode;
- ThemeColor = theme.ThemeColor ?? string.Empty;
UseSystemChrome = theme.UseSystemChrome;
CornerRadiusStyle = GlobalAppearanceSettings.NormalizeCornerRadiusStyle(theme.CornerRadiusStyle);
SelectedCornerRadiusStyle = CornerRadiusStyleOptions.FirstOrDefault(option =>
string.Equals(option.Value, CornerRadiusStyle, StringComparison.OrdinalIgnoreCase))
?? CornerRadiusStyleOptions.FirstOrDefault(o => o.Value == GlobalAppearanceSettings.DefaultCornerRadiusStyle);
- _selectedWallpaperSeed = theme.SelectedWallpaperSeed;
- SyncCustomSeedPickerWithSavedThemeColor();
- var savedThemeColorMode = ThemeAppearanceValues.NormalizeThemeColorMode(theme.ThemeColorMode, theme.ThemeColor);
- var savedSystemMaterialMode = ThemeAppearanceValues.NormalizeSystemMaterialMode(theme.SystemMaterialMode);
- SelectedThemeColorMode = ThemeColorModes.FirstOrDefault(option =>
- string.Equals(option.Value, savedThemeColorMode, StringComparison.OrdinalIgnoreCase))
- ?? ThemeColorModes[0];
- SelectedSystemMaterialMode = SystemMaterialModes.FirstOrDefault(option =>
- string.Equals(option.Value, savedSystemMaterialMode, StringComparison.OrdinalIgnoreCase))
- ?? SystemMaterialModes[0];
-
- // 应用主题模式设置
var savedThemeMode = NormalizeThemeMode(theme.ThemeMode);
SelectedThemeMode = ThemeModeOptions.FirstOrDefault(option =>
string.Equals(option.Value, savedThemeMode, StringComparison.OrdinalIgnoreCase))
@@ -1094,16 +807,18 @@ public sealed partial class AppearanceSettingsPageViewModel : ViewModelBase
{
return ThemeAppearanceValues.ThemeModeDark;
}
+
if (string.Equals(value, ThemeAppearanceValues.ThemeModeFollowSystem, StringComparison.OrdinalIgnoreCase))
{
return ThemeAppearanceValues.ThemeModeFollowSystem;
}
+
return ThemeAppearanceValues.ThemeModeLight;
}
private void PersistCurrentState(bool restartRequired)
{
- var pendingState = BuildPendingState(usePickerSeed: false);
+ var pendingState = BuildPendingState();
_settingsFacade.Theme.Save(pendingState);
var savedState = _settingsFacade.Theme.Get();
@@ -1117,9 +832,6 @@ public sealed partial class AppearanceSettingsPageViewModel : ViewModelBase
_isInitializing = false;
}
- RefreshMaterialModeOptions(_appearanceThemeService.GetCurrent());
- UpdatePreview(savedState);
-
if (restartRequired)
{
RestartRequested?.Invoke(AppearanceRestartMessage);
@@ -1136,110 +848,17 @@ public sealed partial class AppearanceSettingsPageViewModel : ViewModelBase
];
}
- private ThemeAppearanceSettingsState BuildPendingState(bool usePickerSeed)
+ private ThemeAppearanceSettingsState BuildPendingState()
{
- var themeColorMode = ThemeAppearanceValues.NormalizeThemeColorMode(SelectedThemeColorMode?.Value, ThemeColor);
- var themeColor = themeColorMode == ThemeAppearanceValues.ColorModeSeedMonet
- ? (usePickerSeed ? CustomSeedPickerValue.ToString() : string.IsNullOrWhiteSpace(ThemeColor) ? null : ThemeColor)
- : string.IsNullOrWhiteSpace(ThemeColor) ? null : ThemeColor;
-
- return new ThemeAppearanceSettingsState(
- IsNightMode,
- themeColor,
- UseSystemChrome,
- GlobalAppearanceSettings.NormalizeCornerRadiusStyle(CornerRadiusStyle),
- themeColorMode,
- ThemeAppearanceValues.NormalizeSystemMaterialMode(SelectedSystemMaterialMode?.Value),
- _selectedWallpaperSeed,
- SelectedThemeMode?.Value ?? ThemeAppearanceValues.ThemeModeLight);
- }
-
- private void UpdatePreview(ThemeAppearanceSettingsState pendingState)
- {
- var preview = _appearanceThemeService.BuildPreview(pendingState);
- var normalizedMode = preview.ThemeColorMode;
-
- ShowNeutralPreview = normalizedMode == ThemeAppearanceValues.ColorModeDefaultNeutral;
- ShowMonetPreview = !ShowNeutralPreview;
- IsThemeColorEditable = normalizedMode == ThemeAppearanceValues.ColorModeSeedMonet;
- IsWallpaperMode = normalizedMode == ThemeAppearanceValues.ColorModeWallpaperMonet;
-
- PrimarySwatchBrush = new SolidColorBrush(preview.MonetPalette.Primary);
- SecondarySwatchBrush = new SolidColorBrush(preview.MonetPalette.Secondary);
- TertiarySwatchBrush = new SolidColorBrush(preview.MonetPalette.Tertiary);
- NeutralSwatchBrush = new SolidColorBrush(preview.MonetPalette.Neutral);
- SeedSwatchBrush = new SolidColorBrush(preview.EffectiveSeedColor);
-
- if (IsWallpaperMode)
+ return _settingsFacade.Theme.Get() with
{
- WallpaperSeedCandidates = preview.WallpaperSeedCandidates
- .Select((color, index) => new ThemeSeedCandidateOption(
- color.ToString(),
- $"{PreviewSeedLabel} {index + 1}",
- color,
- string.Equals(color.ToString(), _selectedWallpaperSeed, StringComparison.OrdinalIgnoreCase)))
- .ToArray();
- if (WallpaperSeedCandidates.Count > 0 &&
- !string.Equals(_selectedWallpaperSeed, preview.EffectiveSeedColor.ToString(), StringComparison.OrdinalIgnoreCase))
- {
- _selectedWallpaperSeed = preview.EffectiveSeedColor.ToString();
- WallpaperSeedCandidates = preview.WallpaperSeedCandidates
- .Select((color, index) => new ThemeSeedCandidateOption(
- color.ToString(),
- $"{PreviewSeedLabel} {index + 1}",
- color,
- string.Equals(color.ToString(), _selectedWallpaperSeed, StringComparison.OrdinalIgnoreCase)))
- .ToArray();
- }
-
- IsWallpaperSeedSelectable = WallpaperSeedCandidates.Count > 1;
- ThemeColorSourceDescription = preview.ResolvedSeedSource switch
- {
- "app_wallpaper" or "app_video" or "app_solid" => ThemeSourceWallpaperAppDescription,
- "system_wallpaper" => ThemeSourceWallpaperSystemDescription,
- _ => ThemeSourceWallpaperFallbackDescription
- };
- }
- else
- {
- WallpaperSeedCandidates = [];
- IsWallpaperSeedSelectable = false;
- ThemeColorSourceDescription = normalizedMode switch
- {
- ThemeAppearanceValues.ColorModeDefaultNeutral => ThemeSourceDefaultDescription,
- _ => ThemeSourceUserColorDescription
- };
- }
- }
-
- private string ResolveMaterialModeLabel(string value)
- {
- return ThemeAppearanceValues.NormalizeSystemMaterialMode(value) switch
- {
- ThemeAppearanceValues.MaterialAuto => SystemMaterialAutoText,
- ThemeAppearanceValues.MaterialMica => SystemMaterialMicaText,
- ThemeAppearanceValues.MaterialAcrylic => SystemMaterialAcrylicText,
- _ => SystemMaterialNoneText
+ IsNightMode = IsNightMode,
+ UseSystemChrome = UseSystemChrome,
+ CornerRadiusStyle = GlobalAppearanceSettings.NormalizeCornerRadiusStyle(CornerRadiusStyle),
+ ThemeMode = SelectedThemeMode?.Value ?? ThemeAppearanceValues.ThemeModeLight
};
}
- private void SyncCustomSeedPickerWithSavedThemeColor()
- {
- CustomSeedPickerValue = !string.IsNullOrWhiteSpace(ThemeColor) && Color.TryParse(ThemeColor, out var parsedColor)
- ? parsedColor
- : DefaultSeedColor;
- }
-
- private IReadOnlyList CreateThemeColorModes()
- {
- return
- [
- new SelectionOption(ThemeAppearanceValues.ColorModeDefaultNeutral, ThemeSourceNeutralText),
- new SelectionOption(ThemeAppearanceValues.ColorModeSeedMonet, ThemeSourceUserColorText),
- new SelectionOption(ThemeAppearanceValues.ColorModeWallpaperMonet, ThemeSourceWallpaperText)
- ];
- }
-
private string L(string key, string fallback)
=> _localizationService.GetString(_languageCode, key, fallback);
}
@@ -1379,14 +998,10 @@ public sealed partial class ComponentsSettingsPageViewModel : ViewModelBase
private void SaveComponentCornerRadius()
{
var theme = _settingsFacade.Theme.Get();
- _settingsFacade.Theme.Save(new ThemeAppearanceSettingsState(
- theme.IsNightMode,
- theme.ThemeColor,
- theme.UseSystemChrome,
- GlobalAppearanceSettings.NormalizeCornerRadiusStyle(CornerRadiusStyle),
- theme.ThemeColorMode,
- theme.SystemMaterialMode,
- theme.SelectedWallpaperSeed));
+ _settingsFacade.Theme.Save(theme with
+ {
+ CornerRadiusStyle = GlobalAppearanceSettings.NormalizeCornerRadiusStyle(CornerRadiusStyle)
+ });
}
private IReadOnlyList CreateSpacingPresets()
diff --git a/LanMountainDesktop/Views/ComponentEditorWindow.axaml.cs b/LanMountainDesktop/Views/ComponentEditorWindow.axaml.cs
index 823fe3a..5e8d6f1 100644
--- a/LanMountainDesktop/Views/ComponentEditorWindow.axaml.cs
+++ b/LanMountainDesktop/Views/ComponentEditorWindow.axaml.cs
@@ -80,7 +80,7 @@ public partial class ComponentEditorWindow : Window
_materialTheme.SecondaryColor = palette.SecondaryColor;
SetBrushResource("EditorPrimaryBrush", palette.PrimaryColor);
- SetBrushResource("EditorOnPrimaryBrush", palette.IsNightMode ? Colors.Black : Colors.White);
+ SetBrushResource("EditorOnPrimaryBrush", palette.OnPrimaryColor);
SetBrushResource("EditorSecondaryBrush", palette.SecondaryColor);
SetBrushResource("EditorTertiaryBrush", palette.TertiaryColor);
SetBrushResource("EditorWindowBackgroundBrush", palette.WindowBackgroundColor);
diff --git a/LanMountainDesktop/Views/SettingsPages/AppearanceSettingsPage.axaml.cs b/LanMountainDesktop/Views/SettingsPages/AppearanceSettingsPage.axaml.cs
index 7125102..5b9d4b5 100644
--- a/LanMountainDesktop/Views/SettingsPages/AppearanceSettingsPage.axaml.cs
+++ b/LanMountainDesktop/Views/SettingsPages/AppearanceSettingsPage.axaml.cs
@@ -1,10 +1,7 @@
-using System;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services;
using LanMountainDesktop.Services.Settings;
using LanMountainDesktop.ViewModels;
-using Avalonia.Controls;
-using Avalonia.Interactivity;
namespace LanMountainDesktop.Views.SettingsPages;
@@ -20,8 +17,7 @@ public partial class AppearanceSettingsPage : SettingsPageBase
{
public AppearanceSettingsPage()
: this(new AppearanceSettingsPageViewModel(
- HostSettingsFacadeProvider.GetOrCreate(),
- HostAppearanceThemeProvider.GetOrCreate()))
+ HostSettingsFacadeProvider.GetOrCreate()))
{
}
@@ -39,31 +35,4 @@ public partial class AppearanceSettingsPage : SettingsPageBase
{
RequestRestart(reason);
}
-
- private void OnApplyCustomSeedClick(object? sender, RoutedEventArgs e)
- {
- _ = sender;
- _ = e;
- ViewModel.ApplyCustomSeedCommand.Execute(null);
- CustomSeedButton?.Flyout?.Hide();
- }
-
- private void OnCustomSeedFlyoutClosed(object? sender, EventArgs e)
- {
- _ = sender;
- _ = e;
- ViewModel.CancelCustomSeedPreview();
- }
-
- private void OnWallpaperSeedCandidateClick(object? sender, RoutedEventArgs e)
- {
- _ = e;
-
- if (sender is Button { DataContext: ThemeSeedCandidateOption option })
- {
- ViewModel.SelectWallpaperSeed(option.Value);
- }
-
- WallpaperSeedButton?.Flyout?.Hide();
- }
}
diff --git a/docs/PLUGIN_SDK_V5_MIGRATION.md b/docs/PLUGIN_SDK_V5_MIGRATION.md
index bd4cc79..1c7498c 100644
--- a/docs/PLUGIN_SDK_V5_MIGRATION.md
+++ b/docs/PLUGIN_SDK_V5_MIGRATION.md
@@ -15,6 +15,20 @@ SDK v5 is a binary breaking change because the SDK exposes Avalonia UI types suc
The host does not provide an Avalonia 11 / Avalonia 12 dual UI stack. The public extension entry points remain the same: custom settings pages still derive from `SettingsPageBase`, and desktop components still provide Avalonia controls through the existing registration APIs.
+## Appearance Snapshot
+
+`IPluginAppearanceContext.Snapshot` remains read-only. In addition to theme variant and corner radius tokens, the snapshot can now include host material/color data:
+
+- `AccentColor`
+- `SeedColor`
+- `ColorSource`
+- `SystemMaterialMode`
+- `ColorRoles`
+- `MaterialSurfaces`
+- `WallpaperSeedCandidates`
+
+Existing plugins that only read `CornerRadiusTokens` and `ThemeVariant` continue to work. New plugins should treat the added properties as optional and prefer `ColorRoles`/`MaterialSurfaces` over hard-coded colors.
+
## Minimal Package Update
```xml
diff --git a/docs/VISUAL_SPEC.md b/docs/VISUAL_SPEC.md
index 55e2485..d61e352 100644
--- a/docs/VISUAL_SPEC.md
+++ b/docs/VISUAL_SPEC.md
@@ -46,3 +46,11 @@ This specification defines the visual language of LanMountainDesktop, including
- use semantic resource keys instead of hard-coded colors
- keep glass layers visually distinct
- maintain contrast targets for readability
+
+### Material And Color Source
+
+`IMaterialColorService` is the host source of truth for Monet seeds, wallpaper-derived colors, semantic color roles, material surfaces, and plugin appearance snapshots.
+
+New UI, component, window, and plugin appearance consumers should use `MaterialColorSnapshot` or resources produced from it. Do not recalculate application colors from raw wallpaper settings, theme settings, or `MonetPalette` in parallel.
+
+The Wallpaper settings page owns wallpaper asset/source selection. The Material & Color settings page owns color-source selection, wallpaper color-source selection, system material mode, wallpaper color refresh behavior, and color/material previews.