mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-20 23:54:26 +08:00
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:
12
.trae/specs/material-color-service/checklist.md
Normal file
12
.trae/specs/material-color-service/checklist.md
Normal file
@@ -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.
|
||||
62
.trae/specs/material-color-service/spec.md
Normal file
62
.trae/specs/material-color-service/spec.md
Normal file
@@ -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.
|
||||
13
.trae/specs/material-color-service/tasks.md
Normal file
13
.trae/specs/material-color-service/tasks.md
Normal file
@@ -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.
|
||||
@@ -1,15 +1,10 @@
|
||||
namespace LanMountainDesktop.PluginSdk;
|
||||
|
||||
/// <summary>
|
||||
/// 外观变更事件参数,当主题、圆角或其他外观属性变化时触发。
|
||||
/// Provides the latest read-only appearance snapshot when host appearance values change.
|
||||
/// </summary>
|
||||
public sealed class AppearanceChangedEvent : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建外观变更事件实例。
|
||||
/// </summary>
|
||||
/// <param name="snapshot">当前外观快照</param>
|
||||
/// <param name="changedProperties">变更的属性集合</param>
|
||||
public AppearanceChangedEvent(
|
||||
PluginAppearanceSnapshot snapshot,
|
||||
IReadOnlyCollection<AppearanceProperty> changedProperties)
|
||||
@@ -21,89 +16,50 @@ public sealed class AppearanceChangedEvent : EventArgs
|
||||
ChangedProperties = changedProperties;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当前外观快照。
|
||||
/// </summary>
|
||||
public PluginAppearanceSnapshot Snapshot { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 变更的属性集合。
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<AppearanceProperty> ChangedProperties { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 圆角是否发生变化。
|
||||
/// </summary>
|
||||
public bool CornerRadiusChanged => ChangedProperties.Contains(AppearanceProperty.CornerRadius);
|
||||
public bool CornerRadiusChanged => HasChanged(AppearanceProperty.CornerRadius);
|
||||
|
||||
/// <summary>
|
||||
/// 主题变体(亮色/暗色)是否发生变化。
|
||||
/// </summary>
|
||||
public bool ThemeVariantChanged => ChangedProperties.Contains(AppearanceProperty.ThemeVariant);
|
||||
public bool ThemeVariantChanged => HasChanged(AppearanceProperty.ThemeVariant);
|
||||
|
||||
/// <summary>
|
||||
/// 强调色是否发生变化。
|
||||
/// </summary>
|
||||
public bool AccentColorChanged => ChangedProperties.Contains(AppearanceProperty.AccentColor);
|
||||
public bool AccentColorChanged => HasChanged(AppearanceProperty.AccentColor);
|
||||
|
||||
/// <summary>
|
||||
/// 圆角风格是否发生变化。
|
||||
/// </summary>
|
||||
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);
|
||||
|
||||
/// <summary>
|
||||
/// 检查指定属性是否发生变化。
|
||||
/// </summary>
|
||||
/// <param name="property">要检查的属性</param>
|
||||
/// <returns>如果属性发生变化则返回 true</returns>
|
||||
public bool HasChanged(AppearanceProperty property)
|
||||
{
|
||||
return ChangedProperties.Contains(property);
|
||||
return ChangedProperties.Contains(AppearanceProperty.All) ||
|
||||
ChangedProperties.Contains(property);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查是否有任何外观属性发生变化。
|
||||
/// </summary>
|
||||
public bool HasAnyChanges => ChangedProperties.Count > 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 可变更的外观属性枚举。
|
||||
/// </summary>
|
||||
public enum AppearanceProperty
|
||||
{
|
||||
/// <summary>
|
||||
/// 圆角Token值发生变化。
|
||||
/// </summary>
|
||||
CornerRadius,
|
||||
|
||||
/// <summary>
|
||||
/// 主题变体(亮色/暗色)发生变化。
|
||||
/// </summary>
|
||||
ThemeVariant,
|
||||
|
||||
/// <summary>
|
||||
/// 强调色发生变化。
|
||||
/// </summary>
|
||||
AccentColor,
|
||||
|
||||
/// <summary>
|
||||
/// 圆角风格(Sharp/Balanced/Rounded/Open)发生变化。
|
||||
/// </summary>
|
||||
CornerRadiusStyle,
|
||||
|
||||
/// <summary>
|
||||
/// 壁纸发生变化。
|
||||
/// </summary>
|
||||
Wallpaper,
|
||||
|
||||
/// <summary>
|
||||
/// 系统材质模式发生变化。
|
||||
/// </summary>
|
||||
SystemMaterialMode,
|
||||
|
||||
/// <summary>
|
||||
/// 所有外观属性(用于批量更新)。
|
||||
/// </summary>
|
||||
ColorSource,
|
||||
ColorRoles,
|
||||
MaterialSurfaces,
|
||||
WallpaperSeedCandidates,
|
||||
All
|
||||
}
|
||||
|
||||
177
LanMountainDesktop.Tests/AppearanceSettingsPageViewModelTests.cs
Normal file
177
LanMountainDesktop.Tests/AppearanceSettingsPageViewModelTests.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
136
LanMountainDesktop.Tests/MaterialColorIntegrationTests.cs
Normal file
136
LanMountainDesktop.Tests/MaterialColorIntegrationTests.cs
Normal file
@@ -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, MaterialSurfaceSnapshot>
|
||||
{
|
||||
[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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<string>? RestartRequested;
|
||||
|
||||
public IReadOnlyList<SelectionOption> ThemeColorModes { get; }
|
||||
|
||||
[ObservableProperty]
|
||||
private IReadOnlyList<SelectionOption> _systemMaterialModes = [];
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isNightMode;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _themeColor = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private IReadOnlyList<SelectionOption> _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<ThemeSeedCandidateOption> _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<SelectionOption> 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<SelectionOption> CreateSpacingPresets()
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user