mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-22 00: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:
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user