Use MaterialColorSnapshot in appearance flow

Introduce unified material/color spec and tests, and refactor appearance plumbing to use MaterialColorSnapshot as the single source of truth. Add .trae material-color-service spec/checklist/tasks and integration/unit tests for plugin mapping and appearance VM behavior. AppearanceChangedEvent extended with new appearance change flags and HasChanged logic. ComponentEditorMaterialThemeAdapter rewritten to accept MaterialColorSnapshot and derive palette from snapshot data. Simplify AppearanceSettingsPageViewModel and related view code: remove legacy preview/custom-seed UI logic, preserve material/color fields when updating theme or corner radius, and update save calls to use with-expressions. Update ComponentEditorWindow to use adapter-provided OnPrimary brush and minor docs updates.
This commit is contained in:
lincube
2026-05-06 16:02:55 +08:00
parent b71687cecd
commit f8a4bb888c
13 changed files with 551 additions and 633 deletions

View File

@@ -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();
}
}
}