mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-30 06:54:25 +08:00
settings_re11
This commit is contained in:
1069
LanMountainDesktop/Services/AppearanceThemeService.cs
Normal file
1069
LanMountainDesktop/Services/AppearanceThemeService.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -49,15 +49,19 @@ internal static class ComponentEditorMaterialThemeAdapter
|
||||
ArgumentNullException.ThrowIfNull(monetPalette);
|
||||
|
||||
var isNightMode = themeState.IsNightMode;
|
||||
var monetColors = monetPalette.MonetColors?.Where(color => color.A > 0).ToArray() ?? [];
|
||||
var fallbackThemeColor = TryParseColor(themeState.ThemeColor);
|
||||
var useWallpaperPalette = wallpaperMediaType == WallpaperMediaType.Image && monetColors.Length > 0;
|
||||
var useWallpaperPalette = wallpaperMediaType == WallpaperMediaType.Image && monetPalette.Primary.A > 0;
|
||||
|
||||
var primary = useWallpaperPalette
|
||||
? monetColors[0]
|
||||
: fallbackThemeColor ?? monetColors.FirstOrDefault(DefaultPrimary);
|
||||
var secondary = ResolveSecondaryColor(primary, monetColors, isNightMode);
|
||||
var tertiary = ResolveTertiaryColor(primary, secondary, monetColors, isNightMode);
|
||||
? 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;
|
||||
@@ -120,11 +124,11 @@ internal static class ComponentEditorMaterialThemeAdapter
|
||||
onPrimary);
|
||||
}
|
||||
|
||||
private static Color ResolveSecondaryColor(Color primary, IReadOnlyList<Color> monetColors, bool isNightMode)
|
||||
private static Color ResolveSecondaryColor(Color primary, MonetPalette monetPalette, bool isNightMode)
|
||||
{
|
||||
if (monetColors.Count > 1)
|
||||
if (monetPalette.Secondary != default)
|
||||
{
|
||||
return monetColors[1];
|
||||
return monetPalette.Secondary;
|
||||
}
|
||||
|
||||
return ColorMath.Blend(
|
||||
@@ -136,12 +140,12 @@ internal static class ComponentEditorMaterialThemeAdapter
|
||||
private static Color ResolveTertiaryColor(
|
||||
Color primary,
|
||||
Color secondary,
|
||||
IReadOnlyList<Color> monetColors,
|
||||
MonetPalette monetPalette,
|
||||
bool isNightMode)
|
||||
{
|
||||
if (monetColors.Count > 2)
|
||||
if (monetPalette.Tertiary != default)
|
||||
{
|
||||
return monetColors[2];
|
||||
return monetPalette.Tertiary;
|
||||
}
|
||||
|
||||
var blendTarget = isNightMode ? Color.Parse("#FFFFFFFF") : Color.Parse("#FF2A2230");
|
||||
|
||||
@@ -31,13 +31,16 @@ public interface IComponentEditorWindowService
|
||||
internal sealed class ComponentEditorWindowService : IComponentEditorWindowService
|
||||
{
|
||||
private readonly ISettingsFacadeService _settingsFacade;
|
||||
private readonly IAppearanceThemeService _appearanceThemeService;
|
||||
private ComponentEditorWindow? _window;
|
||||
private string? _currentPlacementId;
|
||||
|
||||
public ComponentEditorWindowService(ISettingsFacadeService settingsFacade)
|
||||
{
|
||||
_settingsFacade = settingsFacade ?? throw new ArgumentNullException(nameof(settingsFacade));
|
||||
_appearanceThemeService = HostAppearanceThemeProvider.GetOrCreate();
|
||||
_settingsFacade.Settings.Changed += OnSettingsChanged;
|
||||
_appearanceThemeService.Changed += OnAppearanceThemeChanged;
|
||||
}
|
||||
|
||||
public bool IsOpen => _window is { IsVisible: true };
|
||||
@@ -105,12 +108,15 @@ internal sealed class ComponentEditorWindowService : IComponentEditorWindowServi
|
||||
}
|
||||
|
||||
var changedKeys = e.ChangedKeys?.ToArray() ?? [];
|
||||
var liveAppearance = _appearanceThemeService.GetCurrent();
|
||||
if (changedKeys.Length > 0 &&
|
||||
!changedKeys.Contains(nameof(AppSettingsSnapshot.IsNightMode), StringComparer.OrdinalIgnoreCase) &&
|
||||
!changedKeys.Contains(nameof(AppSettingsSnapshot.ThemeColor), StringComparer.OrdinalIgnoreCase) &&
|
||||
!changedKeys.Contains(nameof(AppSettingsSnapshot.WallpaperPath), StringComparer.OrdinalIgnoreCase) &&
|
||||
!changedKeys.Contains(nameof(AppSettingsSnapshot.WallpaperType), StringComparer.OrdinalIgnoreCase) &&
|
||||
!changedKeys.Contains(nameof(AppSettingsSnapshot.WallpaperColor), StringComparer.OrdinalIgnoreCase) &&
|
||||
!(string.Equals(liveAppearance.ThemeColorMode, ThemeAppearanceValues.ColorModeSeedMonet, StringComparison.OrdinalIgnoreCase) &&
|
||||
changedKeys.Contains(nameof(AppSettingsSnapshot.ThemeColor), StringComparer.OrdinalIgnoreCase)) &&
|
||||
!(string.Equals(liveAppearance.ThemeColorMode, ThemeAppearanceValues.ColorModeWallpaperMonet, StringComparison.OrdinalIgnoreCase) &&
|
||||
(changedKeys.Contains(nameof(AppSettingsSnapshot.WallpaperPath), StringComparer.OrdinalIgnoreCase) ||
|
||||
changedKeys.Contains(nameof(AppSettingsSnapshot.WallpaperType), StringComparer.OrdinalIgnoreCase) ||
|
||||
changedKeys.Contains(nameof(AppSettingsSnapshot.WallpaperColor), StringComparer.OrdinalIgnoreCase))) &&
|
||||
!changedKeys.Contains(nameof(AppSettingsSnapshot.UseSystemChrome), StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
return;
|
||||
@@ -121,21 +127,33 @@ internal sealed class ComponentEditorWindowService : IComponentEditorWindowServi
|
||||
|
||||
private void ApplyTheme(ComponentEditorWindow window)
|
||||
{
|
||||
var appearanceSnapshot = _appearanceThemeService.GetCurrent();
|
||||
var themeState = _settingsFacade.Theme.Get();
|
||||
var wallpaperState = _settingsFacade.Wallpaper.Get();
|
||||
var wallpaperMediaType = _settingsFacade.WallpaperMedia.DetectMediaType(wallpaperState.WallpaperPath);
|
||||
var monetPalette = _settingsFacade.Theme.BuildPalette(
|
||||
themeState.IsNightMode,
|
||||
wallpaperState.WallpaperPath,
|
||||
themeState.ThemeColor);
|
||||
var wallpaperMediaType = _settingsFacade.WallpaperMedia.DetectMediaType(
|
||||
appearanceSnapshot.ResolvedWallpaperPath ?? wallpaperState.WallpaperPath);
|
||||
var palette = ComponentEditorMaterialThemeAdapter.Build(
|
||||
themeState,
|
||||
wallpaperState,
|
||||
monetPalette,
|
||||
appearanceSnapshot.MonetPalette,
|
||||
wallpaperMediaType);
|
||||
|
||||
window.ApplyTheme(palette);
|
||||
window.ApplyChromeMode(themeState.UseSystemChrome);
|
||||
_appearanceThemeService.ApplyWindowMaterial(window, MaterialSurfaceRole.WindowBackground);
|
||||
}
|
||||
|
||||
private void OnAppearanceThemeChanged(object? sender, AppearanceThemeSnapshot e)
|
||||
{
|
||||
_ = sender;
|
||||
_ = e;
|
||||
|
||||
if (_window is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ApplyTheme(_window);
|
||||
}
|
||||
|
||||
private sealed class HostContext : IComponentEditorHostContext
|
||||
|
||||
@@ -9,57 +9,95 @@ public static class GlassEffectService
|
||||
{
|
||||
public static void ApplyGlassResources(IResourceDictionary resources, ThemeColorContext context)
|
||||
{
|
||||
var materialSurfaceService = new MaterialSurfaceService();
|
||||
var monetPalette = context.MonetPalette;
|
||||
var monetColors = context.MonetColors?.Where(color => color.A > 0).ToArray() ?? [];
|
||||
var primary = monetColors.Length > 0 ? monetColors[0] : context.AccentColor;
|
||||
var secondary = monetColors.Length > 1
|
||||
? monetColors[1]
|
||||
: ColorMath.Blend(primary, Color.Parse("#FFFFFFFF"), 0.12);
|
||||
|
||||
var panelBase = context.IsNightMode
|
||||
? ColorMath.Blend(Color.Parse("#FF101722"), primary, 0.26)
|
||||
: ColorMath.Blend(Color.Parse("#FFF9FBFE"), primary, 0.14);
|
||||
var panelRaised = context.IsNightMode
|
||||
? ColorMath.Blend(Color.Parse("#FF15202C"), secondary, 0.30)
|
||||
: ColorMath.Blend(Color.Parse("#FFFFFFFF"), secondary, 0.18);
|
||||
var overlayBase = context.IsNightMode
|
||||
? ColorMath.Blend(Color.Parse("#FF0E1622"), primary, 0.36)
|
||||
: ColorMath.Blend(Color.Parse("#FFF3F7FD"), primary, 0.20);
|
||||
var primary = context.UseNeutralSurfaces
|
||||
? context.AccentColor
|
||||
: monetPalette?.Primary ?? (monetColors.Length > 0 ? monetColors[0] : context.AccentColor);
|
||||
var neutralButtonBase = context.IsNightMode
|
||||
? Color.Parse("#FF171C24")
|
||||
: Color.Parse("#FFFFFFFF");
|
||||
if (!context.UseNeutralSurfaces)
|
||||
{
|
||||
neutralButtonBase = ColorMath.Blend(
|
||||
neutralButtonBase,
|
||||
primary,
|
||||
context.IsNightMode ? 0.08 : 0.04);
|
||||
}
|
||||
|
||||
var buttonBackground = Color.FromArgb(
|
||||
context.IsNightMode ? (byte)0x4D : (byte)0x52,
|
||||
panelRaised.R,
|
||||
panelRaised.G,
|
||||
panelRaised.B);
|
||||
var buttonBorder = Color.FromArgb(
|
||||
context.IsNightMode ? (byte)0x36 : (byte)0x26,
|
||||
primary.R,
|
||||
primary.G,
|
||||
primary.B);
|
||||
context.IsNightMode ? (byte)0xF0 : (byte)0xFF,
|
||||
neutralButtonBase.R,
|
||||
neutralButtonBase.G,
|
||||
neutralButtonBase.B);
|
||||
var buttonBorder = ColorMath.WithAlpha(
|
||||
context.IsNightMode
|
||||
? ColorMath.Blend(neutralButtonBase, Color.Parse("#FFFFFFFF"), 0.14)
|
||||
: ColorMath.Blend(neutralButtonBase, Color.Parse("#FF334155"), 0.10),
|
||||
context.IsNightMode ? (byte)0x26 : (byte)0x14);
|
||||
|
||||
resources["AdaptiveButtonBackgroundBrush"] = new SolidColorBrush(buttonBackground);
|
||||
resources["AdaptiveButtonBorderBrush"] = new SolidColorBrush(buttonBorder);
|
||||
resources["AdaptiveButtonHoverBackgroundBrush"] = new SolidColorBrush(
|
||||
ColorMath.WithAlpha(ColorMath.Blend(buttonBackground, primary, 0.18), context.IsNightMode ? (byte)0x72 : (byte)0x7A));
|
||||
ColorMath.WithAlpha(
|
||||
ColorMath.Blend(buttonBackground, primary, context.IsNightMode ? 0.14 : 0.08),
|
||||
context.IsNightMode ? (byte)0xF4 : (byte)0xFF));
|
||||
resources["AdaptiveButtonPressedBackgroundBrush"] = new SolidColorBrush(
|
||||
ColorMath.WithAlpha(ColorMath.Blend(buttonBackground, primary, 0.30), context.IsNightMode ? (byte)0x8A : (byte)0x8C));
|
||||
ColorMath.WithAlpha(
|
||||
ColorMath.Blend(buttonBackground, primary, context.IsNightMode ? 0.24 : 0.16),
|
||||
context.IsNightMode ? (byte)0xF8 : (byte)0xFF));
|
||||
|
||||
resources["AdaptiveGlassPanelBackgroundBrush"] = new SolidColorBrush(
|
||||
Color.FromArgb(context.IsNightMode ? (byte)0xF2 : (byte)0xFA, panelBase.R, panelBase.G, panelBase.B));
|
||||
resources["AdaptiveGlassPanelBorderBrush"] = new SolidColorBrush(
|
||||
Color.FromArgb(context.IsNightMode ? (byte)0x38 : (byte)0x24, primary.R, primary.G, primary.B));
|
||||
resources["AdaptiveGlassStrongBackgroundBrush"] = new SolidColorBrush(
|
||||
Color.FromArgb(context.IsNightMode ? (byte)0xF6 : (byte)0xFC, panelRaised.R, panelRaised.G, panelRaised.B));
|
||||
resources["AdaptiveGlassStrongBorderBrush"] = new SolidColorBrush(
|
||||
Color.FromArgb(context.IsNightMode ? (byte)0x4A : (byte)0x2C, secondary.R, secondary.G, secondary.B));
|
||||
resources["AdaptiveGlassOverlayBackgroundBrush"] = new SolidColorBrush(
|
||||
Color.FromArgb(context.IsNightMode ? (byte)0xEA : (byte)0xF4, overlayBase.R, overlayBase.G, overlayBase.B));
|
||||
var windowSurface = materialSurfaceService.GetSurface(context, MaterialSurfaceRole.WindowBackground);
|
||||
var settingsWindowSurface = materialSurfaceService.GetSurface(context, MaterialSurfaceRole.SettingsWindowBackground);
|
||||
var dockSurface = materialSurfaceService.GetSurface(context, MaterialSurfaceRole.DockBackground);
|
||||
var statusBarSurface = materialSurfaceService.GetSurface(context, MaterialSurfaceRole.StatusBarBackground);
|
||||
var desktopComponentSurface = materialSurfaceService.GetSurface(context, MaterialSurfaceRole.DesktopComponentHost);
|
||||
var statusBarComponentSurface = materialSurfaceService.GetSurface(context, MaterialSurfaceRole.StatusBarComponentHost);
|
||||
var overlaySurface = materialSurfaceService.GetSurface(context, MaterialSurfaceRole.OverlayPanel);
|
||||
var strongSurfaceColor = ColorMath.Blend(
|
||||
desktopComponentSurface.BackgroundColor,
|
||||
overlaySurface.BackgroundColor,
|
||||
context.IsNightMode ? 0.18 : 0.12);
|
||||
var strongBorderColor = ColorMath.WithAlpha(
|
||||
desktopComponentSurface.BorderColor,
|
||||
context.IsNightMode ? (byte)0x20 : (byte)0x12);
|
||||
var panelBorderColor = ColorMath.WithAlpha(
|
||||
desktopComponentSurface.BorderColor,
|
||||
context.IsNightMode ? (byte)0x18 : (byte)0x10);
|
||||
|
||||
resources["AdaptiveGlassPanelBlurRadius"] = context.IsNightMode ? 22.0 : 28.0;
|
||||
resources["AdaptiveGlassStrongBlurRadius"] = context.IsNightMode ? 28.0 : 34.0;
|
||||
resources["AdaptiveGlassOverlayBlurRadius"] = context.IsNightMode ? 34.0 : 40.0;
|
||||
resources["AdaptiveWindowBackgroundBrush"] = new SolidColorBrush(windowSurface.BackgroundColor);
|
||||
resources["AdaptiveWindowBorderBrush"] = new SolidColorBrush(windowSurface.BorderColor);
|
||||
resources["AdaptiveSettingsWindowBackgroundBrush"] = new SolidColorBrush(settingsWindowSurface.BackgroundColor);
|
||||
resources["AdaptiveSettingsWindowBorderBrush"] = new SolidColorBrush(settingsWindowSurface.BorderColor);
|
||||
resources["AdaptiveDockBackgroundBrush"] = new SolidColorBrush(dockSurface.BackgroundColor);
|
||||
resources["AdaptiveDockBorderBrush"] = new SolidColorBrush(dockSurface.BorderColor);
|
||||
resources["AdaptiveStatusBarBackgroundBrush"] = new SolidColorBrush(statusBarSurface.BackgroundColor);
|
||||
resources["AdaptiveStatusBarBorderBrush"] = new SolidColorBrush(statusBarSurface.BorderColor);
|
||||
resources["AdaptiveDesktopComponentHostBackgroundBrush"] = new SolidColorBrush(desktopComponentSurface.BackgroundColor);
|
||||
resources["AdaptiveDesktopComponentHostBorderBrush"] = new SolidColorBrush(desktopComponentSurface.BorderColor);
|
||||
resources["AdaptiveStatusBarComponentHostBackgroundBrush"] = new SolidColorBrush(statusBarComponentSurface.BackgroundColor);
|
||||
resources["AdaptiveStatusBarComponentHostBorderBrush"] = new SolidColorBrush(statusBarComponentSurface.BorderColor);
|
||||
|
||||
resources["AdaptiveGlassPanelBackgroundBrush"] = new SolidColorBrush(desktopComponentSurface.BackgroundColor);
|
||||
resources["AdaptiveGlassPanelBorderBrush"] = new SolidColorBrush(panelBorderColor);
|
||||
resources["AdaptiveGlassStrongBackgroundBrush"] = new SolidColorBrush(strongSurfaceColor);
|
||||
resources["AdaptiveGlassStrongBorderBrush"] = new SolidColorBrush(strongBorderColor);
|
||||
resources["AdaptiveDockGlassBackgroundBrush"] = new SolidColorBrush(dockSurface.BackgroundColor);
|
||||
resources["AdaptiveDockGlassBorderBrush"] = new SolidColorBrush(dockSurface.BorderColor);
|
||||
resources["AdaptiveGlassOverlayBackgroundBrush"] = new SolidColorBrush(overlaySurface.BackgroundColor);
|
||||
|
||||
resources["AdaptiveGlassPanelBlurRadius"] = desktopComponentSurface.BlurRadius;
|
||||
resources["AdaptiveGlassStrongBlurRadius"] = dockSurface.BlurRadius;
|
||||
resources["AdaptiveGlassOverlayBlurRadius"] = overlaySurface.BlurRadius;
|
||||
resources["AdaptiveGlassPanelOpacity"] = 1.0;
|
||||
resources["AdaptiveGlassStrongOpacity"] = 1.0;
|
||||
resources["AdaptiveGlassOverlayOpacity"] = context.IsNightMode ? 0.95 : 0.98;
|
||||
resources["AdaptiveGlassOverlayOpacity"] = overlaySurface.Opacity;
|
||||
resources["AdaptiveGlassNoiseOpacity"] = context.IsNightMode ? 0.012 : 0.008;
|
||||
|
||||
resources["AdaptiveDockOpacity"] = dockSurface.Opacity;
|
||||
resources["AdaptiveStatusBarOpacity"] = statusBarSurface.Opacity;
|
||||
resources["AdaptiveDesktopComponentHostOpacity"] = desktopComponentSurface.Opacity;
|
||||
resources["AdaptiveStatusBarComponentHostOpacity"] = statusBarComponentSurface.Opacity;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,84 +1,86 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using Avalonia;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Media.Imaging;
|
||||
using Avalonia.Platform;
|
||||
using LanMountainDesktop.Models;
|
||||
using MaterialColorUtilities.Palettes;
|
||||
using MaterialColorUtilities.Utils;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace LanMountainDesktop.Services;
|
||||
|
||||
public sealed class MonetColorService
|
||||
{
|
||||
private static readonly Color DefaultSeedColor = Color.Parse("#FF3B82F6");
|
||||
|
||||
public MonetPalette BuildPalette(Bitmap? wallpaper, bool nightMode, Color? preferredSeed = null)
|
||||
{
|
||||
var recommended = BuildRecommendedPalette(nightMode);
|
||||
var seed = preferredSeed ?? TryExtractSeedColor(wallpaper) ?? TryGetSystemMonetSeedColor() ?? Color.Parse("#FF3B82F6");
|
||||
var monet = BuildMonetPalette(seed, nightMode);
|
||||
return new MonetPalette(recommended, monet);
|
||||
var wallpaperCandidates = wallpaper is null
|
||||
? []
|
||||
: ExtractSeedCandidates(wallpaper);
|
||||
return BuildPaletteCore(wallpaperCandidates, nightMode, preferredSeed);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<Color> BuildRecommendedPalette(bool nightMode)
|
||||
public MonetPalette BuildPaletteFromSeedCandidates(
|
||||
IReadOnlyList<Color>? seedCandidates,
|
||||
bool nightMode,
|
||||
Color? preferredSeed = null)
|
||||
{
|
||||
if (nightMode)
|
||||
{
|
||||
return
|
||||
[
|
||||
Color.Parse("#FF3B82F6"),
|
||||
Color.Parse("#FF22C55E"),
|
||||
Color.Parse("#FFF59E0B"),
|
||||
Color.Parse("#FFF97316"),
|
||||
Color.Parse("#FFA855F7"),
|
||||
Color.Parse("#FFEF4444")
|
||||
];
|
||||
}
|
||||
|
||||
return
|
||||
[
|
||||
Color.Parse("#FF1D4ED8"),
|
||||
Color.Parse("#FF15803D"),
|
||||
Color.Parse("#FFB45309"),
|
||||
Color.Parse("#FFC2410C"),
|
||||
Color.Parse("#FF7E22CE"),
|
||||
Color.Parse("#FFB91C1C")
|
||||
];
|
||||
return BuildPaletteCore(seedCandidates ?? [], nightMode, preferredSeed);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<Color> BuildMonetPalette(Color seed, bool nightMode)
|
||||
public IReadOnlyList<Color> ExtractSeedCandidates(Bitmap wallpaper)
|
||||
{
|
||||
var (hue, saturation, value) = ToHsv(seed);
|
||||
var valueBase = nightMode ? Math.Max(0.70, value) : Math.Min(0.72, Math.Max(0.35, value));
|
||||
var saturationBase = Math.Clamp(saturation, 0.22, 0.74);
|
||||
var offsets = new[] { 0d, 16d, -16d, 36d, -36d, 180d };
|
||||
var palette = new List<Color>(offsets.Length);
|
||||
|
||||
for (var i = 0; i < offsets.Length; i++)
|
||||
{
|
||||
var hueShift = NormalizeHue(hue + offsets[i]);
|
||||
var sat = Math.Clamp(saturationBase + ((i % 2 == 0) ? 0.05 : -0.05), 0.18, 0.86);
|
||||
var val = Math.Clamp(valueBase + ((i < 3) ? 0.06 : -0.04), 0.32, 0.92);
|
||||
palette.Add(FromHsv(hueShift, sat, val));
|
||||
}
|
||||
|
||||
return palette;
|
||||
ArgumentNullException.ThrowIfNull(wallpaper);
|
||||
return ExtractWallpaperSeedCandidates(wallpaper);
|
||||
}
|
||||
|
||||
private static Color? TryExtractSeedColor(Bitmap? wallpaper)
|
||||
private static Color? ResolveSeedColor(
|
||||
IReadOnlyList<Color> wallpaperCandidates,
|
||||
Color? preferredSeed)
|
||||
{
|
||||
if (wallpaper is null)
|
||||
if (wallpaperCandidates.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (preferredSeed is { } explicitSeed)
|
||||
{
|
||||
var exact = wallpaperCandidates.FirstOrDefault(candidate => candidate == explicitSeed);
|
||||
if (exact != default)
|
||||
{
|
||||
return exact;
|
||||
}
|
||||
}
|
||||
|
||||
return wallpaperCandidates[0];
|
||||
}
|
||||
|
||||
private static IReadOnlyList<Color> BuildFallbackSeedCandidates()
|
||||
{
|
||||
return
|
||||
[
|
||||
Color.Parse("#FF3B82F6"),
|
||||
Color.Parse("#FF22C55E"),
|
||||
Color.Parse("#FFF59E0B"),
|
||||
Color.Parse("#FFF97316"),
|
||||
Color.Parse("#FFA855F7")
|
||||
];
|
||||
}
|
||||
|
||||
private static IReadOnlyList<Color> ExtractWallpaperSeedCandidates(Bitmap wallpaper)
|
||||
{
|
||||
try
|
||||
{
|
||||
var sampleWidth = Math.Clamp(wallpaper.PixelSize.Width, 1, 48);
|
||||
var sampleHeight = Math.Clamp(wallpaper.PixelSize.Height, 1, 48);
|
||||
var width = Math.Clamp(wallpaper.PixelSize.Width, 1, 96);
|
||||
var height = Math.Clamp(wallpaper.PixelSize.Height, 1, 96);
|
||||
|
||||
using var scaledBitmap = wallpaper.CreateScaledBitmap(
|
||||
new PixelSize(sampleWidth, sampleHeight),
|
||||
new PixelSize(width, height),
|
||||
BitmapInterpolationMode.MediumQuality);
|
||||
using var writeable = new WriteableBitmap(
|
||||
scaledBitmap.PixelSize,
|
||||
@@ -91,55 +93,52 @@ public sealed class MonetColorService
|
||||
var byteCount = framebuffer.RowBytes * framebuffer.Size.Height;
|
||||
if (byteCount <= 0 || framebuffer.Address == IntPtr.Zero)
|
||||
{
|
||||
return null;
|
||||
return [];
|
||||
}
|
||||
|
||||
var pixelBuffer = new byte[byteCount];
|
||||
Marshal.Copy(framebuffer.Address, pixelBuffer, 0, byteCount);
|
||||
|
||||
double bestScore = double.MinValue;
|
||||
Color? bestColor = null;
|
||||
|
||||
var argbPixels = new List<uint>(framebuffer.Size.Width * framebuffer.Size.Height);
|
||||
for (var y = 0; y < framebuffer.Size.Height; y++)
|
||||
{
|
||||
var rowOffset = y * framebuffer.RowBytes;
|
||||
for (var x = 0; x < framebuffer.Size.Width; x++)
|
||||
{
|
||||
var index = rowOffset + (x * 4);
|
||||
var alpha = pixelBuffer[index + 3] / 255d;
|
||||
if (alpha <= 0.15)
|
||||
var alpha = pixelBuffer[index + 3];
|
||||
if (alpha <= 32)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var blue = (pixelBuffer[index] / 255d) / alpha;
|
||||
var green = (pixelBuffer[index + 1] / 255d) / alpha;
|
||||
var red = (pixelBuffer[index + 2] / 255d) / alpha;
|
||||
red = Math.Clamp(red, 0, 1);
|
||||
green = Math.Clamp(green, 0, 1);
|
||||
blue = Math.Clamp(blue, 0, 1);
|
||||
|
||||
var color = Color.FromRgb(
|
||||
(byte)Math.Round(red * 255),
|
||||
(byte)Math.Round(green * 255),
|
||||
(byte)Math.Round(blue * 255));
|
||||
var (_, saturation, value) = ToHsv(color);
|
||||
var score = (saturation * 1.8) + (value * 0.6);
|
||||
if (score <= bestScore)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
bestScore = score;
|
||||
bestColor = color;
|
||||
var blue = pixelBuffer[index];
|
||||
var green = pixelBuffer[index + 1];
|
||||
var red = pixelBuffer[index + 2];
|
||||
argbPixels.Add(
|
||||
((uint)alpha << 24) |
|
||||
((uint)red << 16) |
|
||||
((uint)green << 8) |
|
||||
blue);
|
||||
}
|
||||
}
|
||||
|
||||
return bestColor;
|
||||
if (argbPixels.Count == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var extracted = ImageUtils.ColorsFromImage(argbPixels.ToArray());
|
||||
return extracted
|
||||
.Select(FromArgb)
|
||||
.Distinct()
|
||||
.Take(6)
|
||||
.ToArray();
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
return null;
|
||||
AppLogger.Warn("Appearance.WallpaperPalette", "Failed to extract wallpaper seed candidates.", ex);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,11 +160,17 @@ public sealed class MonetColorService
|
||||
return null;
|
||||
}
|
||||
|
||||
var bytes = BitConverter.GetBytes(accentDword);
|
||||
var blue = bytes[0];
|
||||
var green = bytes[1];
|
||||
var red = bytes[2];
|
||||
return Color.FromRgb(red, green, blue);
|
||||
var accentColor = unchecked((uint)accentDword);
|
||||
var a = (byte)((accentColor >> 24) & 0xFF);
|
||||
var b = (byte)((accentColor >> 16) & 0xFF);
|
||||
var g = (byte)((accentColor >> 8) & 0xFF);
|
||||
var r = (byte)(accentColor & 0xFF);
|
||||
if (a == 0)
|
||||
{
|
||||
a = 0xFF;
|
||||
}
|
||||
|
||||
return Color.FromArgb(a, r, g, b);
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -173,78 +178,51 @@ public sealed class MonetColorService
|
||||
}
|
||||
}
|
||||
|
||||
private static (double Hue, double Saturation, double Value) ToHsv(Color color)
|
||||
private static uint ToArgb(Color color)
|
||||
{
|
||||
var red = color.R / 255d;
|
||||
var green = color.G / 255d;
|
||||
var blue = color.B / 255d;
|
||||
var max = Math.Max(red, Math.Max(green, blue));
|
||||
var min = Math.Min(red, Math.Min(green, blue));
|
||||
var delta = max - min;
|
||||
|
||||
double hue;
|
||||
if (delta < 0.0001)
|
||||
{
|
||||
hue = 0;
|
||||
}
|
||||
else if (Math.Abs(max - red) < 0.0001)
|
||||
{
|
||||
hue = 60 * (((green - blue) / delta) % 6);
|
||||
}
|
||||
else if (Math.Abs(max - green) < 0.0001)
|
||||
{
|
||||
hue = 60 * (((blue - red) / delta) + 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
hue = 60 * (((red - green) / delta) + 4);
|
||||
}
|
||||
|
||||
hue = NormalizeHue(hue);
|
||||
var saturation = max <= 0.0001 ? 0 : delta / max;
|
||||
return (hue, saturation, max);
|
||||
return
|
||||
((uint)color.A << 24) |
|
||||
((uint)color.R << 16) |
|
||||
((uint)color.G << 8) |
|
||||
color.B;
|
||||
}
|
||||
|
||||
private static Color FromHsv(double hue, double saturation, double value)
|
||||
private static Color FromArgb(uint argb)
|
||||
{
|
||||
hue = NormalizeHue(hue);
|
||||
saturation = Math.Clamp(saturation, 0, 1);
|
||||
value = Math.Clamp(value, 0, 1);
|
||||
|
||||
if (saturation <= 0.0001)
|
||||
{
|
||||
var gray = (byte)Math.Round(value * 255);
|
||||
return Color.FromRgb(gray, gray, gray);
|
||||
}
|
||||
|
||||
var chroma = value * saturation;
|
||||
var x = chroma * (1 - Math.Abs(((hue / 60d) % 2) - 1));
|
||||
var m = value - chroma;
|
||||
|
||||
(double r, double g, double b) = hue switch
|
||||
{
|
||||
>= 0 and < 60 => (chroma, x, 0d),
|
||||
>= 60 and < 120 => (x, chroma, 0d),
|
||||
>= 120 and < 180 => (0d, chroma, x),
|
||||
>= 180 and < 240 => (0d, x, chroma),
|
||||
>= 240 and < 300 => (x, 0d, chroma),
|
||||
_ => (chroma, 0d, x)
|
||||
};
|
||||
|
||||
var red = (byte)Math.Round((r + m) * 255);
|
||||
var green = (byte)Math.Round((g + m) * 255);
|
||||
var blue = (byte)Math.Round((b + m) * 255);
|
||||
return Color.FromRgb(red, green, blue);
|
||||
var a = (byte)((argb >> 24) & 0xFF);
|
||||
var r = (byte)((argb >> 16) & 0xFF);
|
||||
var g = (byte)((argb >> 8) & 0xFF);
|
||||
var b = (byte)(argb & 0xFF);
|
||||
return Color.FromArgb(a, r, g, b);
|
||||
}
|
||||
|
||||
private static double NormalizeHue(double hue)
|
||||
private static MonetPalette BuildPaletteCore(
|
||||
IReadOnlyList<Color> wallpaperCandidates,
|
||||
bool nightMode,
|
||||
Color? preferredSeed)
|
||||
{
|
||||
hue %= 360;
|
||||
if (hue < 0)
|
||||
{
|
||||
hue += 360;
|
||||
}
|
||||
var recommendedColors = wallpaperCandidates.Count > 0
|
||||
? wallpaperCandidates
|
||||
: BuildFallbackSeedCandidates();
|
||||
var seed = ResolveSeedColor(wallpaperCandidates, preferredSeed)
|
||||
?? preferredSeed
|
||||
?? TryGetSystemMonetSeedColor()
|
||||
?? DefaultSeedColor;
|
||||
|
||||
return hue;
|
||||
var corePalette = CorePalette.Of(ToArgb(seed), Style.TonalSpot);
|
||||
var primary = FromArgb(corePalette.Primary.Tone(nightMode ? 80u : 40u));
|
||||
var secondary = FromArgb(corePalette.Secondary.Tone(nightMode ? 80u : 40u));
|
||||
var tertiary = FromArgb(corePalette.Tertiary.Tone(nightMode ? 80u : 40u));
|
||||
var neutral = FromArgb(corePalette.Neutral.Tone(nightMode ? 20u : 94u));
|
||||
var neutralVariant = FromArgb(corePalette.NeutralVariant.Tone(nightMode ? 30u : 90u));
|
||||
|
||||
return new MonetPalette(
|
||||
recommendedColors,
|
||||
seed,
|
||||
primary,
|
||||
secondary,
|
||||
tertiary,
|
||||
neutral,
|
||||
neutralVariant);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ public static class PendingRestartStateService
|
||||
{
|
||||
public const string RenderModeReason = "RenderMode";
|
||||
public const string PluginCatalogReason = "PluginCatalog";
|
||||
public const string SettingsWindowReason = "SettingsWindow";
|
||||
|
||||
private static readonly object Gate = new();
|
||||
private static readonly HashSet<string> PendingReasons = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using LanMountainDesktop.Models;
|
||||
using LanMountainDesktop.PluginSdk;
|
||||
using LanMountainDesktop.Services;
|
||||
|
||||
namespace LanMountainDesktop.Services.Settings;
|
||||
|
||||
@@ -16,7 +17,13 @@ public enum WallpaperMediaType
|
||||
|
||||
public sealed record GridSettingsState(int ShortSideCells, string SpacingPreset, int EdgeInsetPercent);
|
||||
public sealed record WallpaperSettingsState(string? WallpaperPath, string Type, string? Color, string Placement);
|
||||
public sealed record ThemeAppearanceSettingsState(bool IsNightMode, string? ThemeColor, bool UseSystemChrome);
|
||||
public sealed record ThemeAppearanceSettingsState(
|
||||
bool IsNightMode,
|
||||
string? ThemeColor,
|
||||
bool UseSystemChrome,
|
||||
string ThemeColorMode = ThemeAppearanceValues.ColorModeDefaultNeutral,
|
||||
string SystemMaterialMode = ThemeAppearanceValues.MaterialNone,
|
||||
string? SelectedWallpaperSeed = null);
|
||||
public sealed record StatusBarSettingsState(
|
||||
IReadOnlyList<string> TopStatusComponentIds,
|
||||
IReadOnlyList<string> PinnedTaskbarActions,
|
||||
@@ -37,6 +44,9 @@ public sealed record WeatherSettingsState(
|
||||
bool NoTlsRequests,
|
||||
string LocationQuery);
|
||||
public sealed record RegionSettingsState(string LanguageCode, string? TimeZoneId);
|
||||
public sealed record PrivacySettingsState(
|
||||
bool UploadAnonymousCrashData,
|
||||
bool UploadAnonymousUsageData);
|
||||
public sealed record UpdateSettingsState(
|
||||
bool AutoCheckUpdates,
|
||||
bool IncludePrereleaseUpdates,
|
||||
@@ -166,6 +176,12 @@ public interface IRegionSettingsService
|
||||
TimeZoneService GetTimeZoneService();
|
||||
}
|
||||
|
||||
public interface IPrivacySettingsService
|
||||
{
|
||||
PrivacySettingsState Get();
|
||||
void Save(PrivacySettingsState state);
|
||||
}
|
||||
|
||||
public interface IUpdateSettingsService
|
||||
{
|
||||
UpdateSettingsState Get();
|
||||
@@ -224,6 +240,7 @@ public interface ISettingsFacadeService
|
||||
IStatusBarSettingsService StatusBar { get; }
|
||||
IWeatherSettingsService Weather { get; }
|
||||
IRegionSettingsService Region { get; }
|
||||
IPrivacySettingsService Privacy { get; }
|
||||
IUpdateSettingsService Update { get; }
|
||||
ILauncherCatalogService LauncherCatalog { get; }
|
||||
ILauncherPolicyService LauncherPolicy { get; }
|
||||
|
||||
@@ -93,9 +93,12 @@ internal sealed class WallpaperSettingsService : IWallpaperSettingsService
|
||||
public WallpaperSettingsState Get()
|
||||
{
|
||||
var snapshot = _settingsService.Load();
|
||||
var normalizedType = snapshot.WallpaperType ?? "Image";
|
||||
return new WallpaperSettingsState(
|
||||
snapshot.WallpaperPath,
|
||||
snapshot.WallpaperType ?? "Image",
|
||||
string.Equals(normalizedType, "SolidColor", StringComparison.OrdinalIgnoreCase)
|
||||
? null
|
||||
: snapshot.WallpaperPath,
|
||||
normalizedType,
|
||||
snapshot.WallpaperColor,
|
||||
snapshot.WallpaperPlacement);
|
||||
}
|
||||
@@ -103,9 +106,24 @@ internal sealed class WallpaperSettingsService : IWallpaperSettingsService
|
||||
public void Save(WallpaperSettingsState state)
|
||||
{
|
||||
var snapshot = _settingsService.Load();
|
||||
snapshot.WallpaperPath = state.WallpaperPath;
|
||||
snapshot.WallpaperType = state.Type;
|
||||
snapshot.WallpaperColor = state.Color;
|
||||
var normalizedType = string.IsNullOrWhiteSpace(state.Type)
|
||||
? "Image"
|
||||
: state.Type.Trim();
|
||||
var normalizedPath = string.IsNullOrWhiteSpace(state.WallpaperPath)
|
||||
? null
|
||||
: state.WallpaperPath.Trim();
|
||||
var normalizedColor = string.IsNullOrWhiteSpace(state.Color)
|
||||
? null
|
||||
: state.Color.Trim();
|
||||
|
||||
if (string.Equals(normalizedType, "SolidColor", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
normalizedPath = null;
|
||||
}
|
||||
|
||||
snapshot.WallpaperPath = normalizedPath;
|
||||
snapshot.WallpaperType = normalizedType;
|
||||
snapshot.WallpaperColor = normalizedColor;
|
||||
snapshot.WallpaperPlacement = string.IsNullOrWhiteSpace(state.Placement)
|
||||
? "Fill"
|
||||
: state.Placement.Trim();
|
||||
@@ -233,24 +251,68 @@ internal sealed class ThemeAppearanceService : IThemeAppearanceService
|
||||
return new ThemeAppearanceSettingsState(
|
||||
snapshot.IsNightMode ?? false,
|
||||
snapshot.ThemeColor,
|
||||
snapshot.UseSystemChrome);
|
||||
snapshot.UseSystemChrome,
|
||||
ThemeAppearanceValues.NormalizeThemeColorMode(snapshot.ThemeColorMode, snapshot.ThemeColor),
|
||||
ThemeAppearanceValues.NormalizeSystemMaterialMode(snapshot.SystemMaterialMode),
|
||||
snapshot.SelectedWallpaperSeed);
|
||||
}
|
||||
|
||||
public void Save(ThemeAppearanceSettingsState state)
|
||||
{
|
||||
var snapshot = _settingsService.Load();
|
||||
snapshot.IsNightMode = state.IsNightMode;
|
||||
snapshot.ThemeColor = state.ThemeColor;
|
||||
snapshot.UseSystemChrome = state.UseSystemChrome;
|
||||
var changedKeys = new List<string>();
|
||||
var normalizedThemeColor = string.IsNullOrWhiteSpace(state.ThemeColor) ? null : state.ThemeColor;
|
||||
var normalizedThemeColorMode = ThemeAppearanceValues.NormalizeThemeColorMode(state.ThemeColorMode, state.ThemeColor);
|
||||
var normalizedSystemMaterialMode = ThemeAppearanceValues.NormalizeSystemMaterialMode(state.SystemMaterialMode);
|
||||
var normalizedSelectedWallpaperSeed = string.IsNullOrWhiteSpace(state.SelectedWallpaperSeed)
|
||||
? null
|
||||
: state.SelectedWallpaperSeed;
|
||||
|
||||
if ((snapshot.IsNightMode ?? false) != state.IsNightMode)
|
||||
{
|
||||
snapshot.IsNightMode = state.IsNightMode;
|
||||
changedKeys.Add(nameof(AppSettingsSnapshot.IsNightMode));
|
||||
}
|
||||
|
||||
if (!string.Equals(snapshot.ThemeColor, normalizedThemeColor, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
snapshot.ThemeColor = normalizedThemeColor;
|
||||
changedKeys.Add(nameof(AppSettingsSnapshot.ThemeColor));
|
||||
}
|
||||
|
||||
if (snapshot.UseSystemChrome != state.UseSystemChrome)
|
||||
{
|
||||
snapshot.UseSystemChrome = state.UseSystemChrome;
|
||||
changedKeys.Add(nameof(AppSettingsSnapshot.UseSystemChrome));
|
||||
}
|
||||
|
||||
if (!string.Equals(snapshot.ThemeColorMode, normalizedThemeColorMode, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
snapshot.ThemeColorMode = normalizedThemeColorMode;
|
||||
changedKeys.Add(nameof(AppSettingsSnapshot.ThemeColorMode));
|
||||
}
|
||||
|
||||
if (!string.Equals(snapshot.SystemMaterialMode, normalizedSystemMaterialMode, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
snapshot.SystemMaterialMode = normalizedSystemMaterialMode;
|
||||
changedKeys.Add(nameof(AppSettingsSnapshot.SystemMaterialMode));
|
||||
}
|
||||
|
||||
if (!string.Equals(snapshot.SelectedWallpaperSeed, normalizedSelectedWallpaperSeed, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
snapshot.SelectedWallpaperSeed = normalizedSelectedWallpaperSeed;
|
||||
changedKeys.Add(nameof(AppSettingsSnapshot.SelectedWallpaperSeed));
|
||||
}
|
||||
|
||||
if (changedKeys.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_settingsService.SaveSnapshot(
|
||||
SettingsScope.App,
|
||||
snapshot,
|
||||
changedKeys:
|
||||
[
|
||||
nameof(AppSettingsSnapshot.IsNightMode),
|
||||
nameof(AppSettingsSnapshot.ThemeColor),
|
||||
nameof(AppSettingsSnapshot.UseSystemChrome)
|
||||
]);
|
||||
changedKeys: changedKeys);
|
||||
}
|
||||
|
||||
public MonetPalette BuildPalette(bool nightMode, string? wallpaperPath, string? preferredSeedColor = null)
|
||||
@@ -525,6 +587,39 @@ internal sealed class RegionSettingsService : IRegionSettingsService
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class PrivacySettingsService : IPrivacySettingsService
|
||||
{
|
||||
private readonly ISettingsService _settingsService;
|
||||
|
||||
public PrivacySettingsService(ISettingsService settingsService)
|
||||
{
|
||||
_settingsService = settingsService ?? throw new ArgumentNullException(nameof(settingsService));
|
||||
}
|
||||
|
||||
public PrivacySettingsState Get()
|
||||
{
|
||||
var snapshot = _settingsService.Load();
|
||||
return new PrivacySettingsState(
|
||||
snapshot.UploadAnonymousCrashData,
|
||||
snapshot.UploadAnonymousUsageData);
|
||||
}
|
||||
|
||||
public void Save(PrivacySettingsState state)
|
||||
{
|
||||
var snapshot = _settingsService.Load();
|
||||
snapshot.UploadAnonymousCrashData = state.UploadAnonymousCrashData;
|
||||
snapshot.UploadAnonymousUsageData = state.UploadAnonymousUsageData;
|
||||
_settingsService.SaveSnapshot(
|
||||
SettingsScope.App,
|
||||
snapshot,
|
||||
changedKeys:
|
||||
[
|
||||
nameof(AppSettingsSnapshot.UploadAnonymousCrashData),
|
||||
nameof(AppSettingsSnapshot.UploadAnonymousUsageData)
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class UpdateSettingsService : IUpdateSettingsService, IDisposable
|
||||
{
|
||||
private readonly ISettingsService _settingsService;
|
||||
@@ -920,6 +1015,7 @@ internal sealed class SettingsFacadeService : ISettingsFacadeService, IDisposabl
|
||||
_weatherSettingsService = new WeatherSettingsService(Settings);
|
||||
Weather = _weatherSettingsService;
|
||||
Region = new RegionSettingsService(Settings);
|
||||
Privacy = new PrivacySettingsService(Settings);
|
||||
_updateSettingsService = new UpdateSettingsService(Settings);
|
||||
Update = _updateSettingsService;
|
||||
LauncherCatalog = new LauncherCatalogService();
|
||||
@@ -949,6 +1045,8 @@ internal sealed class SettingsFacadeService : ISettingsFacadeService, IDisposabl
|
||||
|
||||
public IRegionSettingsService Region { get; }
|
||||
|
||||
public IPrivacySettingsService Privacy { get; }
|
||||
|
||||
public IUpdateSettingsService Update { get; }
|
||||
|
||||
public ILauncherCatalogService LauncherCatalog { get; }
|
||||
|
||||
@@ -177,6 +177,7 @@ internal sealed class SettingsPageRegistry : ISettingsPageRegistry, IDisposable
|
||||
services.AddSingleton(_settingsFacade);
|
||||
services.AddSingleton(_settingsFacade.Settings);
|
||||
services.AddSingleton(_settingsFacade.Catalog);
|
||||
services.AddSingleton<IAppearanceThemeService>(_ => HostAppearanceThemeProvider.GetOrCreate());
|
||||
services.AddSingleton(_hostApplicationLifecycle);
|
||||
services.AddSingleton(_localizationService);
|
||||
services.AddSingleton<ILocationService>(_ => HostLocationServiceProvider.GetOrCreate());
|
||||
|
||||
@@ -52,10 +52,10 @@ public interface ISettingsWindowService
|
||||
|
||||
internal sealed class SettingsWindowService : ISettingsWindowService
|
||||
{
|
||||
private static readonly Color DefaultAccentColor = Color.Parse("#FF3B82F6");
|
||||
private readonly ISettingsPageRegistry _pageRegistry;
|
||||
private readonly IHostApplicationLifecycle _hostApplicationLifecycle;
|
||||
private readonly ISettingsFacadeService _settingsFacade;
|
||||
private readonly IAppearanceThemeService _appearanceThemeService;
|
||||
private readonly LocalizationService _localizationService;
|
||||
private SettingsWindowViewModel _viewModel = null!;
|
||||
private SettingsWindow? _window;
|
||||
@@ -68,8 +68,10 @@ internal sealed class SettingsWindowService : ISettingsWindowService
|
||||
_pageRegistry = pageRegistry;
|
||||
_hostApplicationLifecycle = hostApplicationLifecycle;
|
||||
_settingsFacade = settingsFacade;
|
||||
_appearanceThemeService = HostAppearanceThemeProvider.GetOrCreate();
|
||||
_localizationService = new();
|
||||
_settingsFacade.Settings.Changed += OnSettingsChanged;
|
||||
_appearanceThemeService.Changed += OnAppearanceThemeChanged;
|
||||
}
|
||||
|
||||
private string L(string key)
|
||||
@@ -86,9 +88,9 @@ internal sealed class SettingsWindowService : ISettingsWindowService
|
||||
{
|
||||
_pageRegistry.Rebuild();
|
||||
_window ??= CreateWindow();
|
||||
var themeState = _settingsFacade.Theme.Get();
|
||||
_window.ApplyChromeMode(themeState.UseSystemChrome);
|
||||
ApplyTheme(_window, themeState);
|
||||
var appearanceSnapshot = _appearanceThemeService.GetCurrent();
|
||||
_window.ApplyChromeMode(appearanceSnapshot.UseSystemChrome);
|
||||
ApplyTheme(_window);
|
||||
_window.ReloadPages(request.PageId);
|
||||
PositionWindow(_window, request);
|
||||
|
||||
@@ -135,15 +137,15 @@ internal sealed class SettingsWindowService : ISettingsWindowService
|
||||
|
||||
_viewModel = new SettingsWindowViewModel(_localizationService, languageCode).Initialize();
|
||||
|
||||
var themeState = _settingsFacade.Theme.Get();
|
||||
var useSystemChrome = themeState.UseSystemChrome;
|
||||
var appearanceSnapshot = _appearanceThemeService.GetCurrent();
|
||||
var useSystemChrome = appearanceSnapshot.UseSystemChrome;
|
||||
|
||||
var window = new SettingsWindow(
|
||||
_viewModel,
|
||||
_pageRegistry,
|
||||
_hostApplicationLifecycle,
|
||||
useSystemChrome);
|
||||
ApplyTheme(window, themeState);
|
||||
ApplyTheme(window);
|
||||
window.ShowInTaskbar = false;
|
||||
window.Closed += (_, _) =>
|
||||
{
|
||||
@@ -277,13 +279,16 @@ internal sealed class SettingsWindowService : ISettingsWindowService
|
||||
var changedKeys = e.ChangedKeys?.ToArray();
|
||||
var refreshAll = changedKeys is null || changedKeys.Length == 0;
|
||||
var languageChanged = refreshAll || changedKeys.Contains(nameof(AppSettingsSnapshot.LanguageCode), StringComparer.OrdinalIgnoreCase);
|
||||
var liveAppearance = _appearanceThemeService.GetCurrent();
|
||||
var themeChanged =
|
||||
refreshAll ||
|
||||
changedKeys.Contains(nameof(AppSettingsSnapshot.IsNightMode), StringComparer.OrdinalIgnoreCase) ||
|
||||
changedKeys.Contains(nameof(AppSettingsSnapshot.ThemeColor), StringComparer.OrdinalIgnoreCase) ||
|
||||
changedKeys.Contains(nameof(AppSettingsSnapshot.WallpaperPath), StringComparer.OrdinalIgnoreCase) ||
|
||||
changedKeys.Contains(nameof(AppSettingsSnapshot.WallpaperType), StringComparer.OrdinalIgnoreCase) ||
|
||||
changedKeys.Contains(nameof(AppSettingsSnapshot.WallpaperColor), StringComparer.OrdinalIgnoreCase) ||
|
||||
(string.Equals(liveAppearance.ThemeColorMode, ThemeAppearanceValues.ColorModeSeedMonet, StringComparison.OrdinalIgnoreCase) &&
|
||||
changedKeys.Contains(nameof(AppSettingsSnapshot.ThemeColor), StringComparer.OrdinalIgnoreCase)) ||
|
||||
(string.Equals(liveAppearance.ThemeColorMode, ThemeAppearanceValues.ColorModeWallpaperMonet, StringComparison.OrdinalIgnoreCase) &&
|
||||
(changedKeys.Contains(nameof(AppSettingsSnapshot.WallpaperPath), StringComparer.OrdinalIgnoreCase) ||
|
||||
changedKeys.Contains(nameof(AppSettingsSnapshot.WallpaperType), StringComparer.OrdinalIgnoreCase) ||
|
||||
changedKeys.Contains(nameof(AppSettingsSnapshot.WallpaperColor), StringComparer.OrdinalIgnoreCase))) ||
|
||||
changedKeys.Contains(nameof(AppSettingsSnapshot.UseSystemChrome), StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
if (languageChanged)
|
||||
@@ -297,59 +302,36 @@ internal sealed class SettingsWindowService : ISettingsWindowService
|
||||
|
||||
if (themeChanged)
|
||||
{
|
||||
var themeState = _settingsFacade.Theme.Get();
|
||||
_window.ApplyChromeMode(themeState.UseSystemChrome);
|
||||
ApplyTheme(_window, themeState);
|
||||
var appearanceSnapshot = _appearanceThemeService.GetCurrent();
|
||||
_window.ApplyChromeMode(appearanceSnapshot.UseSystemChrome);
|
||||
ApplyTheme(_window);
|
||||
}
|
||||
}, DispatcherPriority.Background);
|
||||
}
|
||||
|
||||
private static void ApplyTheme(SettingsWindow window, ThemeAppearanceSettingsState themeState)
|
||||
private void ApplyTheme(SettingsWindow window)
|
||||
{
|
||||
window.RequestedThemeVariant = themeState.IsNightMode
|
||||
var appearanceSnapshot = _appearanceThemeService.GetCurrent();
|
||||
window.RequestedThemeVariant = appearanceSnapshot.IsNightMode
|
||||
? ThemeVariant.Dark
|
||||
: ThemeVariant.Light;
|
||||
|
||||
var settingsFacade = HostSettingsFacadeProvider.GetOrCreate();
|
||||
var wallpaperState = settingsFacade.Wallpaper.Get();
|
||||
var monetPalette = settingsFacade.Theme.BuildPalette(
|
||||
themeState.IsNightMode,
|
||||
wallpaperState.WallpaperPath,
|
||||
themeState.ThemeColor);
|
||||
var accentColor = ResolveAccentColor(themeState.ThemeColor, monetPalette);
|
||||
var context = new ThemeColorContext(
|
||||
accentColor,
|
||||
IsLightBackground: !themeState.IsNightMode,
|
||||
IsLightNavBackground: !themeState.IsNightMode,
|
||||
IsNightMode: themeState.IsNightMode,
|
||||
MonetColors: monetPalette.MonetColors);
|
||||
ThemeColorSystemService.ApplyThemeResources(window.Resources, context);
|
||||
GlassEffectService.ApplyGlassResources(window.Resources, context);
|
||||
_appearanceThemeService.ApplyThemeResources(window.Resources);
|
||||
_appearanceThemeService.ApplyWindowMaterial(window, MaterialSurfaceRole.SettingsWindowBackground);
|
||||
}
|
||||
|
||||
private static Color ResolveAccentColor(string? colorText, MonetPalette monetPalette)
|
||||
private void OnAppearanceThemeChanged(object? sender, AppearanceThemeSnapshot e)
|
||||
{
|
||||
if (monetPalette.MonetColors is { Count: > 0 })
|
||||
_ = sender;
|
||||
_ = e;
|
||||
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
return monetPalette.MonetColors[0];
|
||||
}
|
||||
|
||||
return TryParseThemeColor(colorText);
|
||||
}
|
||||
|
||||
private static Color TryParseThemeColor(string? colorText)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(colorText))
|
||||
{
|
||||
try
|
||||
if (_window is null || _viewModel is null)
|
||||
{
|
||||
return Color.Parse(colorText);
|
||||
return;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
return DefaultAccentColor;
|
||||
ApplyTheme(_window);
|
||||
}, DispatcherPriority.Background);
|
||||
}
|
||||
}
|
||||
|
||||
87
LanMountainDesktop/Services/ThemeAppearanceValues.cs
Normal file
87
LanMountainDesktop/Services/ThemeAppearanceValues.cs
Normal file
@@ -0,0 +1,87 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace LanMountainDesktop.Services;
|
||||
|
||||
public static class ThemeAppearanceValues
|
||||
{
|
||||
public const string ColorModeDefaultNeutral = "default_neutral";
|
||||
public const string ColorModeSeedMonet = "seed_monet";
|
||||
public const string ColorModeWallpaperMonet = "wallpaper_monet";
|
||||
|
||||
public const string MaterialNone = "none";
|
||||
public const string MaterialMica = "mica";
|
||||
public const string MaterialAcrylic = "acrylic";
|
||||
|
||||
public static readonly IReadOnlyList<string> AllColorModes =
|
||||
[
|
||||
ColorModeDefaultNeutral,
|
||||
ColorModeSeedMonet,
|
||||
ColorModeWallpaperMonet
|
||||
];
|
||||
|
||||
public static readonly IReadOnlyList<string> AllMaterialModes =
|
||||
[
|
||||
MaterialNone,
|
||||
MaterialMica,
|
||||
MaterialAcrylic
|
||||
];
|
||||
|
||||
public static string NormalizeThemeColorMode(string? value, string? themeColor = null)
|
||||
{
|
||||
if (string.Equals(value, ColorModeDefaultNeutral, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ColorModeDefaultNeutral;
|
||||
}
|
||||
|
||||
if (string.Equals(value, ColorModeWallpaperMonet, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ColorModeWallpaperMonet;
|
||||
}
|
||||
|
||||
if (string.Equals(value, ColorModeSeedMonet, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ColorModeSeedMonet;
|
||||
}
|
||||
|
||||
return string.IsNullOrWhiteSpace(themeColor)
|
||||
? ColorModeDefaultNeutral
|
||||
: ColorModeSeedMonet;
|
||||
}
|
||||
|
||||
public static string NormalizeSystemMaterialMode(string? value)
|
||||
{
|
||||
if (string.Equals(value, MaterialMica, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return MaterialMica;
|
||||
}
|
||||
|
||||
if (string.Equals(value, MaterialAcrylic, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return MaterialAcrylic;
|
||||
}
|
||||
|
||||
return MaterialNone;
|
||||
}
|
||||
|
||||
public static IReadOnlyList<string> NormalizeAvailableMaterialModes(IEnumerable<string>? values)
|
||||
{
|
||||
if (values is null)
|
||||
{
|
||||
return [MaterialNone];
|
||||
}
|
||||
|
||||
var normalized = values
|
||||
.Select(NormalizeSystemMaterialMode)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
if (!normalized.Contains(MaterialNone, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
normalized.Insert(0, MaterialNone);
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Media;
|
||||
@@ -68,11 +69,19 @@ public static class ThemeColorSystemService
|
||||
|
||||
private static AppThemePalette BuildPalette(ThemeColorContext context)
|
||||
{
|
||||
var monetPalette = context.MonetPalette;
|
||||
var monetColors = context.MonetColors?.Where(color => color.A > 0).ToArray() ?? [];
|
||||
var accent = monetColors.Length > 0 ? monetColors[0] : context.AccentColor;
|
||||
var secondarySeed = monetColors.Length > 1
|
||||
? monetColors[1]
|
||||
: ColorMath.Blend(accent, Color.Parse("#FFFFFFFF"), 0.14);
|
||||
var accent = context.UseNeutralSurfaces
|
||||
? context.AccentColor
|
||||
: monetPalette?.Primary ?? GetColorOrDefault(monetColors, 0, context.AccentColor);
|
||||
var secondarySeed = monetPalette?.Secondary
|
||||
?? GetColorOrDefault(monetColors, 1, ColorMath.Blend(accent, Color.Parse("#FFFFFFFF"), 0.14));
|
||||
var tertiarySeed = monetPalette?.Tertiary
|
||||
?? GetColorOrDefault(monetColors, 2, ColorMath.Blend(accent, Color.Parse("#FFFFFFFF"), 0.22));
|
||||
var neutralSeed = monetPalette?.Neutral
|
||||
?? GetColorOrDefault(monetColors, 3, context.IsNightMode ? Color.Parse("#FF171C23") : Color.Parse("#FFF2F4F7"));
|
||||
var neutralVariantSeed = monetPalette?.NeutralVariant
|
||||
?? GetColorOrDefault(monetColors, 4, context.IsNightMode ? Color.Parse("#FF20262E") : Color.Parse("#FFE8EDF2"));
|
||||
|
||||
var accentLight1 = ColorMath.Blend(accent, Color.Parse("#FFFFFFFF"), 0.22);
|
||||
var accentLight2 = ColorMath.Blend(accent, Color.Parse("#FFFFFFFF"), 0.38);
|
||||
@@ -83,18 +92,23 @@ public static class ThemeColorSystemService
|
||||
|
||||
var primary = context.IsNightMode ? accentLight1 : accentDark1;
|
||||
var secondary = context.IsNightMode
|
||||
? ColorMath.Blend(secondarySeed, Color.Parse("#FFFFFFFF"), 0.16)
|
||||
: ColorMath.Blend(secondarySeed, Color.Parse("#FF111827"), 0.14);
|
||||
? ColorMath.Blend(secondarySeed, Color.Parse("#FFFFFFFF"), 0.12)
|
||||
: ColorMath.Blend(secondarySeed, Color.Parse("#FF111827"), 0.10);
|
||||
|
||||
var surfaceBase = context.IsNightMode
|
||||
? ColorMath.Blend(Color.Parse("#FF0A1018"), accent, 0.18)
|
||||
: ColorMath.Blend(Color.Parse("#FFF7F9FD"), accent, 0.09);
|
||||
var surfaceRaised = context.IsNightMode
|
||||
? ColorMath.Blend(Color.Parse("#FF121A24"), secondarySeed, 0.24)
|
||||
: ColorMath.Blend(Color.Parse("#FFFCFEFF"), secondarySeed, 0.12);
|
||||
var surfaceOverlayBase = context.IsNightMode
|
||||
? ColorMath.Blend(Color.Parse("#FF18212D"), accent, 0.28)
|
||||
: ColorMath.Blend(Color.Parse("#FFF1F5FB"), accent, 0.16);
|
||||
var baseSurface = context.IsNightMode ? Color.Parse("#FF0B0F14") : Color.Parse("#FFF7F8FA");
|
||||
var raisedSurface = context.IsNightMode ? Color.Parse("#FF131922") : Color.Parse("#FFFFFFFF");
|
||||
var overlaySurface = context.IsNightMode ? Color.Parse("#FF171E28") : Color.Parse("#FFF1F4F8");
|
||||
var navSurfaceBase = context.IsLightNavBackground ? Color.Parse("#FFF8FAFC") : Color.Parse("#FF111827");
|
||||
|
||||
var surfaceBase = context.UseNeutralSurfaces
|
||||
? baseSurface
|
||||
: ColorMath.Blend(baseSurface, neutralSeed, context.IsNightMode ? 0.84 : 0.78);
|
||||
var surfaceRaised = context.UseNeutralSurfaces
|
||||
? raisedSurface
|
||||
: ColorMath.Blend(raisedSurface, neutralVariantSeed, context.IsNightMode ? 0.72 : 0.60);
|
||||
var surfaceOverlayBase = context.UseNeutralSurfaces
|
||||
? overlaySurface
|
||||
: ColorMath.Blend(overlaySurface, neutralVariantSeed, context.IsNightMode ? 0.76 : 0.64);
|
||||
var surfaceOverlay = Color.FromArgb(
|
||||
context.IsNightMode ? (byte)0xE8 : (byte)0xF2,
|
||||
surfaceOverlayBase.R,
|
||||
@@ -115,9 +129,9 @@ public static class ThemeColorSystemService
|
||||
? ColorMath.EnsureContrast(ColorMath.Blend(accent, Color.Parse("#FF0B1220"), 0.20), surfaceRaised, WcagNormalTextContrast)
|
||||
: ColorMath.EnsureContrast(ColorMath.Blend(accent, Color.Parse("#FFFFFFFF"), 0.16), surfaceRaised, WcagNormalTextContrast);
|
||||
|
||||
var navSurface = context.IsLightNavBackground
|
||||
? ColorMath.Blend(surfaceRaised, accentLight2, 0.08)
|
||||
: ColorMath.Blend(Color.Parse("#FF111827"), accentDark2, 0.24);
|
||||
var navSurface = context.UseNeutralSurfaces
|
||||
? navSurfaceBase
|
||||
: ColorMath.Blend(navSurfaceBase, neutralSeed, context.IsNightMode ? 0.66 : 0.70);
|
||||
var navText = ColorMath.EnsureContrast(
|
||||
context.IsLightNavBackground ? Color.Parse("#FF0B1220") : Color.Parse("#FFF8FAFC"),
|
||||
navSurface,
|
||||
@@ -129,18 +143,23 @@ public static class ThemeColorSystemService
|
||||
? Color.FromArgb(0x33, surfaceRaised.R, surfaceRaised.G, surfaceRaised.B)
|
||||
: Color.FromArgb(0x38, navSurface.R, navSurface.G, navSurface.B);
|
||||
var navItemHoverBackground = context.IsLightNavBackground
|
||||
? ColorMath.WithAlpha(ColorMath.Blend(accentLight2, surfaceRaised, 0.30), 0x7A)
|
||||
: ColorMath.WithAlpha(ColorMath.Blend(accentDark1, navSurface, 0.26), 0x88);
|
||||
var navItemSelectedBackground = ColorMath.WithAlpha(accent, context.IsNightMode ? (byte)0xCE : (byte)0xD9);
|
||||
var navSelectionIndicator = ColorMath.EnsureContrast(accentLight1, navSurface, WcagLargeTextContrast);
|
||||
? ColorMath.WithAlpha(ColorMath.Blend(accentLight2, navSurface, 0.12), 0x64)
|
||||
: ColorMath.WithAlpha(ColorMath.Blend(accentDark1, navSurface, 0.18), 0x74);
|
||||
var navItemSelectedBackground = context.UseNeutralSurfaces
|
||||
? ColorMath.WithAlpha(ColorMath.Blend(accent, navSurface, 0.24), context.IsNightMode ? (byte)0x8F : (byte)0x6A)
|
||||
: ColorMath.WithAlpha(ColorMath.Blend(accent, navSurface, context.IsNightMode ? 0.28 : 0.22), context.IsNightMode ? (byte)0x88 : (byte)0x72);
|
||||
var navSelectionIndicator = ColorMath.EnsureContrast(
|
||||
context.UseNeutralSurfaces ? accent : accentLight1,
|
||||
navSurface,
|
||||
WcagLargeTextContrast);
|
||||
|
||||
var toggleOn = context.IsNightMode ? accent : accentDark1;
|
||||
var toggleOff = context.IsNightMode
|
||||
? Color.FromArgb(0x88, accentDark2.R, accentDark2.G, accentDark2.B)
|
||||
: Color.FromArgb(0x88, accentLight2.R, accentLight2.G, accentLight2.B);
|
||||
? Color.FromArgb(0x88, neutralVariantSeed.R, neutralVariantSeed.G, neutralVariantSeed.B)
|
||||
: Color.FromArgb(0x88, neutralVariantSeed.R, neutralVariantSeed.G, neutralVariantSeed.B);
|
||||
var toggleBorder = context.IsNightMode
|
||||
? ColorMath.WithAlpha(ColorMath.Blend(accentLight2, Color.Parse("#FFF8FAFC"), 0.28), 0x8C)
|
||||
: ColorMath.WithAlpha(ColorMath.Blend(accentDark2, Color.Parse("#FF334155"), 0.26), 0x78);
|
||||
? ColorMath.WithAlpha(ColorMath.Blend(neutralVariantSeed, Color.Parse("#FFF8FAFC"), 0.20), 0x8C)
|
||||
: ColorMath.WithAlpha(ColorMath.Blend(tertiarySeed, Color.Parse("#FF334155"), 0.18), 0x78);
|
||||
var onAccent = ColorMath.EnsureContrast(Color.Parse("#FFFFFFFF"), accent, WcagNormalTextContrast);
|
||||
|
||||
return new AppThemePalette(
|
||||
@@ -171,4 +190,11 @@ public static class ThemeColorSystemService
|
||||
toggleOff,
|
||||
toggleBorder);
|
||||
}
|
||||
|
||||
private static Color GetColorOrDefault(IReadOnlyList<Color> colors, int index, Color fallback)
|
||||
{
|
||||
return index >= 0 && index < colors.Count
|
||||
? colors[index]
|
||||
: fallback;
|
||||
}
|
||||
}
|
||||
|
||||
101
LanMountainDesktop/Services/XiaomiWeatherCodeMapper.cs
Normal file
101
LanMountainDesktop/Services/XiaomiWeatherCodeMapper.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace LanMountainDesktop.Services;
|
||||
|
||||
internal enum WeatherConditionBucket
|
||||
{
|
||||
Unknown,
|
||||
Clear,
|
||||
PartlyCloudy,
|
||||
Cloudy,
|
||||
Haze,
|
||||
Fog,
|
||||
RainLight,
|
||||
RainHeavy,
|
||||
Storm,
|
||||
Sleet,
|
||||
Snow
|
||||
}
|
||||
|
||||
internal static class XiaomiWeatherCodeMapper
|
||||
{
|
||||
private readonly record struct WeatherCodeEntry(string Zh, string En, WeatherConditionBucket Bucket);
|
||||
|
||||
private static readonly IReadOnlyDictionary<int, WeatherCodeEntry> Entries = new Dictionary<int, WeatherCodeEntry>
|
||||
{
|
||||
[0] = new("\u6674", "Clear", WeatherConditionBucket.Clear),
|
||||
[1] = new("\u591a\u4e91", "Partly Cloudy", WeatherConditionBucket.PartlyCloudy),
|
||||
[2] = new("\u9634", "Cloudy", WeatherConditionBucket.Cloudy),
|
||||
[3] = new("\u9635\u96e8", "Shower", WeatherConditionBucket.RainLight),
|
||||
[4] = new("\u96f7\u9635\u96e8", "Thunder Shower", WeatherConditionBucket.Storm),
|
||||
[5] = new("\u96f7\u9635\u96e8\u4f34\u6709\u51b0\u96f9", "Thunder Shower with Hail", WeatherConditionBucket.Storm),
|
||||
[6] = new("\u96e8\u5939\u96ea", "Sleet", WeatherConditionBucket.Sleet),
|
||||
[7] = new("\u5c0f\u96e8", "Light Rain", WeatherConditionBucket.RainLight),
|
||||
[8] = new("\u4e2d\u96e8", "Moderate Rain", WeatherConditionBucket.RainHeavy),
|
||||
[9] = new("\u5927\u96e8", "Heavy Rain", WeatherConditionBucket.RainHeavy),
|
||||
[10] = new("\u66b4\u96e8", "Storm", WeatherConditionBucket.RainHeavy),
|
||||
[11] = new("\u5927\u66b4\u96e8", "Heavy Storm", WeatherConditionBucket.RainHeavy),
|
||||
[12] = new("\u7279\u5927\u66b4\u96e8", "Severe Storm", WeatherConditionBucket.RainHeavy),
|
||||
[13] = new("\u9635\u96ea", "Snow Flurry", WeatherConditionBucket.Snow),
|
||||
[14] = new("\u5c0f\u96ea", "Light Snow", WeatherConditionBucket.Snow),
|
||||
[15] = new("\u4e2d\u96ea", "Moderate Snow", WeatherConditionBucket.Snow),
|
||||
[16] = new("\u5927\u96ea", "Heavy Snow", WeatherConditionBucket.Snow),
|
||||
[17] = new("\u66b4\u96ea", "Snowstorm", WeatherConditionBucket.Snow),
|
||||
[18] = new("\u96fe", "Fog", WeatherConditionBucket.Fog),
|
||||
[19] = new("\u51bb\u96e8", "Freezing Rain", WeatherConditionBucket.RainLight),
|
||||
[20] = new("\u6c99\u5c18\u66b4", "Duststorm", WeatherConditionBucket.Haze),
|
||||
[21] = new("\u5c0f\u5230\u4e2d\u96e8", "Light to Moderate Rain", WeatherConditionBucket.RainLight),
|
||||
[22] = new("\u4e2d\u5230\u5927\u96e8", "Moderate to Heavy Rain", WeatherConditionBucket.RainHeavy),
|
||||
[23] = new("\u5927\u5230\u66b4\u96e8", "Heavy Rain to Storm", WeatherConditionBucket.RainHeavy),
|
||||
[24] = new("\u66b4\u96e8\u5230\u5927\u66b4\u96e8", "Storm to Heavy Storm", WeatherConditionBucket.RainHeavy),
|
||||
[25] = new("\u5927\u66b4\u96e8\u5230\u7279\u5927\u66b4\u96e8", "Heavy to Severe Storm", WeatherConditionBucket.RainHeavy),
|
||||
[26] = new("\u5c0f\u5230\u4e2d\u96ea", "Light to Moderate Snow", WeatherConditionBucket.Snow),
|
||||
[27] = new("\u4e2d\u5230\u5927\u96ea", "Moderate to Heavy Snow", WeatherConditionBucket.Snow),
|
||||
[28] = new("\u5927\u5230\u66b4\u96ea", "Heavy Snow to Snowstorm", WeatherConditionBucket.Snow),
|
||||
[29] = new("\u6d6e\u5c18", "Dust", WeatherConditionBucket.Haze),
|
||||
[30] = new("\u626c\u6c99", "Sand", WeatherConditionBucket.Haze),
|
||||
[31] = new("\u5f3a\u6c99\u5c18\u66b4", "Sandstorm", WeatherConditionBucket.Haze),
|
||||
[32] = new("\u6d53\u96fe", "Dense Fog", WeatherConditionBucket.Fog),
|
||||
[49] = new("\u5f3a\u6d53\u96fe", "Strong Fog", WeatherConditionBucket.Fog),
|
||||
[53] = new("\u973e", "Haze", WeatherConditionBucket.Haze),
|
||||
[54] = new("\u4e2d\u5ea6\u973e", "Moderate Haze", WeatherConditionBucket.Haze),
|
||||
[55] = new("\u91cd\u5ea6\u973e", "Heavy Haze", WeatherConditionBucket.Haze),
|
||||
[56] = new("\u4e25\u91cd\u973e", "Severe Haze", WeatherConditionBucket.Haze),
|
||||
[57] = new("\u5927\u96fe", "Heavy Fog", WeatherConditionBucket.Fog),
|
||||
[58] = new("\u7279\u5f3a\u6d53\u96fe", "Extra Heavy Fog", WeatherConditionBucket.Fog),
|
||||
[301] = new("\u96e8", "Rain", WeatherConditionBucket.RainLight),
|
||||
[302] = new("\u96ea", "Snow", WeatherConditionBucket.Snow)
|
||||
};
|
||||
|
||||
public static WeatherConditionBucket ResolveBucket(int? code)
|
||||
{
|
||||
if (!code.HasValue)
|
||||
{
|
||||
return WeatherConditionBucket.Unknown;
|
||||
}
|
||||
|
||||
return Entries.TryGetValue(code.Value, out var entry)
|
||||
? entry.Bucket
|
||||
: WeatherConditionBucket.Unknown;
|
||||
}
|
||||
|
||||
public static string? ResolveDisplayText(int? code, string locale)
|
||||
{
|
||||
if (!code.HasValue)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Entries.TryGetValue(code.Value, out var entry))
|
||||
{
|
||||
return locale.StartsWith("zh", StringComparison.OrdinalIgnoreCase)
|
||||
? entry.Zh
|
||||
: entry.En;
|
||||
}
|
||||
|
||||
return locale.StartsWith("zh", StringComparison.OrdinalIgnoreCase)
|
||||
? $"\u5929\u6c14\u7801 {code.Value}"
|
||||
: $"Weather {code.Value}";
|
||||
}
|
||||
}
|
||||
@@ -39,42 +39,6 @@ public sealed class XiaomiWeatherService : IWeatherDataService, IDisposable
|
||||
{
|
||||
private sealed record CacheEntry(WeatherSnapshot Snapshot, DateTimeOffset ExpireAt);
|
||||
|
||||
private static readonly IReadOnlyDictionary<int, string> ZhWeatherDescriptions = new Dictionary<int, string>
|
||||
{
|
||||
[0] = "\u6674",
|
||||
[1] = "\u591a\u4e91",
|
||||
[2] = "\u9634",
|
||||
[3] = "\u9635\u96e8",
|
||||
[4] = "\u96f7\u9635\u96e8",
|
||||
[7] = "\u5c0f\u96e8",
|
||||
[8] = "\u4e2d\u96e8",
|
||||
[9] = "\u5927\u96e8",
|
||||
[13] = "\u9635\u96ea",
|
||||
[14] = "\u5c0f\u96ea",
|
||||
[15] = "\u4e2d\u96ea",
|
||||
[16] = "\u5927\u96ea",
|
||||
[18] = "\u96fe",
|
||||
[32] = "\u973e"
|
||||
};
|
||||
|
||||
private static readonly IReadOnlyDictionary<int, string> EnWeatherDescriptions = new Dictionary<int, string>
|
||||
{
|
||||
[0] = "Clear",
|
||||
[1] = "Partly Cloudy",
|
||||
[2] = "Cloudy",
|
||||
[3] = "Shower",
|
||||
[4] = "Thunder Shower",
|
||||
[7] = "Light Rain",
|
||||
[8] = "Moderate Rain",
|
||||
[9] = "Heavy Rain",
|
||||
[13] = "Snow Flurry",
|
||||
[14] = "Light Snow",
|
||||
[15] = "Moderate Snow",
|
||||
[16] = "Heavy Snow",
|
||||
[18] = "Fog",
|
||||
[32] = "Haze"
|
||||
};
|
||||
|
||||
private readonly XiaomiWeatherApiOptions _options;
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly bool _ownsHttpClient;
|
||||
@@ -412,9 +376,7 @@ public sealed class XiaomiWeatherService : IWeatherDataService, IDisposable
|
||||
TryGetNode(payload, "hourly") ??
|
||||
TryGetNode(payload, "hourlyForecast");
|
||||
|
||||
var weatherCode = ReadInt(currentNode, "weather", "value") ??
|
||||
ReadInt(currentNode, "weatherCode") ??
|
||||
ReadInt(currentNode, "code");
|
||||
var weatherCode = ReadWeatherCode(currentNode);
|
||||
|
||||
var weatherText = ReadString(currentNode, "weather", "desc") ??
|
||||
ReadString(currentNode, "weather", "text") ??
|
||||
@@ -497,8 +459,12 @@ public sealed class XiaomiWeatherService : IWeatherDataService, IDisposable
|
||||
var sunItem = GetArrayItem(sunArray, i);
|
||||
var precipitationItem = GetArrayItem(precipitationArray, i);
|
||||
|
||||
var dayCode = ReadInt(weatherItem, "from") ?? ReadInt(weatherItem, "day");
|
||||
var nightCode = ReadInt(weatherItem, "to") ?? ReadInt(weatherItem, "night");
|
||||
var dayCode = ReadInt(weatherItem, "from") ??
|
||||
ReadInt(weatherItem, "day") ??
|
||||
ReadWeatherCode(weatherItem);
|
||||
var nightCode = ReadInt(weatherItem, "to") ??
|
||||
ReadInt(weatherItem, "night") ??
|
||||
ReadWeatherCode(weatherItem);
|
||||
var dayText = ResolveWeatherDescription(dayCode, locale);
|
||||
var nightText = ResolveWeatherDescription(nightCode, locale);
|
||||
|
||||
@@ -591,11 +557,7 @@ public sealed class XiaomiWeatherService : IWeatherDataService, IDisposable
|
||||
continue;
|
||||
}
|
||||
|
||||
var code = ReadInt(weatherItem, "value") ??
|
||||
ReadInt(weatherItem, "code") ??
|
||||
ReadInt(weatherItem, "weatherCode") ??
|
||||
ReadInt(weatherItem, "from") ??
|
||||
ReadInt(weatherItem);
|
||||
var code = ReadInt(weatherItem, "from") ?? ReadWeatherCode(weatherItem);
|
||||
var weatherText = ReadString(weatherItem, "text") ??
|
||||
ReadString(weatherItem, "desc") ??
|
||||
ResolveWeatherDescription(code, locale);
|
||||
@@ -640,11 +602,7 @@ public sealed class XiaomiWeatherService : IWeatherDataService, IDisposable
|
||||
continue;
|
||||
}
|
||||
|
||||
var code = ReadInt(item, "weatherCode") ??
|
||||
ReadInt(item, "code") ??
|
||||
ReadInt(item, "weather", "value") ??
|
||||
ReadInt(item, "weather") ??
|
||||
ReadInt(item, "from");
|
||||
var code = ReadInt(item, "from") ?? ReadWeatherCode(item);
|
||||
var weatherText = ReadString(item, "weatherText") ??
|
||||
ReadString(item, "weather", "desc") ??
|
||||
ReadString(item, "weather", "text") ??
|
||||
@@ -903,6 +861,16 @@ public sealed class XiaomiWeatherService : IWeatherDataService, IDisposable
|
||||
return null;
|
||||
}
|
||||
|
||||
private static int? ReadWeatherCode(JsonElement? node)
|
||||
{
|
||||
return ReadInt(node, "weather", "value") ??
|
||||
ReadInt(node, "weatherCode") ??
|
||||
ReadInt(node, "code") ??
|
||||
ReadInt(node, "weather") ??
|
||||
ReadInt(node, "value") ??
|
||||
ReadInt(node);
|
||||
}
|
||||
|
||||
private static double? ReadDouble(JsonElement? node, params string[] path)
|
||||
{
|
||||
if (!node.HasValue)
|
||||
@@ -995,19 +963,7 @@ public sealed class XiaomiWeatherService : IWeatherDataService, IDisposable
|
||||
|
||||
private static string? ResolveWeatherDescription(int? code, string locale)
|
||||
{
|
||||
if (!code.HasValue)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var isZh = locale.StartsWith("zh", StringComparison.OrdinalIgnoreCase);
|
||||
var source = isZh ? ZhWeatherDescriptions : EnWeatherDescriptions;
|
||||
if (source.TryGetValue(code.Value, out var text))
|
||||
{
|
||||
return text;
|
||||
}
|
||||
|
||||
return isZh ? $"\u5929\u6c14\u7801 {code.Value}" : $"Weather {code.Value}";
|
||||
return XiaomiWeatherCodeMapper.ResolveDisplayText(code, locale);
|
||||
}
|
||||
|
||||
private static string Truncate(string? text, int maxLength)
|
||||
|
||||
Reference in New Issue
Block a user