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.