settings_re11

This commit is contained in:
lincube
2026-03-15 17:08:07 +08:00
parent c7fb48c8ee
commit f83c6ede1d
49 changed files with 3243 additions and 815 deletions

View File

@@ -43,6 +43,7 @@ public partial class App : Application
}
private readonly ISettingsFacadeService _settingsFacade = HostSettingsFacadeProvider.GetOrCreate();
private readonly IAppearanceThemeService _appearanceThemeService = HostAppearanceThemeProvider.GetOrCreate();
private readonly LocalizationService _localizationService = new();
private readonly IHostApplicationLifecycle _hostApplicationLifecycle = new HostApplicationLifecycleService();
private readonly IDetachedComponentLibraryWindowService _detachedComponentLibraryWindowService = new DetachedComponentLibraryWindowService();
@@ -84,6 +85,7 @@ public partial class App : Application
public App()
{
_settingsFacade.Settings.Changed += OnSettingsChanged;
_appearanceThemeService.Changed += OnAppearanceThemeChanged;
}
public override void Initialize()
@@ -104,7 +106,6 @@ public partial class App : Application
RegisterUiUnhandledExceptionGuard();
LinuxDesktopEntryInstaller.EnsureInstalled();
InitializePluginRuntime();
AppSettingsService.SettingsSaved += OnAppSettingsSaved;
InitializeTrayIcon();
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
@@ -337,11 +338,11 @@ public partial class App : Application
private void ApplyThemeFromSettings()
{
var themeState = _settingsFacade.Theme.Get();
RequestedThemeVariant = themeState.IsNightMode
var snapshot = _appearanceThemeService.GetCurrent();
RequestedThemeVariant = snapshot.IsNightMode
? ThemeVariant.Dark
: ThemeVariant.Light;
ApplyAdaptiveThemeResources(themeState);
ApplyAdaptiveThemeResources();
}
private void ApplyCurrentCultureFromSettings()
@@ -464,19 +465,6 @@ public partial class App : Application
Dispatcher.UIThread.InvokeAsync(Reset, DispatcherPriority.Send).GetAwaiter().GetResult();
}
private void OnAppSettingsSaved(string _)
{
Dispatcher.UIThread.Post(() =>
{
ApplyThemeFromSettings();
ApplyCurrentCultureFromSettings();
if (_trayIcons is not null)
{
InitializeTrayIcon();
}
}, DispatcherPriority.Background);
}
private void OnSettingsChanged(object? sender, SettingsChangedEvent e)
{
_ = sender;
@@ -490,13 +478,17 @@ public partial class App : Application
{
var changedKeys = e.ChangedKeys?.ToArray();
var refreshAll = changedKeys is null || changedKeys.Length == 0;
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);
changedKeys.Contains(nameof(AppSettingsSnapshot.UseSystemChrome), 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)));
var languageChanged =
refreshAll ||
changedKeys.Contains(nameof(AppSettingsSnapshot.LanguageCode), StringComparer.OrdinalIgnoreCase);
@@ -517,48 +509,17 @@ public partial class App : Application
}, DispatcherPriority.Background);
}
private void ApplyAdaptiveThemeResources(ThemeAppearanceSettingsState themeState)
private void OnAppearanceThemeChanged(object? sender, AppearanceThemeSnapshot e)
{
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(Resources, context);
GlassEffectService.ApplyGlassResources(Resources, context);
_ = sender;
_ = e;
Dispatcher.UIThread.Post(ApplyThemeFromSettings, DispatcherPriority.Background);
}
private static Color ResolveAccentColor(string? colorText, MonetPalette monetPalette)
private void ApplyAdaptiveThemeResources()
{
if (monetPalette.MonetColors is { Count: > 0 })
{
return monetPalette.MonetColors[0];
}
return TryParseThemeColor(colorText);
}
private static Color TryParseThemeColor(string? colorText)
{
if (!string.IsNullOrWhiteSpace(colorText))
{
try
{
return Color.Parse(colorText);
}
catch
{
}
}
return DefaultAccentColor;
_appearanceThemeService.ApplyThemeResources(Resources);
}
private void RegisterUiUnhandledExceptionGuard()
@@ -606,8 +567,8 @@ public partial class App : Application
}
_exitCleanupCompleted = true;
AppSettingsService.SettingsSaved -= OnAppSettingsSaved;
_settingsFacade.Settings.Changed -= OnSettingsChanged;
_appearanceThemeService.Changed -= OnAppearanceThemeChanged;
try
{
HostUpdateWorkflowServiceProvider.GetOrCreate().TryApplyPendingUpdateOnExit();

View File

@@ -9,5 +9,6 @@ public sealed record DesktopComponentRuntimeContext(
string? PlacementId,
ISettingsFacadeService SettingsFacade,
ISettingsService SettingsService,
IAppearanceThemeService AppearanceTheme,
IComponentSettingsAccessor ComponentSettingsAccessor,
IComponentInstanceSettingsStore ComponentSettingsStore);

View File

@@ -9,6 +9,7 @@ public sealed record DesktopComponentSettingsContext(
string? PlacementId,
ISettingsFacadeService SettingsFacade,
ISettingsService SettingsService,
IAppearanceThemeService AppearanceTheme,
IComponentSettingsAccessor ComponentSettingsAccessor,
IComponentInstanceSettingsStore ComponentSettingsStore);

View File

@@ -54,6 +54,7 @@
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.0" />
<PackageReference Include="LibVLCSharp.Avalonia" Version="3.9.5" />
<PackageReference Include="PortAudioSharp2" Version="1.0.6" />
<PackageReference Include="MaterialColorUtilities" Version="0.3.0" />
<PackageReference Include="System.Runtime.WindowsRuntime" Version="4.7.0" />
<PackageReference Include="System.Drawing.Common" Version="10.0.0" />
<PackageReference Include="VideoLAN.LibVLC.Windows" Version="3.0.23" Condition="('$(RuntimeIdentifier)' == '' and $([MSBuild]::IsOSPlatform('Windows')))&#xA; or '$(RuntimeIdentifier)' == 'win-x64'&#xA; or '$(RuntimeIdentifier)' == 'win-x86'" />

View File

@@ -26,6 +26,7 @@
"settings.nav.weather": "Weather",
"settings.nav.region": "Region",
"settings.nav.update": "Update",
"settings.nav.privacy": "Privacy",
"settings.nav.launcher": "App Launcher",
"settings.nav.plugins": "Plugins",
"settings.nav.about": "About",
@@ -92,6 +93,12 @@
"settings.status_bar.spacing_mode_custom": "Custom",
"settings.status_bar.spacing_custom_label": "Custom spacing (%)",
"settings.status_bar.spacing_custom_px_format": "≈ {0:F1}px",
"settings.privacy.title": "Privacy",
"settings.privacy.description": "Manage optional anonymous uploads that help improve the app over time.",
"settings.privacy.crash_upload_title": "Anonymous crash data uploads",
"settings.privacy.crash_upload_description": "Help us improve application stability.",
"settings.privacy.usage_upload_title": "Anonymous usage data uploads",
"settings.privacy.usage_upload_description": "Help us improve application features.",
"settings.weather.title": "Weather",
"settings.weather.description": "Configure weather location, Xiaomi weather preview, and startup positioning behavior.",
"settings.weather.location_source_header": "Location Source",
@@ -242,11 +249,38 @@
"settings.general.preview_date_label": "Date",
"settings.general.render_mode_restart_message": "Rendering mode changes require restarting the app.",
"settings.appearance.title": "Appearance",
"settings.appearance.description": "Adjust theme, wallpaper, and window chrome.",
"settings.appearance.description": "Adjust theme source, system material, and window chrome.",
"settings.appearance.theme_header": "Theme",
"settings.color.enable_night_mode_toggle": "Enable night mode",
"settings.color.use_system_chrome_toggle": "Use system window chrome",
"settings.color.theme_color_label": "Theme accent color",
"settings.appearance.theme_color_mode_label": "Theme color source",
"settings.appearance.system_material_label": "System material",
"settings.appearance.theme_color_mode.neutral": "Default neutral",
"settings.appearance.theme_color_mode.user": "User theme color Monet",
"settings.appearance.theme_color_mode.wallpaper": "Wallpaper Monet",
"settings.appearance.theme_color_mode_desc.neutral": "Use the default white and black neutral surfaces for light and dark mode.",
"settings.appearance.theme_color_mode_desc.user": "Use the selected theme color as the Monet seed for the whole shell.",
"settings.appearance.theme_color_mode_desc.wallpaper": "Use wallpaper colors. The app wallpaper is preferred, then the system wallpaper.",
"settings.appearance.theme_color_preview.app": "Currently previewing colors extracted from the app wallpaper.",
"settings.appearance.theme_color_preview.system": "Currently previewing colors extracted from the system wallpaper.",
"settings.appearance.theme_color_preview.fallback": "No usable wallpaper was found. The app is using a fallback accent.",
"settings.appearance.system_material.none": "None",
"settings.appearance.system_material.mica": "Mica",
"settings.appearance.system_material.acrylic": "Acrylic",
"settings.appearance.system_material_desc.switchable": "Apply the selected material to windows, Dock, status bar, and component hosts.",
"settings.appearance.system_material_desc.fixed": "Your current system only exposes the material modes listed here.",
"settings.appearance.restart_message": "Theme source and system material changes require restarting the app.",
"settings.appearance.preview.primary": "Primary",
"settings.appearance.preview.secondary": "Secondary",
"settings.appearance.preview.tertiary": "Tertiary",
"settings.appearance.preview.neutral": "Neutral",
"settings.appearance.preview.seed": "Seed",
"settings.appearance.preview.neutral_light": "White",
"settings.appearance.preview.neutral_dark": "Black",
"settings.appearance.preview.apply_seed": "Apply",
"settings.appearance.preview.wallpaper_candidates": "Wallpaper seed candidates",
"settings.appearance.preview.wallpaper_current": "Current",
"settings.wallpaper.placement.fill": "Fill",
"settings.wallpaper.placement.fit": "Fit",
"settings.wallpaper.placement.stretch": "Stretch",
@@ -345,6 +379,7 @@
"settings.restart_dialog.title": "Restart required",
"settings.restart_dialog.render_mode_message": "Restart the app to switch the rendering mode from \"{0}\" to \"{1}\". Restart now?",
"settings.restart_dialog.restart": "Restart now",
"settings.restart_dialog.later": "Later",
"settings.restart_dialog.cancel": "Cancel",
"settings.restart_dock.title": "Restart required",
"settings.restart_dock.description": "Some changes will take effect after restarting the app.",

View File

@@ -26,6 +26,7 @@
"settings.nav.weather": "天气",
"settings.nav.region": "地区",
"settings.nav.update": "更新",
"settings.nav.privacy": "隐私",
"settings.nav.launcher": "应用启动台",
"settings.nav.plugins": "插件",
"settings.nav.about": "关于",
@@ -97,6 +98,12 @@
"settings.status_bar.spacing_mode_custom": "自定义",
"settings.status_bar.spacing_custom_label": "自定义间距(%",
"settings.status_bar.spacing_custom_px_format": "≈ {0:F1}px",
"settings.privacy.title": "隐私",
"settings.privacy.description": "管理可选的匿名上传设置,帮助我们逐步改进应用体验。",
"settings.privacy.crash_upload_title": "匿名上传崩溃数据",
"settings.privacy.crash_upload_description": "帮助我们提高应用稳定性。",
"settings.privacy.usage_upload_title": "匿名上传使用数据",
"settings.privacy.usage_upload_description": "帮助我们改善应用功能。",
"settings.weather.title": "天气",
"settings.weather.description": "配置天气位置、小米天气预览和启动时的位置刷新行为。",
"settings.weather.location_source_header": "位置来源",
@@ -247,11 +254,38 @@
"settings.general.preview_date_label": "日期",
"settings.general.render_mode_restart_message": "渲染模式变更需要重启应用。",
"settings.appearance.title": "外观",
"settings.appearance.description": "调整主题、壁纸与窗口外观。",
"settings.appearance.description": "调整主题来源、系统材质与窗口外观。",
"settings.appearance.theme_header": "主题",
"settings.color.enable_night_mode_toggle": "启用夜间模式",
"settings.color.use_system_chrome_toggle": "使用系统窗口标题栏",
"settings.color.theme_color_label": "主题强调色",
"settings.appearance.theme_color_mode_label": "主题色来源",
"settings.appearance.system_material_label": "系统材质",
"settings.appearance.theme_color_mode.neutral": "默认中性",
"settings.appearance.theme_color_mode.user": "用户主题色 Monet",
"settings.appearance.theme_color_mode.wallpaper": "壁纸 Monet 取色",
"settings.appearance.theme_color_mode_desc.neutral": "使用标准的日间白底黑字与夜间黑底白字中性色表面。",
"settings.appearance.theme_color_mode_desc.user": "使用用户选择的主题色作为整个桌面壳层的 Monet 种子色。",
"settings.appearance.theme_color_mode_desc.wallpaper": "使用壁纸颜色。优先取应用壁纸,失败后回退系统桌面壁纸。",
"settings.appearance.theme_color_preview.app": "当前正在预览从应用壁纸提取的颜色。",
"settings.appearance.theme_color_preview.system": "当前正在预览从系统壁纸提取的颜色。",
"settings.appearance.theme_color_preview.fallback": "没有可用壁纸,当前使用回退强调色。",
"settings.appearance.system_material.none": "无",
"settings.appearance.system_material.mica": "Mica",
"settings.appearance.system_material.acrylic": "Acrylic",
"settings.appearance.system_material_desc.switchable": "将所选材质应用到窗口、Dock、状态栏和组件宿主背板。",
"settings.appearance.system_material_desc.fixed": "当前系统仅提供这里列出的材质模式。",
"settings.appearance.restart_message": "主题色来源和系统材质更改需要重启应用。",
"settings.appearance.preview.primary": "主色",
"settings.appearance.preview.secondary": "次色",
"settings.appearance.preview.tertiary": "三次色",
"settings.appearance.preview.neutral": "中性色",
"settings.appearance.preview.seed": "种子色",
"settings.appearance.preview.neutral_light": "白色",
"settings.appearance.preview.neutral_dark": "黑色",
"settings.appearance.preview.apply_seed": "应用",
"settings.appearance.preview.wallpaper_candidates": "壁纸候选主题色",
"settings.appearance.preview.wallpaper_current": "当前",
"settings.wallpaper.placement.fill": "填充",
"settings.wallpaper.placement.fit": "适应",
"settings.wallpaper.placement.stretch": "拉伸",
@@ -350,6 +384,7 @@
"settings.restart_dialog.title": "需要重启应用",
"settings.restart_dialog.render_mode_message": "需要重启应用,才能将渲染模式从“{0}”切换到“{1}”。是否现在重启?",
"settings.restart_dialog.restart": "立即重启",
"settings.restart_dialog.later": "稍后",
"settings.restart_dialog.cancel": "取消",
"settings.restart_dock.title": "需要重启应用",
"settings.restart_dock.description": "部分更改需要在重启应用后才会生效。",

View File

@@ -16,6 +16,12 @@ public sealed class AppSettingsSnapshot
public bool UseSystemChrome { get; set; }
public string ThemeColorMode { get; set; } = "default_neutral";
public string SystemMaterialMode { get; set; } = "none";
public string? SelectedWallpaperSeed { get; set; }
public string? WallpaperPath { get; set; }
public string WallpaperType { get; set; } = "Image";
@@ -60,6 +66,10 @@ public sealed class AppSettingsSnapshot
public bool IncludePrereleaseUpdates { get; set; }
public bool UploadAnonymousCrashData { get; set; }
public bool UploadAnonymousUsageData { get; set; }
public string UpdateChannel { get; set; } = "stable";
public string UpdateMode { get; set; } = "download_then_confirm";

View File

@@ -1,8 +1,49 @@
using System.Collections.Generic;
using System.Collections.Generic;
using Avalonia.Media;
namespace LanMountainDesktop.Models;
public sealed record MonetPalette(
IReadOnlyList<Color> RecommendedColors,
IReadOnlyList<Color> MonetColors);
public sealed record MonetPalette
{
public MonetPalette(
IReadOnlyList<Color> recommendedColors,
Color seed,
Color primary,
Color secondary,
Color tertiary,
Color neutral,
Color neutralVariant)
{
RecommendedColors = recommendedColors;
Seed = seed;
Primary = primary;
Secondary = secondary;
Tertiary = tertiary;
Neutral = neutral;
NeutralVariant = neutralVariant;
MonetColors =
[
primary,
secondary,
tertiary,
neutral,
neutralVariant
];
}
public IReadOnlyList<Color> RecommendedColors { get; }
public IReadOnlyList<Color> MonetColors { get; }
public Color Seed { get; }
public Color Primary { get; }
public Color Secondary { get; }
public Color Tertiary { get; }
public Color Neutral { get; }
public Color NeutralVariant { get; }
}

File diff suppressed because it is too large Load Diff

View File

@@ -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");

View File

@@ -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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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;
}
}

View File

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

View 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}";
}
}

View File

@@ -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)

View File

@@ -184,8 +184,8 @@
</Style>
<Style Selector="Border.glass-island">
<Setter Property="Background" Value="{DynamicResource AdaptiveGlassStrongBackgroundBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource AdaptiveGlassStrongBorderBrush}" />
<Setter Property="Background" Value="{DynamicResource AdaptiveDockGlassBackgroundBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource AdaptiveDockGlassBorderBrush}" />
<Setter Property="BorderThickness" Value="1.5" />
<Setter Property="CornerRadius" Value="36" />
<Setter Property="Opacity" Value="{DynamicResource AdaptiveGlassStrongOpacity}" />

View File

@@ -1,5 +1,7 @@
using System.Collections.Generic;
using Avalonia.Media;
using LanMountainDesktop.Models;
using LanMountainDesktop.Services;
namespace LanMountainDesktop.Theme;
@@ -8,4 +10,7 @@ public sealed record ThemeColorContext(
bool IsLightBackground,
bool IsLightNavBackground,
bool IsNightMode,
IReadOnlyList<Color>? MonetColors = null);
MonetPalette? MonetPalette = null,
IReadOnlyList<Color>? MonetColors = null,
bool UseNeutralSurfaces = false,
string SystemMaterialMode = ThemeAppearanceValues.MaterialNone);

View File

@@ -0,0 +1,91 @@
using CommunityToolkit.Mvvm.ComponentModel;
using LanMountainDesktop.Services;
using LanMountainDesktop.Services.Settings;
namespace LanMountainDesktop.ViewModels;
public sealed partial class PrivacySettingsPageViewModel : ViewModelBase
{
private readonly ISettingsFacadeService _settingsFacade;
private readonly LocalizationService _localizationService = new();
private readonly string _languageCode;
private bool _isInitializing;
public PrivacySettingsPageViewModel(ISettingsFacadeService settingsFacade)
{
_settingsFacade = settingsFacade;
_languageCode = _localizationService.NormalizeLanguageCode(_settingsFacade.Region.Get().LanguageCode);
RefreshLocalizedText();
_isInitializing = true;
Load();
_isInitializing = false;
}
[ObservableProperty]
private bool _uploadAnonymousCrashData;
[ObservableProperty]
private bool _uploadAnonymousUsageData;
[ObservableProperty]
private string _privacyHeader = string.Empty;
[ObservableProperty]
private string _crashUploadHeader = string.Empty;
[ObservableProperty]
private string _crashUploadDescription = string.Empty;
[ObservableProperty]
private string _usageUploadHeader = string.Empty;
[ObservableProperty]
private string _usageUploadDescription = string.Empty;
public void Load()
{
var state = _settingsFacade.Privacy.Get();
UploadAnonymousCrashData = state.UploadAnonymousCrashData;
UploadAnonymousUsageData = state.UploadAnonymousUsageData;
}
partial void OnUploadAnonymousCrashDataChanged(bool value)
{
if (_isInitializing)
{
return;
}
Save();
}
partial void OnUploadAnonymousUsageDataChanged(bool value)
{
if (_isInitializing)
{
return;
}
Save();
}
private void Save()
{
_settingsFacade.Privacy.Save(new PrivacySettingsState(
UploadAnonymousCrashData,
UploadAnonymousUsageData));
}
private void RefreshLocalizedText()
{
PrivacyHeader = L("settings.privacy.title", "Privacy");
CrashUploadHeader = L("settings.privacy.crash_upload_title", "Anonymous crash data uploads");
CrashUploadDescription = L("settings.privacy.crash_upload_description", "Help us improve application stability.");
UsageUploadHeader = L("settings.privacy.usage_upload_title", "Anonymous usage data uploads");
UsageUploadDescription = L("settings.privacy.usage_upload_description", "Help us improve application features.");
}
private string L(string key, string fallback)
=> _localizationService.GetString(_languageCode, key, fallback);
}

View File

@@ -62,6 +62,15 @@ public sealed partial class SettingsWindowViewModel : ViewModelBase
[ObservableProperty]
private string _restartButtonText = string.Empty;
[ObservableProperty]
private string _restartDialogTitle = string.Empty;
[ObservableProperty]
private string _restartDialogPrimaryText = string.Empty;
[ObservableProperty]
private string _restartDialogCloseText = string.Empty;
[ObservableProperty]
private string? _drawerTitle;
@@ -84,6 +93,12 @@ public sealed partial class SettingsWindowViewModel : ViewModelBase
Title = L("settings.title");
RestartTitle = L("settings.restart_dock.title");
RestartButtonText = L("settings.restart_dock.button");
RestartDialogTitle = L("settings.restart_dialog.title");
RestartDialogPrimaryText = L("settings.restart_dialog.restart");
RestartDialogCloseText = _localizationService.GetString(
_languageCode,
"settings.restart_dialog.later",
L("settings.restart_dialog.cancel"));
DrawerFallbackTitle = L("settings.window.drawer_default");
var nextDefaultRestartMessage = L("settings.restart_dock.description");
@@ -113,6 +128,28 @@ public sealed class SelectionOption
public string Label { get; }
}
public sealed class ThemeSeedCandidateOption
{
public ThemeSeedCandidateOption(string value, string label, Color color, bool isSelected)
{
Value = value;
Label = label;
Color = color;
IsSelected = isSelected;
Brush = new SolidColorBrush(color);
}
public string Value { get; }
public string Label { get; }
public Color Color { get; }
public bool IsSelected { get; }
public IBrush Brush { get; }
}
public sealed class TimeZoneOption
{
public TimeZoneOption(string? id, string label)
@@ -384,27 +421,37 @@ public sealed partial class GeneralSettingsPageViewModel : ViewModelBase
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)
public AppearanceSettingsPageViewModel(
ISettingsFacadeService settingsFacade,
IAppearanceThemeService appearanceThemeService)
{
_settingsFacade = settingsFacade;
_appearanceThemeService = appearanceThemeService;
_languageCode = _localizationService.NormalizeLanguageCode(_settingsFacade.Region.Get().LanguageCode);
WallpaperPlacements = CreateWallpaperPlacements();
ClockFormats = CreateClockFormats();
RefreshLocalizedText();
ThemeColorModes = CreateThemeColorModes();
_isInitializing = true;
Load();
_isInitializing = false;
}
public IReadOnlyList<SelectionOption> WallpaperPlacements { get; }
public event Action<string>? RestartRequested;
public IReadOnlyList<SelectionOption> ClockFormats { get; }
public IReadOnlyList<SelectionOption> ThemeColorModes { get; }
[ObservableProperty]
private IReadOnlyList<SelectionOption> _systemMaterialModes = [];
[ObservableProperty]
private bool _isNightMode;
@@ -413,32 +460,66 @@ public sealed partial class AppearanceSettingsPageViewModel : ViewModelBase
private string _themeColor = string.Empty;
[ObservableProperty]
private Color _themeColorPickerValue;
private Color _customSeedPickerValue = DefaultSeedColor;
partial void OnThemeColorPickerValueChanged(Color value)
partial void OnCustomSeedPickerValueChanged(Color value)
{
if (_isInitializing)
if (_isInitializing ||
!string.Equals(SelectedThemeColorMode?.Value, ThemeAppearanceValues.ColorModeSeedMonet, StringComparison.OrdinalIgnoreCase))
{
return;
}
ThemeColor = value.ToString();
UpdatePreview(BuildPendingState(usePickerSeed: true));
}
[ObservableProperty]
private bool _useSystemChrome;
[ObservableProperty]
private string _wallpaperPath = string.Empty;
private SelectionOption _selectedThemeColorMode = new(ThemeAppearanceValues.ColorModeSeedMonet, "User theme color Monet");
[ObservableProperty]
private SelectionOption _selectedWallpaperPlacement = new("Fill", "Fill");
private SelectionOption _selectedSystemMaterialMode = new(ThemeAppearanceValues.MaterialNone, "None");
[ObservableProperty]
private bool _showClock = true;
private bool _isThemeColorEditable;
[ObservableProperty]
private SelectionOption _selectedClockFormat = new("HourMinuteSecond", "Hour:Minute:Second");
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;
@@ -455,75 +536,111 @@ public sealed partial class AppearanceSettingsPageViewModel : ViewModelBase
[ObservableProperty]
private string _themeColorLabel = string.Empty;
[ObservableProperty]
private string _themeColorModeLabel = string.Empty;
[ObservableProperty]
private string _systemMaterialLabel = string.Empty;
[ObservableProperty]
private string _themeHeader = string.Empty;
[ObservableProperty]
private string _wallpaperHeader = string.Empty;
private string _themeSourceNeutralText = string.Empty;
[ObservableProperty]
private string _wallpaperPathLabel = string.Empty;
private string _themeSourceUserColorText = string.Empty;
[ObservableProperty]
private string _wallpaperPlacementLabel = string.Empty;
private string _themeSourceWallpaperText = string.Empty;
[ObservableProperty]
private string _importWallpaperButtonText = string.Empty;
private string _themeSourceDefaultDescription = string.Empty;
[ObservableProperty]
private string _clockHeader = string.Empty;
private string _themeSourceUserColorDescription = string.Empty;
[ObservableProperty]
private string _clockDescription = string.Empty;
private string _themeSourceWallpaperDescription = string.Empty;
[ObservableProperty]
private string _clockFormatLabel = string.Empty;
private string _themeSourceWallpaperAppDescription = string.Empty;
[ObservableProperty]
private string _filePickerTitle = string.Empty;
private string _themeSourceWallpaperSystemDescription = string.Empty;
[ObservableProperty]
private string _themeSourceWallpaperFallbackDescription = string.Empty;
[ObservableProperty]
private string _systemMaterialNoneText = 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 _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;
public void Load()
{
var theme = _settingsFacade.Theme.Get();
IsNightMode = theme.IsNightMode;
ThemeColor = theme.ThemeColor ?? string.Empty;
if (Color.TryParse(ThemeColor, out var color))
{
ThemeColorPickerValue = color;
}
else
{
ThemeColorPickerValue = Color.Parse("#FF3B82F6");
}
UseSystemChrome = theme.UseSystemChrome;
var liveSnapshot = _appearanceThemeService.GetCurrent();
RefreshMaterialModeOptions(liveSnapshot);
var wallpaper = _settingsFacade.Wallpaper.Get();
WallpaperPath = wallpaper.WallpaperPath ?? string.Empty;
var wallpaperPlacement = string.IsNullOrWhiteSpace(wallpaper.Placement)
? "Fill"
: wallpaper.Placement;
SelectedWallpaperPlacement = WallpaperPlacements.FirstOrDefault(option =>
string.Equals(option.Value, wallpaperPlacement, StringComparison.OrdinalIgnoreCase))
?? WallpaperPlacements[0];
var statusBar = _settingsFacade.StatusBar.Get();
ShowClock = statusBar.TopStatusComponentIds.Any(id =>
string.Equals(id, BuiltInComponentIds.Clock, StringComparison.OrdinalIgnoreCase));
var clockFormat = string.IsNullOrWhiteSpace(statusBar.ClockDisplayFormat)
? "HourMinuteSecond"
: statusBar.ClockDisplayFormat;
SelectedClockFormat = ClockFormats.FirstOrDefault(option =>
string.Equals(option.Value, clockFormat, StringComparison.OrdinalIgnoreCase))
?? ClockFormats[1];
}
public async Task ImportWallpaperAsync(string sourcePath)
{
var importedPath = await _settingsFacade.WallpaperMedia.ImportAssetAsync(sourcePath);
if (!string.IsNullOrWhiteSpace(importedPath))
_isInitializing = true;
try
{
WallpaperPath = importedPath;
ApplySavedState(theme);
}
finally
{
_isInitializing = false;
}
UpdatePreview(theme);
}
partial void OnIsNightModeChanged(bool value)
@@ -533,20 +650,7 @@ public sealed partial class AppearanceSettingsPageViewModel : ViewModelBase
return;
}
SaveTheme();
}
partial void OnThemeColorChanged(string value)
{
if (_isInitializing)
{
return;
}
if (string.IsNullOrWhiteSpace(value) || Color.TryParse(value, out _))
{
SaveTheme();
}
PersistCurrentState(restartRequired: false);
}
partial void OnUseSystemChromeChanged(bool value)
@@ -556,126 +660,254 @@ public sealed partial class AppearanceSettingsPageViewModel : ViewModelBase
return;
}
SaveTheme();
PersistCurrentState(restartRequired: false);
}
partial void OnWallpaperPathChanged(string value)
{
if (_isInitializing)
{
return;
}
SaveWallpaper();
}
partial void OnSelectedWallpaperPlacementChanged(SelectionOption value)
partial void OnSelectedThemeColorModeChanged(SelectionOption value)
{
if (_isInitializing || value is null)
{
return;
}
SaveWallpaper();
PersistCurrentState(restartRequired: true);
}
partial void OnShowClockChanged(bool value)
{
if (_isInitializing)
{
return;
}
SaveStatusBar();
}
partial void OnSelectedClockFormatChanged(SelectionOption value)
partial void OnSelectedSystemMaterialModeChanged(SelectionOption value)
{
if (_isInitializing || value is null)
{
return;
}
SaveStatusBar();
PersistCurrentState(restartRequired: true);
}
private void SaveTheme()
[RelayCommand]
private void ApplyCustomSeed()
{
_settingsFacade.Theme.Save(new ThemeAppearanceSettingsState(
IsNightMode,
string.IsNullOrWhiteSpace(ThemeColor) ? null : ThemeColor,
UseSystemChrome));
}
private void SaveWallpaper()
{
var current = _settingsFacade.Wallpaper.Get();
_settingsFacade.Wallpaper.Save(new WallpaperSettingsState(
string.IsNullOrWhiteSpace(WallpaperPath) ? null : WallpaperPath,
current.Type,
current.Color,
SelectedWallpaperPlacement.Value));
}
private void SaveStatusBar()
{
var state = _settingsFacade.StatusBar.Get();
var topComponents = state.TopStatusComponentIds
.Where(id => !string.Equals(id, BuiltInComponentIds.Clock, StringComparison.OrdinalIgnoreCase))
.ToList();
if (ShowClock)
if (!IsThemeColorEditable)
{
topComponents.Add(BuiltInComponentIds.Clock);
return;
}
_settingsFacade.StatusBar.Save(new StatusBarSettingsState(
topComponents,
state.PinnedTaskbarActions,
state.EnableDynamicTaskbarActions,
state.TaskbarLayoutMode,
SelectedClockFormat.Value,
state.SpacingMode,
state.CustomSpacingPercent));
ThemeColor = CustomSeedPickerValue.ToString();
PersistCurrentState(restartRequired: false);
}
private IReadOnlyList<SelectionOption> CreateWallpaperPlacements()
public void CancelCustomSeedPreview()
{
return
[
new SelectionOption("Fill", L("settings.wallpaper.placement.fill", "Fill")),
new SelectionOption("Fit", L("settings.wallpaper.placement.fit", "Fit")),
new SelectionOption("Stretch", L("settings.wallpaper.placement.stretch", "Stretch")),
new SelectionOption("Center", L("settings.wallpaper.placement.center", "Center")),
new SelectionOption("Tile", L("settings.wallpaper.placement.tile", "Tile"))
];
if (_isInitializing)
{
return;
}
SyncCustomSeedPickerWithSavedThemeColor();
UpdatePreview(BuildPendingState(usePickerSeed: false));
}
private IReadOnlyList<SelectionOption> CreateClockFormats()
public void SelectWallpaperSeed(string value)
{
return
[
new SelectionOption("HourMinute", L("settings.status_bar.clock_format.hm", "Hour:Minute")),
new SelectionOption("HourMinuteSecond", L("settings.status_bar.clock_format.hms", "Hour:Minute:Second"))
];
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", "Theme and status bar presentation.");
PageDescription = L("settings.appearance.description", "Adjust theme source, material background, and window chrome.");
ThemeHeader = L("settings.appearance.theme_header", "Theme");
NightModeLabel = L("settings.color.enable_night_mode_toggle", "Enable night mode");
UseSystemChromeLabel = L("settings.color.use_system_chrome_toggle", "Use system window chrome");
ThemeColorLabel = L("settings.color.theme_color_label", "Theme Accent Color");
WallpaperHeader = L("settings.wallpaper.title", "Wallpaper");
WallpaperPathLabel = L("settings.wallpaper.current_label", "Current Wallpaper");
WallpaperPlacementLabel = L("settings.wallpaper.placement_label", "Placement");
ImportWallpaperButtonText = L("settings.wallpaper.pick_button", "Import Wallpaper");
ClockHeader = L("settings.status_bar.clock_header", "Clock Component");
ClockDescription = L("settings.status_bar.clock_description", "Display a clock on the top status bar.");
ClockFormatLabel = L("settings.status_bar.clock_format_label", "Clock Format");
FilePickerTitle = L("filepicker.title", "Select wallpaper");
ThemeColorModeLabel = L("settings.appearance.theme_color_mode_label", "Theme color source");
SystemMaterialLabel = L("settings.appearance.system_material_label", "System material");
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");
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.");
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
? SystemMaterialSwitchableDescription
: SystemMaterialFixedDescription;
}
private void ApplySavedState(ThemeAppearanceSettingsState theme)
{
IsNightMode = theme.IsNightMode;
ThemeColor = theme.ThemeColor ?? string.Empty;
UseSystemChrome = theme.UseSystemChrome;
_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];
}
private void PersistCurrentState(bool restartRequired)
{
var pendingState = BuildPendingState(usePickerSeed: false);
_settingsFacade.Theme.Save(pendingState);
var savedState = _settingsFacade.Theme.Get();
_isInitializing = true;
try
{
ApplySavedState(savedState);
}
finally
{
_isInitializing = false;
}
RefreshMaterialModeOptions(_appearanceThemeService.GetCurrent());
UpdatePreview(savedState);
if (restartRequired)
{
RestartRequested?.Invoke(AppearanceRestartMessage);
}
}
private ThemeAppearanceSettingsState BuildPendingState(bool usePickerSeed)
{
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,
themeColorMode,
ThemeAppearanceValues.NormalizeSystemMaterialMode(SelectedSystemMaterialMode?.Value),
_selectedWallpaperSeed);
}
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)
{
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.MaterialMica => SystemMaterialMicaText,
ThemeAppearanceValues.MaterialAcrylic => SystemMaterialAcrylicText,
_ => SystemMaterialNoneText
};
}
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)

View File

@@ -169,8 +169,11 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
private void SaveWallpaper()
{
var normalizedPath = SelectedWallpaperType?.Value == "SolidColor" || string.IsNullOrWhiteSpace(WallpaperPath)
? null
: WallpaperPath;
_settingsFacade.Wallpaper.Save(new WallpaperSettingsState(
string.IsNullOrWhiteSpace(WallpaperPath) ? null : WallpaperPath,
normalizedPath,
SelectedWallpaperType.Value,
SelectedColor,
SelectedWallpaperPlacement.Value));

View File

@@ -41,19 +41,8 @@ public sealed partial class WeatherSettingsPageViewModel : ViewModelBase
LocationModes = CreateLocationModes();
var weatherState = _settingsFacade.Weather.Get();
SearchKeyword = weatherState.LocationQuery;
SelectedLocationMode = LocationModes.FirstOrDefault(option =>
string.Equals(option.Value, weatherState.LocationMode, StringComparison.OrdinalIgnoreCase))
?? LocationModes[0];
_isInitializing = true;
Latitude = weatherState.Latitude;
Longitude = weatherState.Longitude;
LocationKey = weatherState.LocationKey;
LocationName = weatherState.LocationName;
AutoRefreshLocation = weatherState.AutoRefreshLocation;
ExcludedAlerts = weatherState.ExcludedAlerts;
NoTlsRequests = weatherState.NoTlsRequests;
ApplySavedState(weatherState, save: false);
_isInitializing = false;
IsLocationSupported = _locationService.IsSupported;
@@ -455,15 +444,19 @@ public sealed partial class WeatherSettingsPageViewModel : ViewModelBase
var isNight = snapshot.Current.IsDaylight.HasValue
? !snapshot.Current.IsDaylight.Value
: _settingsFacade.Theme.Get().IsNightMode;
var visualKind = HyperOS3WeatherTheme.ResolveVisualKind(snapshot.Current.WeatherCode, isNight);
PreviewIcon = HyperOS3WeatherAssetLoader.LoadImage(HyperOS3WeatherTheme.ResolveHeroIconAsset(visualKind));
var preview = XiaomiWeatherVisualResolver.Resolve(
snapshot.Current.WeatherText,
snapshot.Current.WeatherCode,
isNight,
_languageCode);
PreviewIcon = HyperOS3WeatherAssetLoader.LoadImage(preview.PrimaryIconAsset);
PreviewLocation = string.IsNullOrWhiteSpace(snapshot.LocationName)
? state.LocationName
: snapshot.LocationName!;
PreviewTemperature = snapshot.Current.TemperatureC.HasValue
? string.Format(CultureInfo.InvariantCulture, "{0:0.#}°C", snapshot.Current.TemperatureC.Value)
: "--";
PreviewCondition = snapshot.Current.WeatherText ?? L("settings.weather.preview_unknown", "Unknown");
PreviewCondition = preview.DisplayText;
var updatedAt = (snapshot.ObservationTime ?? snapshot.FetchedAt).ToLocalTime();
PreviewUpdated = string.Format(

View File

@@ -17,7 +17,7 @@
SizeToContent="Manual"
ShowInTaskbar="False"
SystemDecorations="BorderOnly"
Background="{DynamicResource EditorWindowBackgroundBrush}"
Background="Transparent"
Title="Component Editor">
<Window.Resources>
<!-- Material Design 3 Brushes -->

View File

@@ -96,6 +96,7 @@ public sealed class DesktopComponentRuntimeDescriptor
ArgumentNullException.ThrowIfNull(settingsFacade);
var settingsService = settingsFacade.Settings;
var appearanceTheme = HostAppearanceThemeProvider.GetOrCreate();
var componentAccessor = settingsService.GetComponentAccessor(Definition.Id, placementId);
var componentSettingsStore = new ComponentSettingsService(settingsService);
componentSettingsStore.SetScopedComponentContext(Definition.Id, placementId);
@@ -116,6 +117,7 @@ public sealed class DesktopComponentRuntimeDescriptor
placementId,
settingsFacade,
settingsService,
appearanceTheme,
componentAccessor,
componentSettingsStore);
@@ -133,6 +135,7 @@ public sealed class DesktopComponentRuntimeDescriptor
placementId,
settingsFacade,
settingsService,
appearanceTheme,
componentAccessor,
componentSettingsStore));
}

View File

@@ -84,7 +84,7 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
[
DailyIcon0, DailyIcon1, DailyIcon2, DailyIcon3, DailyIcon4
];
_dailyIconKinds = Enumerable.Repeat(HyperOS3WeatherVisualKind.CloudyDay, _dailyIconBlocks.Length).ToArray();
_dailyIconKinds = Enumerable.Repeat(HyperOS3WeatherVisualKind.Unknown, _dailyIconBlocks.Length).ToArray();
ConfigureTextOverflowGuards();
_refreshTimer.Tick += OnRefreshTimerTick;
_animationTimer.Tick += OnAnimationTick;
@@ -328,12 +328,17 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
snapshot,
_timeZoneService?.CurrentTimeZone,
_timeZoneService?.GetCurrentTime() ?? DateTime.Now);
var kind = HyperOS3WeatherTheme.ResolveVisualKind(snapshot.Current.WeatherCode, isNight);
var currentVisual = XiaomiWeatherVisualResolver.Resolve(
snapshot.Current.WeatherText,
snapshot.Current.WeatherCode,
isNight,
_languageCode);
var kind = currentVisual.VisualKind;
ApplyVisualTheme(kind);
SetLoadingSkeleton(false);
WeatherIconImage.Source = HyperOS3WeatherAssetLoader.LoadImage(HyperOS3WeatherTheme.ResolveHeroIconAsset(kind));
WeatherIconImage.Source = HyperOS3WeatherAssetLoader.LoadImage(currentVisual.PrimaryIconAsset);
CityTextBlock.Text = ResolveLocation(snapshot.LocationName, fallbackLocationName);
ConditionTextBlock.Text = ResolveWeatherText(snapshot.Current.WeatherText, kind);
ConditionTextBlock.Text = currentVisual.DisplayText;
TemperatureTextBlock.Text = FormatTemperature(snapshot.Current.TemperatureC);
var today = snapshot.DailyForecasts.FirstOrDefault();
@@ -354,12 +359,17 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
.OrderBy(entry => Math.Abs((entry.Time - target).TotalMinutes))
.FirstOrDefault();
var weatherCode = item?.Source.WeatherCode ?? snapshot.Current.WeatherCode;
var hourKind = HyperOS3WeatherTheme.ResolveVisualKind(weatherCode, IsNightHour(target));
var hourVisual = XiaomiWeatherVisualResolver.Resolve(
item?.Source.WeatherText,
weatherCode,
IsNightHour(target),
_languageCode);
var hourKind = hourVisual.VisualKind;
_hourlyTempBlocks[i].Text = i == sunsetSlotIndex
? L("weather.hourly.sunset", "Sunset")
: FormatTemperature(item?.Source.TemperatureC ?? snapshot.Current.TemperatureC);
_hourlyTimeBlocks[i].Text = target.ToString("HH:mm", CultureInfo.InvariantCulture);
_hourlyIconBlocks[i].Source = HyperOS3WeatherAssetLoader.LoadImage(HyperOS3WeatherTheme.ResolveMiniIconAsset(hourKind));
_hourlyIconBlocks[i].Source = HyperOS3WeatherAssetLoader.LoadImage(hourVisual.CompactIconAsset);
}
var todayDate = DateOnly.FromDateTime(now);
@@ -368,21 +378,26 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
var date = todayDate.AddDays(i + 1);
var daily = snapshot.DailyForecasts.FirstOrDefault(entry => entry.Date == date) ?? snapshot.DailyForecasts.FirstOrDefault();
var weatherCode = daily?.DayWeatherCode ?? daily?.NightWeatherCode ?? snapshot.Current.WeatherCode;
var dayKind = HyperOS3WeatherTheme.ResolveVisualKind(weatherCode, false);
var dayText = ResolveWeatherText(daily?.DayWeatherText ?? daily?.NightWeatherText, dayKind);
var dayVisual = XiaomiWeatherVisualResolver.Resolve(
daily?.DayWeatherText ?? daily?.NightWeatherText,
weatherCode,
false,
_languageCode);
var dayKind = dayVisual.VisualKind;
var dayText = dayVisual.DisplayText;
_dailyLabelBlocks[i].Text = $"{ResolveDayLabel(date, i + 1)}·{dayText}";
_dailyHighBlocks[i].Text = FormatTemperatureValue(daily?.HighTemperatureC);
_dailyLowBlocks[i].Text = FormatTemperatureValue(daily?.LowTemperatureC);
_dailyIconKinds[i] = dayKind;
_dailyIconBlocks[i].Source = HyperOS3WeatherAssetLoader.LoadImage(HyperOS3WeatherTheme.ResolveMiniIconAsset(dayKind));
_dailyIconBlocks[i].Source = HyperOS3WeatherAssetLoader.LoadImage(dayVisual.CompactIconAsset);
}
}
private void ApplyFallback()
{
ApplyVisualTheme(HyperOS3WeatherVisualKind.CloudyDay);
ApplyVisualTheme(HyperOS3WeatherVisualKind.Unknown);
SetLoadingSkeleton(false);
WeatherIconImage.Source = HyperOS3WeatherAssetLoader.LoadImage(HyperOS3WeatherTheme.ResolveHeroIconAsset(HyperOS3WeatherVisualKind.CloudyDay));
WeatherIconImage.Source = HyperOS3WeatherAssetLoader.LoadImage(HyperOS3WeatherTheme.ResolveHeroIconAsset(HyperOS3WeatherVisualKind.Unknown));
CityTextBlock.Text = L("weather.widget.location_unknown", "Unknown location");
ConditionTextBlock.Text = L("weather.widget.loading", "Loading...");
TemperatureTextBlock.Text = "--°";
@@ -393,7 +408,7 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
{
_hourlyTempBlocks[i].Text = i == 3 ? L("weather.hourly.sunset", "Sunset") : "--°";
_hourlyTimeBlocks[i].Text = timelineStart.AddHours(i).ToString("HH:mm", CultureInfo.InvariantCulture);
_hourlyIconBlocks[i].Source = HyperOS3WeatherAssetLoader.LoadImage(HyperOS3WeatherTheme.ResolveMiniIconAsset(HyperOS3WeatherVisualKind.CloudyDay));
_hourlyIconBlocks[i].Source = HyperOS3WeatherAssetLoader.LoadImage(HyperOS3WeatherTheme.ResolveMiniIconAsset(HyperOS3WeatherVisualKind.Unknown));
}
for (var i = 0; i < _dailyLabelBlocks.Length; i++)
@@ -401,8 +416,8 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
_dailyLabelBlocks[i].Text = $"{ResolveDayLabel(DateOnly.FromDateTime(DateTime.Now).AddDays(i + 1), i + 1)}·{L("weather.widget.condition_cloudy", "Cloudy")}";
_dailyHighBlocks[i].Text = "--";
_dailyLowBlocks[i].Text = "--";
_dailyIconKinds[i] = HyperOS3WeatherVisualKind.CloudyDay;
_dailyIconBlocks[i].Source = HyperOS3WeatherAssetLoader.LoadImage(HyperOS3WeatherTheme.ResolveMiniIconAsset(HyperOS3WeatherVisualKind.CloudyDay));
_dailyIconKinds[i] = HyperOS3WeatherVisualKind.Unknown;
_dailyIconBlocks[i].Source = HyperOS3WeatherAssetLoader.LoadImage(HyperOS3WeatherTheme.ResolveMiniIconAsset(HyperOS3WeatherVisualKind.Unknown));
}
}
@@ -417,7 +432,7 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
BackgroundMotionLayer.Background = background ?? CreateGradientBrush(palette.GradientFrom, palette.GradientTo);
BackgroundTintLayer.Background = CreateSolidBrush(palette.Tint);
var isNightVisual = kind is HyperOS3WeatherVisualKind.ClearNight or HyperOS3WeatherVisualKind.CloudyNight;
var isNightVisual = kind is HyperOS3WeatherVisualKind.ClearNight or HyperOS3WeatherVisualKind.PartlyCloudyNight or HyperOS3WeatherVisualKind.CloudyNight;
var backgroundSamples = WeatherTypographyAccessibility.BuildBackgroundSamples(
palette.GradientFrom,
palette.GradientTo,
@@ -612,7 +627,7 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
var dailyKind = i < _dailyIconKinds.Length
? _dailyIconKinds[i]
: HyperOS3WeatherVisualKind.CloudyDay;
: HyperOS3WeatherVisualKind.Unknown;
var dailyIconVisualSize = Math.Clamp(
dailyIconSize * ResolveDailyMiniIconScaleBoost(dailyKind),
8,
@@ -851,18 +866,10 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
return score;
}
private string ResolveWeatherText(string? weatherText, HyperOS3WeatherVisualKind kind)
private string ResolveWeatherText(string? weatherText, int? weatherCode, HyperOS3WeatherVisualKind kind)
{
if (!string.IsNullOrWhiteSpace(weatherText)) return weatherText;
return kind switch
{
HyperOS3WeatherVisualKind.ClearDay or HyperOS3WeatherVisualKind.ClearNight => L("weather.widget.condition_clear", "Clear"),
HyperOS3WeatherVisualKind.CloudyDay or HyperOS3WeatherVisualKind.CloudyNight => L("weather.widget.condition_cloudy", "Cloudy"),
HyperOS3WeatherVisualKind.RainLight or HyperOS3WeatherVisualKind.RainHeavy => L("weather.widget.condition_rain", "Rain"),
HyperOS3WeatherVisualKind.Storm => L("weather.widget.condition_storm", "Thunderstorm"),
HyperOS3WeatherVisualKind.Snow => L("weather.widget.condition_snow", "Snow"),
_ => L("weather.widget.condition_fog", "Fog")
};
_ = kind;
return XiaomiWeatherVisualResolver.ResolveDisplayText(weatherText, weatherCode, _languageCode);
}
private DateTime ConvertToConfiguredTime(DateTimeOffset sourceTime)
@@ -999,7 +1006,8 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
kind switch
{
HyperOS3WeatherVisualKind.RainLight or HyperOS3WeatherVisualKind.RainHeavy or HyperOS3WeatherVisualKind.Storm or HyperOS3WeatherVisualKind.Snow => 1.16,
HyperOS3WeatherVisualKind.ClearNight or HyperOS3WeatherVisualKind.CloudyNight => 1.08,
HyperOS3WeatherVisualKind.ClearNight or HyperOS3WeatherVisualKind.PartlyCloudyNight or HyperOS3WeatherVisualKind.CloudyNight => 1.08,
HyperOS3WeatherVisualKind.Haze or HyperOS3WeatherVisualKind.Fog => 1.04,
_ => 1.0
};
@@ -1008,9 +1016,13 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
{
HyperOS3WeatherVisualKind.CloudyDay => 1.30,
HyperOS3WeatherVisualKind.CloudyNight => 1.28,
HyperOS3WeatherVisualKind.PartlyCloudyDay => 1.28,
HyperOS3WeatherVisualKind.PartlyCloudyNight => 1.26,
HyperOS3WeatherVisualKind.ClearDay => 1.26,
HyperOS3WeatherVisualKind.ClearNight => 1.24,
HyperOS3WeatherVisualKind.Haze => 1.20,
HyperOS3WeatherVisualKind.Fog => 1.18,
HyperOS3WeatherVisualKind.Sleet => 1.14,
HyperOS3WeatherVisualKind.RainLight => 1.14,
HyperOS3WeatherVisualKind.RainHeavy => 1.12,
HyperOS3WeatherVisualKind.Snow => 1.12,

View File

@@ -22,10 +22,15 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
{
private enum WeatherVisualKind
{
Unknown,
ClearDay,
ClearNight,
PartlyCloudyDay,
PartlyCloudyNight,
CloudyDay,
CloudyNight,
Haze,
Sleet,
RainLight,
RainHeavy,
Storm,
@@ -482,7 +487,12 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
private void ApplySnapshot(WeatherSnapshot snapshot, string fallbackLocationName)
{
var isNight = ResolveIsNight(snapshot);
var visualKind = ResolveVisualKind(snapshot.Current.WeatherCode, isNight);
var visual = XiaomiWeatherVisualResolver.Resolve(
snapshot.Current.WeatherText,
snapshot.Current.WeatherCode,
isNight,
_languageCode);
var visualKind = ResolveVisualKind(visual.VisualKind);
ApplyVisualTheme(visualKind);
var rawLocation = string.IsNullOrWhiteSpace(snapshot.LocationName)
@@ -490,8 +500,8 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
: snapshot.LocationName;
CityTextBlock.Text = ResolvePreciseDisplayLocation(rawLocation, _languageCode, L("weather.widget.location_unknown", "Unknown location"));
ConditionTextBlock.Text = ResolveWeatherConditionText(snapshot.Current.WeatherText, visualKind);
SetMainWeatherIcon(visualKind);
ConditionTextBlock.Text = visual.DisplayText;
SetMainWeatherIcon(visual.PrimaryIconAsset, visualKind);
SetLoadingSkeleton(false);
TemperatureTextBlock.Text = FormatTemperature(snapshot.Current.TemperatureC);
@@ -505,7 +515,7 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
{
var fallbackKind = ResolveFallbackVisualKind();
ApplyVisualTheme(fallbackKind);
SetMainWeatherIcon(fallbackKind);
SetMainWeatherIcon(null, fallbackKind);
SetLoadingSkeleton(false);
CityTextBlock.Text = L("weather.widget.location_not_configured", "Weather location is not configured");
ConditionTextBlock.Text = L("weather.widget.configure_hint", "Open Settings > Weather to configure");
@@ -520,7 +530,7 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
{
var loadingKind = IsNightNow() ? WeatherVisualKind.CloudyNight : WeatherVisualKind.CloudyDay;
ApplyVisualTheme(loadingKind);
SetMainWeatherIcon(loadingKind);
SetMainWeatherIcon(null, loadingKind);
SetLoadingSkeleton(true);
CityTextBlock.Text = ResolvePreciseDisplayLocation(
locationName,
@@ -535,8 +545,8 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
private void ApplyFailedState(string locationName)
{
ApplyVisualTheme(WeatherVisualKind.Fog);
SetMainWeatherIcon(WeatherVisualKind.Fog);
ApplyVisualTheme(WeatherVisualKind.Unknown);
SetMainWeatherIcon(HyperOS3WeatherTheme.ResolveHeroIconAsset(HyperOS3WeatherVisualKind.Unknown), WeatherVisualKind.Unknown);
SetLoadingSkeleton(false);
CityTextBlock.Text = ResolvePreciseDisplayLocation(
locationName,
@@ -545,7 +555,7 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
ConditionTextBlock.Text = L("weather.widget.fetch_failed", "Weather fetch failed");
TemperatureTextBlock.Text = "--°";
RangeTextBlock.Text = L("weather.widget.range_unknown", "-- / --");
ApplyHourlyForecastItems(BuildPlaceholderHourlyForecastItems(WeatherVisualKind.Fog));
ApplyHourlyForecastItems(BuildPlaceholderHourlyForecastItems(WeatherVisualKind.Unknown));
ApplyAdaptiveTypography();
_latestSnapshot = null;
}
@@ -560,7 +570,7 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
BackgroundTintLayer.Background = CreateSolidBrush(palette.Tint);
var particleBrush = ResolveParticleBrush(ToThemeKind(kind), palette.ParticleColor);
var isNightVisual = kind is WeatherVisualKind.ClearNight or WeatherVisualKind.CloudyNight;
var isNightVisual = kind is WeatherVisualKind.ClearNight or WeatherVisualKind.PartlyCloudyNight or WeatherVisualKind.CloudyNight;
var backgroundSamples = WeatherTypographyAccessibility.BuildBackgroundSamples(
palette.GradientFrom,
palette.GradientTo,
@@ -690,17 +700,28 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
private static WeatherVisualKind ResolveVisualKind(int? weatherCode, bool isNight)
{
return HyperOS3WeatherTheme.ResolveVisualKind(weatherCode, isNight) switch
return ResolveVisualKind(HyperOS3WeatherTheme.ResolveVisualKind(weatherCode, isNight));
}
private static WeatherVisualKind ResolveVisualKind(HyperOS3WeatherVisualKind kind)
{
return kind switch
{
HyperOS3WeatherVisualKind.Unknown => WeatherVisualKind.Unknown,
HyperOS3WeatherVisualKind.ClearDay => WeatherVisualKind.ClearDay,
HyperOS3WeatherVisualKind.ClearNight => WeatherVisualKind.ClearNight,
HyperOS3WeatherVisualKind.PartlyCloudyDay => WeatherVisualKind.PartlyCloudyDay,
HyperOS3WeatherVisualKind.PartlyCloudyNight => WeatherVisualKind.PartlyCloudyNight,
HyperOS3WeatherVisualKind.CloudyDay => WeatherVisualKind.CloudyDay,
HyperOS3WeatherVisualKind.CloudyNight => WeatherVisualKind.CloudyNight,
HyperOS3WeatherVisualKind.Haze => WeatherVisualKind.Haze,
HyperOS3WeatherVisualKind.Sleet => WeatherVisualKind.Sleet,
HyperOS3WeatherVisualKind.RainLight => WeatherVisualKind.RainLight,
HyperOS3WeatherVisualKind.RainHeavy => WeatherVisualKind.RainHeavy,
HyperOS3WeatherVisualKind.Storm => WeatherVisualKind.Storm,
HyperOS3WeatherVisualKind.Snow => WeatherVisualKind.Snow,
_ => WeatherVisualKind.Fog
HyperOS3WeatherVisualKind.Fog => WeatherVisualKind.Fog,
_ => WeatherVisualKind.Unknown
};
}
@@ -721,35 +742,28 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
{
return kind switch
{
WeatherVisualKind.Unknown => HyperOS3WeatherVisualKind.Unknown,
WeatherVisualKind.ClearDay => HyperOS3WeatherVisualKind.ClearDay,
WeatherVisualKind.ClearNight => HyperOS3WeatherVisualKind.ClearNight,
WeatherVisualKind.PartlyCloudyDay => HyperOS3WeatherVisualKind.PartlyCloudyDay,
WeatherVisualKind.PartlyCloudyNight => HyperOS3WeatherVisualKind.PartlyCloudyNight,
WeatherVisualKind.CloudyDay => HyperOS3WeatherVisualKind.CloudyDay,
WeatherVisualKind.CloudyNight => HyperOS3WeatherVisualKind.CloudyNight,
WeatherVisualKind.Haze => HyperOS3WeatherVisualKind.Haze,
WeatherVisualKind.Sleet => HyperOS3WeatherVisualKind.Sleet,
WeatherVisualKind.RainLight => HyperOS3WeatherVisualKind.RainLight,
WeatherVisualKind.RainHeavy => HyperOS3WeatherVisualKind.RainHeavy,
WeatherVisualKind.Storm => HyperOS3WeatherVisualKind.Storm,
WeatherVisualKind.Snow => HyperOS3WeatherVisualKind.Snow,
_ => HyperOS3WeatherVisualKind.Fog
WeatherVisualKind.Fog => HyperOS3WeatherVisualKind.Fog,
_ => HyperOS3WeatherVisualKind.Unknown
};
}
private string ResolveWeatherConditionText(string? weatherText, WeatherVisualKind kind)
private string ResolveWeatherConditionText(string? weatherText, int? weatherCode, WeatherVisualKind kind)
{
if (!string.IsNullOrWhiteSpace(weatherText))
{
return weatherText;
}
return kind switch
{
WeatherVisualKind.ClearDay or WeatherVisualKind.ClearNight => L("weather.widget.condition_clear", "Clear"),
WeatherVisualKind.CloudyDay or WeatherVisualKind.CloudyNight => L("weather.widget.condition_cloudy", "Cloudy"),
WeatherVisualKind.RainLight or WeatherVisualKind.RainHeavy => L("weather.widget.condition_rain", "Rain"),
WeatherVisualKind.Storm => L("weather.widget.condition_storm", "Thunderstorm"),
WeatherVisualKind.Snow => L("weather.widget.condition_snow", "Snow"),
WeatherVisualKind.Fog => L("weather.widget.condition_fog", "Fog"),
_ => L("weather.widget.condition_unknown", "Unknown")
};
_ = kind;
return XiaomiWeatherVisualResolver.ResolveDisplayText(weatherText, weatherCode, _languageCode);
}
private static (double? Low, double? High) ResolveTemperatureRange(WeatherSnapshot snapshot)
@@ -1321,15 +1335,16 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
return kind switch
{
WeatherVisualKind.RainLight or WeatherVisualKind.RainHeavy or WeatherVisualKind.Storm or WeatherVisualKind.Snow => 1.16,
WeatherVisualKind.ClearNight or WeatherVisualKind.CloudyNight => 1.08,
WeatherVisualKind.ClearNight or WeatherVisualKind.PartlyCloudyNight or WeatherVisualKind.CloudyNight => 1.08,
WeatherVisualKind.Haze or WeatherVisualKind.Fog => 1.04,
_ => 1.0
};
}
private void SetMainWeatherIcon(WeatherVisualKind kind)
private void SetMainWeatherIcon(string? assetUri, WeatherVisualKind fallbackKind)
{
WeatherIconImage.Source = HyperOS3WeatherAssetLoader.LoadImage(
HyperOS3WeatherTheme.ResolveHeroIconAsset(ToThemeKind(kind)));
assetUri ?? HyperOS3WeatherTheme.ResolveHeroIconAsset(ToThemeKind(fallbackKind)));
}
private void SetLoadingSkeleton(bool isLoading)
@@ -1549,7 +1564,7 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
var sway = _activeVisualKind == WeatherVisualKind.Snow
? Math.Sin(_animationPhase + (i * 0.45)) * 0.55
: _activeVisualKind == WeatherVisualKind.Fog
: _activeVisualKind is WeatherVisualKind.Fog or WeatherVisualKind.Haze
? Math.Sin((_animationPhase * 0.7) + (i * 0.31)) * 0.18
: 0;
@@ -1579,16 +1594,16 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
var thickness = _activeVisualKind switch
{
WeatherVisualKind.Snow => NextRange(2.2, 4.3),
WeatherVisualKind.Fog => NextRange(10.0, 22.0),
WeatherVisualKind.Fog or WeatherVisualKind.Haze => NextRange(10.0, 22.0),
_ => NextRange(1.0, 2.2)
};
var opacity = _activeVisualKind switch
{
WeatherVisualKind.Storm => NextRange(0.26, 0.52),
WeatherVisualKind.RainHeavy => NextRange(0.24, 0.46),
WeatherVisualKind.RainLight => NextRange(0.18, 0.34),
WeatherVisualKind.RainLight or WeatherVisualKind.Sleet => NextRange(0.18, 0.34),
WeatherVisualKind.Snow => NextRange(0.40, 0.72),
WeatherVisualKind.Fog => NextRange(0.08, 0.20),
WeatherVisualKind.Fog or WeatherVisualKind.Haze => NextRange(0.08, 0.20),
_ => NextRange(0.10, 0.24)
};
@@ -1600,7 +1615,7 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
{
WeatherVisualKind.Storm => -24,
WeatherVisualKind.RainHeavy => -20,
WeatherVisualKind.RainLight => -14,
WeatherVisualKind.RainLight or WeatherVisualKind.Sleet => -14,
WeatherVisualKind.Snow => -6,
_ => 0
});

View File

@@ -3,15 +3,21 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using LanMountainDesktop.Models;
using LanMountainDesktop.Services;
namespace LanMountainDesktop.Views.Components;
public enum HyperOS3WeatherVisualKind
{
Unknown,
ClearDay,
ClearNight,
PartlyCloudyDay,
PartlyCloudyNight,
CloudyDay,
CloudyNight,
Haze,
Sleet,
RainLight,
RainHeavy,
Storm,
@@ -91,38 +97,53 @@ public static class HyperOS3WeatherTheme
private static readonly IReadOnlyDictionary<HyperOS3WeatherVisualKind, string> BackgroundAssets =
new Dictionary<HyperOS3WeatherVisualKind, string>
{
[HyperOS3WeatherVisualKind.Unknown] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/hyper_sky_back.png",
[HyperOS3WeatherVisualKind.ClearDay] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/hyper_cross_sky_day.png",
[HyperOS3WeatherVisualKind.ClearNight] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/hyper_cross_sky_night.png",
[HyperOS3WeatherVisualKind.PartlyCloudyDay] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/hyper_cross_sky_day.png",
[HyperOS3WeatherVisualKind.PartlyCloudyNight] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/hyper_cross_sky_night.png",
[HyperOS3WeatherVisualKind.CloudyDay] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/hyper_sky_front.png",
[HyperOS3WeatherVisualKind.CloudyNight] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/hyper_sky_back.png",
[HyperOS3WeatherVisualKind.Haze] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/hyper_haze.png",
[HyperOS3WeatherVisualKind.Sleet] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/hyper_sky_front.png",
[HyperOS3WeatherVisualKind.RainLight] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/hyper_sky_front.png",
[HyperOS3WeatherVisualKind.RainHeavy] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/hyper_sky_back.png",
[HyperOS3WeatherVisualKind.Storm] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/hyper_cross_sky_night.png",
[HyperOS3WeatherVisualKind.Snow] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/hyper_sky_top.png",
[HyperOS3WeatherVisualKind.Fog] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/hyper_sky_back.png"
[HyperOS3WeatherVisualKind.Fog] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/hyper_fog.png"
};
private static readonly IReadOnlyDictionary<HyperOS3WeatherVisualKind, string> HeroIconAssets =
new Dictionary<HyperOS3WeatherVisualKind, string>
{
[HyperOS3WeatherVisualKind.Unknown] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_cloudy.webp",
[HyperOS3WeatherVisualKind.ClearDay] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_hero_sun_soft.png",
[HyperOS3WeatherVisualKind.ClearNight] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_hero_moon_soft.png",
[HyperOS3WeatherVisualKind.CloudyDay] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_hero_sun_soft.png",
[HyperOS3WeatherVisualKind.CloudyNight] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_hero_moon_soft.png",
[HyperOS3WeatherVisualKind.RainLight] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_rain_light_soft.png",
[HyperOS3WeatherVisualKind.RainHeavy] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_rain_heavy_soft.png",
[HyperOS3WeatherVisualKind.Storm] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_storm_soft.png",
[HyperOS3WeatherVisualKind.Snow] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_snow_soft.png",
[HyperOS3WeatherVisualKind.Fog] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_hero_sun_soft.png"
[HyperOS3WeatherVisualKind.PartlyCloudyDay] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_partly_cloudy_day.webp",
[HyperOS3WeatherVisualKind.PartlyCloudyNight] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_partly_cloudy_night.webp",
[HyperOS3WeatherVisualKind.CloudyDay] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_cloudy.webp",
[HyperOS3WeatherVisualKind.CloudyNight] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_cloudy.webp",
[HyperOS3WeatherVisualKind.Haze] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_haze.webp",
[HyperOS3WeatherVisualKind.Sleet] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_sleet.webp",
[HyperOS3WeatherVisualKind.RainLight] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_rain_light.webp",
[HyperOS3WeatherVisualKind.RainHeavy] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_rain_heavy.webp",
[HyperOS3WeatherVisualKind.Storm] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_thunder.webp",
[HyperOS3WeatherVisualKind.Snow] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_snow.webp",
[HyperOS3WeatherVisualKind.Fog] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_fog_soft.png"
};
private static readonly IReadOnlyDictionary<HyperOS3WeatherVisualKind, string> MiniIconAssets =
new Dictionary<HyperOS3WeatherVisualKind, string>
{
[HyperOS3WeatherVisualKind.Unknown] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_cloudy_soft.png",
[HyperOS3WeatherVisualKind.ClearDay] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_partly_cloudy_day_soft.png",
[HyperOS3WeatherVisualKind.ClearNight] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_partly_cloudy_night_soft.png",
[HyperOS3WeatherVisualKind.CloudyDay] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_partly_cloudy_day_soft.png",
[HyperOS3WeatherVisualKind.CloudyNight] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_partly_cloudy_night_soft.png",
[HyperOS3WeatherVisualKind.PartlyCloudyDay] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_partly_cloudy_day_soft.png",
[HyperOS3WeatherVisualKind.PartlyCloudyNight] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_partly_cloudy_night_soft.png",
[HyperOS3WeatherVisualKind.CloudyDay] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_cloudy_soft.png",
[HyperOS3WeatherVisualKind.CloudyNight] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_cloudy_soft.png",
[HyperOS3WeatherVisualKind.Haze] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_haze.webp",
[HyperOS3WeatherVisualKind.Sleet] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_sleet.webp",
[HyperOS3WeatherVisualKind.RainLight] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_rain_light_soft.png",
[HyperOS3WeatherVisualKind.RainHeavy] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_rain_heavy_soft.png",
[HyperOS3WeatherVisualKind.Storm] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_storm_soft.png",
@@ -133,6 +154,14 @@ public static class HyperOS3WeatherTheme
private static readonly IReadOnlyDictionary<HyperOS3WeatherVisualKind, HyperOS3WeatherPalette> Palettes =
new Dictionary<HyperOS3WeatherVisualKind, HyperOS3WeatherPalette>
{
[HyperOS3WeatherVisualKind.Unknown] = new(
GradientFrom: "#6B7785",
GradientTo: "#98A4B3",
Tint: "#55606E",
PrimaryText: "#F8FBFF",
SecondaryText: "#E1E8F0",
TertiaryText: "#C2CCD8",
ParticleColor: "#24FFFFFF"),
[HyperOS3WeatherVisualKind.ClearDay] = new(
GradientFrom: "#5F7FA3",
GradientTo: "#9BB4CF",
@@ -149,6 +178,22 @@ public static class HyperOS3WeatherTheme
SecondaryText: "#D9E4F0",
TertiaryText: "#B4C3D6",
ParticleColor: "#00FFFFFF"),
[HyperOS3WeatherVisualKind.PartlyCloudyDay] = new(
GradientFrom: "#607D9F",
GradientTo: "#9BB2C8",
Tint: "#55728F",
PrimaryText: "#F8FCFF",
SecondaryText: "#E4EDF7",
TertiaryText: "#C4D4E4",
ParticleColor: "#12FFFFFF"),
[HyperOS3WeatherVisualKind.PartlyCloudyNight] = new(
GradientFrom: "#5A6E87",
GradientTo: "#8FA4BC",
Tint: "#4D6178",
PrimaryText: "#F8FBFF",
SecondaryText: "#D9E5F0",
TertiaryText: "#B6C5D7",
ParticleColor: "#1FE8F2FF"),
[HyperOS3WeatherVisualKind.CloudyDay] = new(
GradientFrom: "#5D799A",
GradientTo: "#95ADC6",
@@ -165,6 +210,22 @@ public static class HyperOS3WeatherTheme
SecondaryText: "#D4E0ED",
TertiaryText: "#B0BFD2",
ParticleColor: "#30F0F5FF"),
[HyperOS3WeatherVisualKind.Haze] = new(
GradientFrom: "#6A7E95",
GradientTo: "#A5B2BE",
Tint: "#657789",
PrimaryText: "#F7FBFF",
SecondaryText: "#E3E8EE",
TertiaryText: "#C1CBD6",
ParticleColor: "#6FD6DEE8"),
[HyperOS3WeatherVisualKind.Sleet] = new(
GradientFrom: "#61788F",
GradientTo: "#9AB0C4",
Tint: "#587087",
PrimaryText: "#F7FBFF",
SecondaryText: "#DCE6F0",
TertiaryText: "#B8C7D7",
ParticleColor: "#98DCEBFF"),
[HyperOS3WeatherVisualKind.RainLight] = new(
GradientFrom: "#4F6786",
GradientTo: "#7A92AF",
@@ -210,6 +271,14 @@ public static class HyperOS3WeatherTheme
private static readonly IReadOnlyDictionary<HyperOS3WeatherVisualKind, HyperOS3WeatherMotion> Motions =
new Dictionary<HyperOS3WeatherVisualKind, HyperOS3WeatherMotion>
{
[HyperOS3WeatherVisualKind.Unknown] = new(
DriftX: 8.0, DriftY: 5.0, ZoomBase: 1.050, ZoomAmplitude: 0.010,
MotionOpacityBase: 0.24, MotionOpacityPulse: 0.05,
LightOpacityBase: 0.60, LightOpacityPulse: 0.05,
ShadeOpacityBase: 0.80, ShadeOpacityPulse: 0.03,
PhaseStep: 0.018, ParticleCount: 0,
ParticleSpeedMin: 0, ParticleSpeedMax: 0,
ParticleLengthMin: 0, ParticleLengthMax: 0, ParticleDriftPerTick: 0),
[HyperOS3WeatherVisualKind.ClearDay] = new(
DriftX: 8.0, DriftY: 4.0, ZoomBase: 1.055, ZoomAmplitude: 0.012,
MotionOpacityBase: 0.22, MotionOpacityPulse: 0.05,
@@ -226,6 +295,22 @@ public static class HyperOS3WeatherTheme
PhaseStep: 0.018, ParticleCount: 0,
ParticleSpeedMin: 0, ParticleSpeedMax: 0,
ParticleLengthMin: 0, ParticleLengthMax: 0, ParticleDriftPerTick: 0),
[HyperOS3WeatherVisualKind.PartlyCloudyDay] = new(
DriftX: 10.0, DriftY: 6.0, ZoomBase: 1.058, ZoomAmplitude: 0.013,
MotionOpacityBase: 0.26, MotionOpacityPulse: 0.05,
LightOpacityBase: 0.65, LightOpacityPulse: 0.06,
ShadeOpacityBase: 0.76, ShadeOpacityPulse: 0.03,
PhaseStep: 0.017, ParticleCount: 0,
ParticleSpeedMin: 0, ParticleSpeedMax: 0,
ParticleLengthMin: 0, ParticleLengthMax: 0, ParticleDriftPerTick: 0),
[HyperOS3WeatherVisualKind.PartlyCloudyNight] = new(
DriftX: 12.0, DriftY: 7.0, ZoomBase: 1.061, ZoomAmplitude: 0.013,
MotionOpacityBase: 0.30, MotionOpacityPulse: 0.06,
LightOpacityBase: 0.55, LightOpacityPulse: 0.05,
ShadeOpacityBase: 0.82, ShadeOpacityPulse: 0.03,
PhaseStep: 0.019, ParticleCount: 0,
ParticleSpeedMin: 0, ParticleSpeedMax: 0,
ParticleLengthMin: 0, ParticleLengthMax: 0, ParticleDriftPerTick: 0),
[HyperOS3WeatherVisualKind.CloudyDay] = new(
DriftX: 12.0, DriftY: 7.0, ZoomBase: 1.060, ZoomAmplitude: 0.013,
MotionOpacityBase: 0.32, MotionOpacityPulse: 0.06,
@@ -242,6 +327,22 @@ public static class HyperOS3WeatherTheme
PhaseStep: 0.021, ParticleCount: 0,
ParticleSpeedMin: 0.35, ParticleSpeedMax: 0.80,
ParticleLengthMin: 16, ParticleLengthMax: 30, ParticleDriftPerTick: 0.12),
[HyperOS3WeatherVisualKind.Haze] = new(
DriftX: 9.0, DriftY: 5.0, ZoomBase: 1.052, ZoomAmplitude: 0.010,
MotionOpacityBase: 0.30, MotionOpacityPulse: 0.04,
LightOpacityBase: 0.54, LightOpacityPulse: 0.04,
ShadeOpacityBase: 0.85, ShadeOpacityPulse: 0.03,
PhaseStep: 0.018, ParticleCount: 0,
ParticleSpeedMin: 0.20, ParticleSpeedMax: 0.45,
ParticleLengthMin: 12, ParticleLengthMax: 28, ParticleDriftPerTick: 0.10),
[HyperOS3WeatherVisualKind.Sleet] = new(
DriftX: 7.0, DriftY: 9.0, ZoomBase: 1.048, ZoomAmplitude: 0.011,
MotionOpacityBase: 0.31, MotionOpacityPulse: 0.06,
LightOpacityBase: 0.52, LightOpacityPulse: 0.05,
ShadeOpacityBase: 0.82, ShadeOpacityPulse: 0.04,
PhaseStep: 0.026, ParticleCount: 20,
ParticleSpeedMin: 1.20, ParticleSpeedMax: 2.40,
ParticleLengthMin: 8, ParticleLengthMax: 18, ParticleDriftPerTick: 0.34),
[HyperOS3WeatherVisualKind.RainLight] = new(
DriftX: 6.0, DriftY: 10.0, ZoomBase: 1.050, ZoomAmplitude: 0.010,
MotionOpacityBase: 0.30, MotionOpacityPulse: 0.08,
@@ -296,16 +397,20 @@ public static class HyperOS3WeatherTheme
public static HyperOS3WeatherVisualKind ResolveVisualKind(int? weatherCode, bool isNight)
{
return weatherCode switch
return XiaomiWeatherCodeMapper.ResolveBucket(weatherCode) switch
{
0 => isNight ? HyperOS3WeatherVisualKind.ClearNight : HyperOS3WeatherVisualKind.ClearDay,
1 or 2 => isNight ? HyperOS3WeatherVisualKind.CloudyNight : HyperOS3WeatherVisualKind.CloudyDay,
3 or 7 => HyperOS3WeatherVisualKind.RainLight,
8 or 9 => HyperOS3WeatherVisualKind.RainHeavy,
4 => HyperOS3WeatherVisualKind.Storm,
13 or 14 or 15 or 16 => HyperOS3WeatherVisualKind.Snow,
18 or 32 => HyperOS3WeatherVisualKind.Fog,
_ => isNight ? HyperOS3WeatherVisualKind.CloudyNight : HyperOS3WeatherVisualKind.CloudyDay
WeatherConditionBucket.Unknown => HyperOS3WeatherVisualKind.Unknown,
WeatherConditionBucket.Clear => isNight ? HyperOS3WeatherVisualKind.ClearNight : HyperOS3WeatherVisualKind.ClearDay,
WeatherConditionBucket.PartlyCloudy => isNight ? HyperOS3WeatherVisualKind.PartlyCloudyNight : HyperOS3WeatherVisualKind.PartlyCloudyDay,
WeatherConditionBucket.Cloudy => isNight ? HyperOS3WeatherVisualKind.CloudyNight : HyperOS3WeatherVisualKind.CloudyDay,
WeatherConditionBucket.Haze => HyperOS3WeatherVisualKind.Haze,
WeatherConditionBucket.Sleet => HyperOS3WeatherVisualKind.Sleet,
WeatherConditionBucket.RainLight => HyperOS3WeatherVisualKind.RainLight,
WeatherConditionBucket.RainHeavy => HyperOS3WeatherVisualKind.RainHeavy,
WeatherConditionBucket.Storm => HyperOS3WeatherVisualKind.Storm,
WeatherConditionBucket.Snow => HyperOS3WeatherVisualKind.Snow,
WeatherConditionBucket.Fog => HyperOS3WeatherVisualKind.Fog,
_ => HyperOS3WeatherVisualKind.Unknown
};
}
@@ -360,12 +465,14 @@ public static class HyperOS3WeatherTheme
{
return kind switch
{
HyperOS3WeatherVisualKind.RainLight or HyperOS3WeatherVisualKind.RainHeavy or HyperOS3WeatherVisualKind.Storm
HyperOS3WeatherVisualKind.Sleet or HyperOS3WeatherVisualKind.RainLight or HyperOS3WeatherVisualKind.RainHeavy or HyperOS3WeatherVisualKind.Storm
=> "avares://LanMountainDesktop/Assets/Weather/HyperOS3/hyper_rain_drop.png",
HyperOS3WeatherVisualKind.Haze
=> "avares://LanMountainDesktop/Assets/Weather/HyperOS3/hyper_haze.png",
HyperOS3WeatherVisualKind.Fog
=> "avares://LanMountainDesktop/Assets/Weather/HyperOS3/hyper_fog.png",
HyperOS3WeatherVisualKind.Snow
=> "avares://LanMountainDesktop/Assets/Weather/HyperOS3/hyper_snow_flake.png",
HyperOS3WeatherVisualKind.Fog
=> "avares://LanMountainDesktop/Assets/Weather/HyperOS3/hyper_haze.png",
_ => null
};
}

View File

@@ -20,10 +20,15 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
{
private enum WeatherVisualKind
{
Unknown,
ClearDay,
ClearNight,
PartlyCloudyDay,
PartlyCloudyNight,
CloudyDay,
CloudyNight,
Haze,
Sleet,
RainLight,
RainHeavy,
Storm,
@@ -480,7 +485,12 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
private void ApplySnapshot(WeatherSnapshot snapshot, string fallbackLocationName)
{
var isNight = ResolveIsNight(snapshot);
var visualKind = ResolveVisualKind(snapshot.Current.WeatherCode, isNight);
var visual = XiaomiWeatherVisualResolver.Resolve(
snapshot.Current.WeatherText,
snapshot.Current.WeatherCode,
isNight,
_languageCode);
var visualKind = ResolveVisualKind(visual.VisualKind);
ApplyVisualTheme(visualKind);
var rawLocation = string.IsNullOrWhiteSpace(snapshot.LocationName)
@@ -488,8 +498,8 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
: snapshot.LocationName;
CityTextBlock.Text = ResolvePreciseDisplayLocation(rawLocation, _languageCode, L("weather.widget.location_unknown", "Unknown location"));
ConditionTextBlock.Text = ResolveWeatherConditionText(snapshot.Current.WeatherText, visualKind);
SetMainWeatherIcon(visualKind);
ConditionTextBlock.Text = visual.DisplayText;
SetMainWeatherIcon(visual.PrimaryIconAsset, visualKind);
SetLoadingSkeleton(false);
TemperatureTextBlock.Text = FormatTemperature(snapshot.Current.TemperatureC);
@@ -503,7 +513,7 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
{
var fallbackKind = ResolveFallbackVisualKind();
ApplyVisualTheme(fallbackKind);
SetMainWeatherIcon(fallbackKind);
SetMainWeatherIcon(null, fallbackKind);
SetLoadingSkeleton(false);
CityTextBlock.Text = L("weather.widget.location_not_configured", "Weather location is not configured");
ConditionTextBlock.Text = L("weather.widget.condition_unknown", "Unknown");
@@ -518,7 +528,7 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
{
var loadingKind = IsNightNow() ? WeatherVisualKind.CloudyNight : WeatherVisualKind.CloudyDay;
ApplyVisualTheme(loadingKind);
SetMainWeatherIcon(loadingKind);
SetMainWeatherIcon(null, loadingKind);
SetLoadingSkeleton(true);
CityTextBlock.Text = ResolvePreciseDisplayLocation(
locationName,
@@ -533,8 +543,8 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
private void ApplyFailedState(string locationName)
{
ApplyVisualTheme(WeatherVisualKind.Fog);
SetMainWeatherIcon(WeatherVisualKind.Fog);
ApplyVisualTheme(WeatherVisualKind.Unknown);
SetMainWeatherIcon(HyperOS3WeatherTheme.ResolveHeroIconAsset(HyperOS3WeatherVisualKind.Unknown), WeatherVisualKind.Unknown);
SetLoadingSkeleton(false);
CityTextBlock.Text = ResolvePreciseDisplayLocation(
locationName,
@@ -543,7 +553,7 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
ConditionTextBlock.Text = L("weather.widget.fetch_failed", "Weather fetch failed");
TemperatureTextBlock.Text = "--°";
RangeTextBlock.Text = L("weather.widget.range_unknown", "-- / --");
ApplyHourlyForecastItems(BuildPlaceholderHourlyForecastItems(WeatherVisualKind.Fog));
ApplyHourlyForecastItems(BuildPlaceholderHourlyForecastItems(WeatherVisualKind.Unknown));
ApplyAdaptiveTypography();
_latestSnapshot = null;
}
@@ -558,7 +568,7 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
BackgroundTintLayer.Background = CreateSolidBrush(palette.Tint);
var particleBrush = ResolveParticleBrush(ToThemeKind(kind), palette.ParticleColor);
var isNightVisual = kind is WeatherVisualKind.ClearNight or WeatherVisualKind.CloudyNight;
var isNightVisual = kind is WeatherVisualKind.ClearNight or WeatherVisualKind.PartlyCloudyNight or WeatherVisualKind.CloudyNight;
var backgroundSamples = WeatherTypographyAccessibility.BuildBackgroundSamples(
palette.GradientFrom,
palette.GradientTo,
@@ -676,17 +686,28 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
private static WeatherVisualKind ResolveVisualKind(int? weatherCode, bool isNight)
{
return HyperOS3WeatherTheme.ResolveVisualKind(weatherCode, isNight) switch
return ResolveVisualKind(HyperOS3WeatherTheme.ResolveVisualKind(weatherCode, isNight));
}
private static WeatherVisualKind ResolveVisualKind(HyperOS3WeatherVisualKind kind)
{
return kind switch
{
HyperOS3WeatherVisualKind.Unknown => WeatherVisualKind.Unknown,
HyperOS3WeatherVisualKind.ClearDay => WeatherVisualKind.ClearDay,
HyperOS3WeatherVisualKind.ClearNight => WeatherVisualKind.ClearNight,
HyperOS3WeatherVisualKind.PartlyCloudyDay => WeatherVisualKind.PartlyCloudyDay,
HyperOS3WeatherVisualKind.PartlyCloudyNight => WeatherVisualKind.PartlyCloudyNight,
HyperOS3WeatherVisualKind.CloudyDay => WeatherVisualKind.CloudyDay,
HyperOS3WeatherVisualKind.CloudyNight => WeatherVisualKind.CloudyNight,
HyperOS3WeatherVisualKind.Haze => WeatherVisualKind.Haze,
HyperOS3WeatherVisualKind.Sleet => WeatherVisualKind.Sleet,
HyperOS3WeatherVisualKind.RainLight => WeatherVisualKind.RainLight,
HyperOS3WeatherVisualKind.RainHeavy => WeatherVisualKind.RainHeavy,
HyperOS3WeatherVisualKind.Storm => WeatherVisualKind.Storm,
HyperOS3WeatherVisualKind.Snow => WeatherVisualKind.Snow,
_ => WeatherVisualKind.Fog
HyperOS3WeatherVisualKind.Fog => WeatherVisualKind.Fog,
_ => WeatherVisualKind.Unknown
};
}
@@ -707,35 +728,28 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
{
return kind switch
{
WeatherVisualKind.Unknown => HyperOS3WeatherVisualKind.Unknown,
WeatherVisualKind.ClearDay => HyperOS3WeatherVisualKind.ClearDay,
WeatherVisualKind.ClearNight => HyperOS3WeatherVisualKind.ClearNight,
WeatherVisualKind.PartlyCloudyDay => HyperOS3WeatherVisualKind.PartlyCloudyDay,
WeatherVisualKind.PartlyCloudyNight => HyperOS3WeatherVisualKind.PartlyCloudyNight,
WeatherVisualKind.CloudyDay => HyperOS3WeatherVisualKind.CloudyDay,
WeatherVisualKind.CloudyNight => HyperOS3WeatherVisualKind.CloudyNight,
WeatherVisualKind.Haze => HyperOS3WeatherVisualKind.Haze,
WeatherVisualKind.Sleet => HyperOS3WeatherVisualKind.Sleet,
WeatherVisualKind.RainLight => HyperOS3WeatherVisualKind.RainLight,
WeatherVisualKind.RainHeavy => HyperOS3WeatherVisualKind.RainHeavy,
WeatherVisualKind.Storm => HyperOS3WeatherVisualKind.Storm,
WeatherVisualKind.Snow => HyperOS3WeatherVisualKind.Snow,
_ => HyperOS3WeatherVisualKind.Fog
WeatherVisualKind.Fog => HyperOS3WeatherVisualKind.Fog,
_ => HyperOS3WeatherVisualKind.Unknown
};
}
private string ResolveWeatherConditionText(string? weatherText, WeatherVisualKind kind)
private string ResolveWeatherConditionText(string? weatherText, int? weatherCode, WeatherVisualKind kind)
{
if (!string.IsNullOrWhiteSpace(weatherText))
{
return weatherText;
}
return kind switch
{
WeatherVisualKind.ClearDay or WeatherVisualKind.ClearNight => L("weather.widget.condition_clear", "Clear"),
WeatherVisualKind.CloudyDay or WeatherVisualKind.CloudyNight => L("weather.widget.condition_cloudy", "Cloudy"),
WeatherVisualKind.RainLight or WeatherVisualKind.RainHeavy => L("weather.widget.condition_rain", "Rain"),
WeatherVisualKind.Storm => L("weather.widget.condition_storm", "Thunderstorm"),
WeatherVisualKind.Snow => L("weather.widget.condition_snow", "Snow"),
WeatherVisualKind.Fog => L("weather.widget.condition_fog", "Fog"),
_ => L("weather.widget.condition_unknown", "Unknown")
};
_ = kind;
return XiaomiWeatherVisualResolver.ResolveDisplayText(weatherText, weatherCode, _languageCode);
}
private static (double? Low, double? High) ResolveTemperatureRange(WeatherSnapshot snapshot)
@@ -1171,15 +1185,16 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
return kind switch
{
WeatherVisualKind.RainLight or WeatherVisualKind.RainHeavy or WeatherVisualKind.Storm or WeatherVisualKind.Snow => 1.16,
WeatherVisualKind.ClearNight or WeatherVisualKind.CloudyNight => 1.08,
WeatherVisualKind.ClearNight or WeatherVisualKind.PartlyCloudyNight or WeatherVisualKind.CloudyNight => 1.08,
WeatherVisualKind.Haze or WeatherVisualKind.Fog => 1.04,
_ => 1.0
};
}
private void SetMainWeatherIcon(WeatherVisualKind kind)
private void SetMainWeatherIcon(string? assetUri, WeatherVisualKind fallbackKind)
{
WeatherIconImage.Source = HyperOS3WeatherAssetLoader.LoadImage(
HyperOS3WeatherTheme.ResolveHeroIconAsset(ToThemeKind(kind)));
assetUri ?? HyperOS3WeatherTheme.ResolveHeroIconAsset(ToThemeKind(fallbackKind)));
}
private void SetLoadingSkeleton(bool isLoading)
@@ -1399,7 +1414,7 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
var sway = _activeVisualKind == WeatherVisualKind.Snow
? Math.Sin(_animationPhase + (i * 0.45)) * 0.55
: _activeVisualKind == WeatherVisualKind.Fog
: _activeVisualKind is WeatherVisualKind.Fog or WeatherVisualKind.Haze
? Math.Sin((_animationPhase * 0.7) + (i * 0.31)) * 0.18
: 0;
@@ -1429,16 +1444,16 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
var thickness = _activeVisualKind switch
{
WeatherVisualKind.Snow => NextRange(2.2, 4.3),
WeatherVisualKind.Fog => NextRange(10.0, 22.0),
WeatherVisualKind.Fog or WeatherVisualKind.Haze => NextRange(10.0, 22.0),
_ => NextRange(1.0, 2.2)
};
var opacity = _activeVisualKind switch
{
WeatherVisualKind.Storm => NextRange(0.26, 0.52),
WeatherVisualKind.RainHeavy => NextRange(0.24, 0.46),
WeatherVisualKind.RainLight => NextRange(0.18, 0.34),
WeatherVisualKind.RainLight or WeatherVisualKind.Sleet => NextRange(0.18, 0.34),
WeatherVisualKind.Snow => NextRange(0.40, 0.72),
WeatherVisualKind.Fog => NextRange(0.08, 0.20),
WeatherVisualKind.Fog or WeatherVisualKind.Haze => NextRange(0.08, 0.20),
_ => NextRange(0.10, 0.24)
};
@@ -1450,7 +1465,7 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
{
WeatherVisualKind.Storm => -24,
WeatherVisualKind.RainHeavy => -20,
WeatherVisualKind.RainLight => -14,
WeatherVisualKind.RainLight or WeatherVisualKind.Sleet => -14,
WeatherVisualKind.Snow => -6,
_ => 0
});

View File

@@ -22,10 +22,15 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
{
private enum WeatherVisualKind
{
Unknown,
ClearDay,
ClearNight,
PartlyCloudyDay,
PartlyCloudyNight,
CloudyDay,
CloudyNight,
Haze,
Sleet,
RainLight,
RainHeavy,
Storm,
@@ -427,7 +432,12 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
private void ApplySnapshot(WeatherSnapshot snapshot, string fallbackLocationName)
{
var isNight = ResolveIsNight(snapshot);
var visualKind = ResolveVisualKind(snapshot.Current.WeatherCode, isNight);
var visual = XiaomiWeatherVisualResolver.Resolve(
snapshot.Current.WeatherText,
snapshot.Current.WeatherCode,
isNight,
_languageCode);
var visualKind = ResolveVisualKind(visual.VisualKind);
ApplyVisualTheme(visualKind);
var rawLocation = string.IsNullOrWhiteSpace(snapshot.LocationName)
@@ -435,8 +445,8 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
: snapshot.LocationName;
CityTextBlock.Text = ResolvePreciseDisplayLocation(rawLocation, _languageCode, L("weather.widget.location_unknown", "Unknown location"));
ConditionTextBlock.Text = ResolveWeatherConditionText(snapshot.Current.WeatherText, visualKind);
SetWeatherIcon(visualKind);
ConditionTextBlock.Text = visual.DisplayText;
SetWeatherIcon(visual.PrimaryIconAsset, visualKind);
SetLoadingSkeleton(false);
TemperatureTextBlock.Text = FormatTemperature(snapshot.Current.TemperatureC);
@@ -449,7 +459,7 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
{
var fallbackKind = ResolveFallbackVisualKind();
ApplyVisualTheme(fallbackKind);
SetWeatherIcon(fallbackKind);
SetWeatherIcon(null, fallbackKind);
SetLoadingSkeleton(false);
CityTextBlock.Text = L("weather.widget.location_not_configured", "Weather location is not configured");
ConditionTextBlock.Text = L("weather.widget.configure_hint", "Open Settings > Weather to configure");
@@ -463,7 +473,7 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
{
var loadingKind = IsNightNow() ? WeatherVisualKind.CloudyNight : WeatherVisualKind.CloudyDay;
ApplyVisualTheme(loadingKind);
SetWeatherIcon(loadingKind);
SetWeatherIcon(null, loadingKind);
SetLoadingSkeleton(true);
CityTextBlock.Text = ResolvePreciseDisplayLocation(
locationName,
@@ -477,8 +487,8 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
private void ApplyFailedState(string locationName)
{
ApplyVisualTheme(WeatherVisualKind.Fog);
SetWeatherIcon(WeatherVisualKind.Fog);
ApplyVisualTheme(WeatherVisualKind.Unknown);
SetWeatherIcon(HyperOS3WeatherTheme.ResolveHeroIconAsset(HyperOS3WeatherVisualKind.Unknown), WeatherVisualKind.Unknown);
SetLoadingSkeleton(false);
CityTextBlock.Text = ResolvePreciseDisplayLocation(
locationName,
@@ -500,7 +510,7 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
BackgroundMotionLayer.Background = ResolveWeatherBackgroundBrush(kind, palette);
BackgroundTintLayer.Background = CreateSolidBrush(palette.Tint);
var isNightVisual = kind is WeatherVisualKind.ClearNight or WeatherVisualKind.CloudyNight;
var isNightVisual = kind is WeatherVisualKind.ClearNight or WeatherVisualKind.PartlyCloudyNight or WeatherVisualKind.CloudyNight;
var backgroundSamples = WeatherTypographyAccessibility.BuildBackgroundSamples(
palette.GradientFrom,
palette.GradientTo,
@@ -610,17 +620,28 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
private static WeatherVisualKind ResolveVisualKind(int? weatherCode, bool isNight)
{
return HyperOS3WeatherTheme.ResolveVisualKind(weatherCode, isNight) switch
return ResolveVisualKind(HyperOS3WeatherTheme.ResolveVisualKind(weatherCode, isNight));
}
private static WeatherVisualKind ResolveVisualKind(HyperOS3WeatherVisualKind kind)
{
return kind switch
{
HyperOS3WeatherVisualKind.Unknown => WeatherVisualKind.Unknown,
HyperOS3WeatherVisualKind.ClearDay => WeatherVisualKind.ClearDay,
HyperOS3WeatherVisualKind.ClearNight => WeatherVisualKind.ClearNight,
HyperOS3WeatherVisualKind.PartlyCloudyDay => WeatherVisualKind.PartlyCloudyDay,
HyperOS3WeatherVisualKind.PartlyCloudyNight => WeatherVisualKind.PartlyCloudyNight,
HyperOS3WeatherVisualKind.CloudyDay => WeatherVisualKind.CloudyDay,
HyperOS3WeatherVisualKind.CloudyNight => WeatherVisualKind.CloudyNight,
HyperOS3WeatherVisualKind.Haze => WeatherVisualKind.Haze,
HyperOS3WeatherVisualKind.Sleet => WeatherVisualKind.Sleet,
HyperOS3WeatherVisualKind.RainLight => WeatherVisualKind.RainLight,
HyperOS3WeatherVisualKind.RainHeavy => WeatherVisualKind.RainHeavy,
HyperOS3WeatherVisualKind.Storm => WeatherVisualKind.Storm,
HyperOS3WeatherVisualKind.Snow => WeatherVisualKind.Snow,
_ => WeatherVisualKind.Fog
HyperOS3WeatherVisualKind.Fog => WeatherVisualKind.Fog,
_ => WeatherVisualKind.Unknown
};
}
@@ -641,35 +662,28 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
{
return kind switch
{
WeatherVisualKind.Unknown => HyperOS3WeatherVisualKind.Unknown,
WeatherVisualKind.ClearDay => HyperOS3WeatherVisualKind.ClearDay,
WeatherVisualKind.ClearNight => HyperOS3WeatherVisualKind.ClearNight,
WeatherVisualKind.PartlyCloudyDay => HyperOS3WeatherVisualKind.PartlyCloudyDay,
WeatherVisualKind.PartlyCloudyNight => HyperOS3WeatherVisualKind.PartlyCloudyNight,
WeatherVisualKind.CloudyDay => HyperOS3WeatherVisualKind.CloudyDay,
WeatherVisualKind.CloudyNight => HyperOS3WeatherVisualKind.CloudyNight,
WeatherVisualKind.Haze => HyperOS3WeatherVisualKind.Haze,
WeatherVisualKind.Sleet => HyperOS3WeatherVisualKind.Sleet,
WeatherVisualKind.RainLight => HyperOS3WeatherVisualKind.RainLight,
WeatherVisualKind.RainHeavy => HyperOS3WeatherVisualKind.RainHeavy,
WeatherVisualKind.Storm => HyperOS3WeatherVisualKind.Storm,
WeatherVisualKind.Snow => HyperOS3WeatherVisualKind.Snow,
_ => HyperOS3WeatherVisualKind.Fog
WeatherVisualKind.Fog => HyperOS3WeatherVisualKind.Fog,
_ => HyperOS3WeatherVisualKind.Unknown
};
}
private string ResolveWeatherConditionText(string? weatherText, WeatherVisualKind kind)
private string ResolveWeatherConditionText(string? weatherText, int? weatherCode, WeatherVisualKind kind)
{
if (!string.IsNullOrWhiteSpace(weatherText))
{
return weatherText;
}
return kind switch
{
WeatherVisualKind.ClearDay or WeatherVisualKind.ClearNight => L("weather.widget.condition_clear", "Clear"),
WeatherVisualKind.CloudyDay or WeatherVisualKind.CloudyNight => L("weather.widget.condition_cloudy", "Cloudy"),
WeatherVisualKind.RainLight or WeatherVisualKind.RainHeavy => L("weather.widget.condition_rain", "Rain"),
WeatherVisualKind.Storm => L("weather.widget.condition_storm", "Thunderstorm"),
WeatherVisualKind.Snow => L("weather.widget.condition_snow", "Snow"),
WeatherVisualKind.Fog => L("weather.widget.condition_fog", "Fog"),
_ => L("weather.widget.condition_unknown", "Unknown")
};
_ = kind;
return XiaomiWeatherVisualResolver.ResolveDisplayText(weatherText, weatherCode, _languageCode);
}
private static (double? Low, double? High) ResolveTemperatureRange(WeatherSnapshot snapshot)
@@ -965,15 +979,16 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
return kind switch
{
WeatherVisualKind.RainLight or WeatherVisualKind.RainHeavy or WeatherVisualKind.Storm or WeatherVisualKind.Snow => 1.16,
WeatherVisualKind.ClearNight or WeatherVisualKind.CloudyNight => 1.08,
WeatherVisualKind.ClearNight or WeatherVisualKind.PartlyCloudyNight or WeatherVisualKind.CloudyNight => 1.08,
WeatherVisualKind.Haze or WeatherVisualKind.Fog => 1.04,
_ => 1.0
};
}
private void SetWeatherIcon(WeatherVisualKind kind)
private void SetWeatherIcon(string? assetUri, WeatherVisualKind fallbackKind)
{
WeatherIconImage.Source = HyperOS3WeatherAssetLoader.LoadImage(
HyperOS3WeatherTheme.ResolveHeroIconAsset(ToThemeKind(kind)));
assetUri ?? HyperOS3WeatherTheme.ResolveHeroIconAsset(ToThemeKind(fallbackKind)));
}
private void SetLoadingSkeleton(bool isLoading)
@@ -1190,7 +1205,7 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
var sway = _activeVisualKind == WeatherVisualKind.Snow
? Math.Sin(_animationPhase + (i * 0.45)) * 0.55
: _activeVisualKind == WeatherVisualKind.Fog
: _activeVisualKind is WeatherVisualKind.Fog or WeatherVisualKind.Haze
? Math.Sin((_animationPhase * 0.7) + (i * 0.31)) * 0.18
: 0;
@@ -1220,16 +1235,16 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
var thickness = _activeVisualKind switch
{
WeatherVisualKind.Snow => NextRange(2.2, 4.3),
WeatherVisualKind.Fog => NextRange(10.0, 22.0),
WeatherVisualKind.Fog or WeatherVisualKind.Haze => NextRange(10.0, 22.0),
_ => NextRange(1.0, 2.2)
};
var opacity = _activeVisualKind switch
{
WeatherVisualKind.Storm => NextRange(0.26, 0.52),
WeatherVisualKind.RainHeavy => NextRange(0.24, 0.46),
WeatherVisualKind.RainLight => NextRange(0.18, 0.34),
WeatherVisualKind.RainLight or WeatherVisualKind.Sleet => NextRange(0.18, 0.34),
WeatherVisualKind.Snow => NextRange(0.40, 0.72),
WeatherVisualKind.Fog => NextRange(0.08, 0.20),
WeatherVisualKind.Fog or WeatherVisualKind.Haze => NextRange(0.08, 0.20),
_ => NextRange(0.10, 0.24)
};
@@ -1241,7 +1256,7 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
{
WeatherVisualKind.Storm => -24,
WeatherVisualKind.RainHeavy => -20,
WeatherVisualKind.RainLight => -14,
WeatherVisualKind.RainLight or WeatherVisualKind.Sleet => -14,
WeatherVisualKind.Snow => -6,
_ => 0
});

View File

@@ -0,0 +1,45 @@
using System;
using LanMountainDesktop.Services;
namespace LanMountainDesktop.Views.Components;
internal readonly record struct WeatherVisualSpec(
HyperOS3WeatherVisualKind VisualKind,
string DisplayText,
string? BackgroundAsset,
string? PrimaryIconAsset,
string? CompactIconAsset,
string? ParticleAsset);
internal static class XiaomiWeatherVisualResolver
{
public static WeatherVisualSpec Resolve(string? weatherText, int? weatherCode, bool isNight, string locale)
{
var visualKind = HyperOS3WeatherTheme.ResolveVisualKind(weatherCode, isNight);
return new WeatherVisualSpec(
visualKind,
ResolveDisplayText(weatherText, weatherCode, locale),
HyperOS3WeatherTheme.ResolveBackgroundAsset(visualKind),
HyperOS3WeatherTheme.ResolveHeroIconAsset(visualKind),
HyperOS3WeatherTheme.ResolveMiniIconAsset(visualKind),
HyperOS3WeatherTheme.ResolveParticleAsset(visualKind));
}
public static string ResolveDisplayText(string? weatherText, int? weatherCode, string locale)
{
if (!string.IsNullOrWhiteSpace(weatherText))
{
return weatherText.Trim();
}
var mappedText = XiaomiWeatherCodeMapper.ResolveDisplayText(weatherCode, locale);
if (!string.IsNullOrWhiteSpace(mappedText))
{
return mappedText;
}
return locale.StartsWith("zh", StringComparison.OrdinalIgnoreCase)
? "\u672a\u77e5\u5929\u6c14"
: "Unknown";
}
}

View File

@@ -239,6 +239,7 @@ public partial class MainWindow
private void ApplyTopStatusComponentVisibility()
{
var showClock = _topStatusComponentIds.Contains(BuiltInComponentIds.Clock);
var hasVisibleTopStatusComponent = false;
if (ClockWidget is not null)
{
@@ -248,8 +249,33 @@ public partial class MainWindow
ClockWidget.SetDisplayFormat(_clockDisplayFormat);
var columnSpan = _clockDisplayFormat == ClockDisplayFormat.HourMinute ? 2 : 3;
Grid.SetColumnSpan(ClockWidget, columnSpan);
hasVisibleTopStatusComponent = true;
}
}
if (TopStatusBarHost is not null)
{
TopStatusBarHost.IsVisible = hasVisibleTopStatusComponent;
}
if (WallpaperPreviewClockWidget is not null)
{
WallpaperPreviewClockWidget.IsVisible = showClock;
if (showClock)
{
WallpaperPreviewClockWidget.SetDisplayFormat(_clockDisplayFormat);
}
}
if (WallpaperPreviewTopStatusBarHost is not null)
{
WallpaperPreviewTopStatusBarHost.IsVisible = hasVisibleTopStatusComponent;
}
if (GridPreviewTopStatusBarHost is not null)
{
GridPreviewTopStatusBarHost.IsVisible = hasVisibleTopStatusComponent;
}
}
private TaskbarContext GetCurrentTaskbarContext()

View File

@@ -71,7 +71,19 @@ public partial class MainWindow
private void OnSettingsChanged(object? sender, SettingsChangedEvent e)
{
_ = sender;
_ = e;
if (e.Scope == SettingsScope.App && e.ChangedKeys is { Count: > 0 })
{
var changedKeys = e.ChangedKeys.ToArray();
if (changedKeys.All(key =>
string.Equals(key, nameof(AppSettingsSnapshot.ThemeColorMode), StringComparison.OrdinalIgnoreCase) ||
string.Equals(key, nameof(AppSettingsSnapshot.SystemMaterialMode), StringComparison.OrdinalIgnoreCase) ||
string.Equals(key, nameof(AppSettingsSnapshot.SelectedWallpaperSeed), StringComparison.OrdinalIgnoreCase)))
{
return;
}
}
ScheduleReloadFromExternalSettings();
}
@@ -198,16 +210,20 @@ public partial class MainWindow
private ThemeColorContext BuildAdaptiveThemeContext()
{
var palette = _themeSettingsService.BuildPalette(_isNightMode, _wallpaperPath, _selectedThemeColor.ToString());
var accentColor = palette.MonetColors is { Count: > 0 }
? palette.MonetColors[0]
: _selectedThemeColor;
var appearanceSnapshot = _appearanceThemeService.GetCurrent();
return new ThemeColorContext(
accentColor,
IsLightBackground: !_isNightMode,
IsLightNavBackground: !_isNightMode,
IsNightMode: _isNightMode,
MonetColors: palette.MonetColors);
appearanceSnapshot.AccentColor,
IsLightBackground: !appearanceSnapshot.IsNightMode,
IsLightNavBackground: !appearanceSnapshot.IsNightMode,
IsNightMode: appearanceSnapshot.IsNightMode,
MonetPalette: appearanceSnapshot.MonetPalette,
MonetColors: appearanceSnapshot.MonetPalette.MonetColors,
UseNeutralSurfaces: string.Equals(
appearanceSnapshot.ThemeColorMode,
ThemeAppearanceValues.ColorModeDefaultNeutral,
StringComparison.OrdinalIgnoreCase),
SystemMaterialMode: appearanceSnapshot.SystemMaterialMode);
}
private void ApplyAdaptiveThemeResources()
@@ -222,7 +238,8 @@ public partial class MainWindow
GlassEffectService.ApplyGlassResources(applicationResources, context);
}
_defaultDesktopBackground = GetThemeBrush("AdaptiveSurfaceBaseBrush");
_defaultDesktopBackground = GetThemeBrush("AdaptiveWindowBackgroundBrush")
?? GetThemeBrush("AdaptiveSurfaceBaseBrush");
}
private void TryRestoreWallpaper(string? savedWallpaperPath, string? type = null, string? color = null)
@@ -391,9 +408,9 @@ public partial class MainWindow
return;
}
var palette = _themeSettingsService.BuildPalette(enabled, _wallpaperPath, _selectedThemeColor.ToString());
_recommendedColors = palette.RecommendedColors;
_monetColors = palette.MonetColors;
var snapshot = _appearanceThemeService.GetCurrent();
_recommendedColors = snapshot.MonetPalette.RecommendedColors;
_monetColors = snapshot.MonetPalette.MonetColors;
}
private static double CalculateRelativeLuminance(Color color)
@@ -521,13 +538,18 @@ public partial class MainWindow
{
var latestWeatherState = _weatherSettingsService.Get();
var latestUpdateState = _updateSettingsService.Get();
var latestThemeState = _themeSettingsService.Get();
return new AppSettingsSnapshot
{
GridShortSideCells = _targetShortSideCells,
GridSpacingPreset = _gridSpacingPreset,
DesktopEdgeInsetPercent = _desktopEdgeInsetPercent,
IsNightMode = _isNightMode,
ThemeColor = _selectedThemeColor.ToString(),
ThemeColor = latestThemeState.ThemeColor,
ThemeColorMode = latestThemeState.ThemeColorMode,
SystemMaterialMode = latestThemeState.SystemMaterialMode,
SelectedWallpaperSeed = latestThemeState.SelectedWallpaperSeed,
UseSystemChrome = latestThemeState.UseSystemChrome,
WallpaperPath = _wallpaperPath,
WallpaperType = _wallpaperType,
WallpaperColor = _wallpaperSolidColor?.ToString(),

View File

@@ -19,7 +19,7 @@
CanResize="False"
UseLayoutRounding="True"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
Background="{DynamicResource AdaptiveSurfaceBaseBrush}"
Background="Transparent"
Title="LanMountainDesktop">
<Design.DataContext>

View File

@@ -78,6 +78,7 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
TaskbarActionId.MinimizeToWindows
];
private readonly ISettingsFacadeService _settingsFacade = HostSettingsFacadeProvider.GetOrCreate();
private readonly IAppearanceThemeService _appearanceThemeService = HostAppearanceThemeProvider.GetOrCreate();
private readonly IGridSettingsService _gridSettingsService;
private readonly IThemeAppearanceService _themeSettingsService;
private readonly IWeatherSettingsService _weatherSettingsService;
@@ -206,6 +207,7 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
_componentEditorWindowService = new ComponentEditorWindowService(_settingsFacade);
_fluentAvaloniaTheme = Application.Current?.Styles.OfType<FluentAvaloniaTheme>().FirstOrDefault();
_settingsService.Changed += OnSettingsChanged;
_appearanceThemeService.Changed += OnAppearanceThemeChanged;
PropertyChanged += OnWindowPropertyChanged;
InitializeDesktopSurfaceSwipeHandlers();
InitializeDesktopComponentDragHandlers();
@@ -340,6 +342,7 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
_wallpaperBitmap?.Dispose();
_wallpaperBitmap = null;
_settingsService.Changed -= OnSettingsChanged;
_appearanceThemeService.Changed -= OnAppearanceThemeChanged;
PropertyChanged -= OnWindowPropertyChanged;
DesktopHost.SizeChanged -= OnDesktopHostSizeChanged;
if (Application.Current is App app && app.SettingsWindowService is { } settingsWindowService)
@@ -349,6 +352,23 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
base.OnClosed(e);
}
private void OnAppearanceThemeChanged(object? sender, AppearanceThemeSnapshot snapshot)
{
_ = sender;
Dispatcher.UIThread.Post(() =>
{
if (!IsVisible)
{
return;
}
ApplyAdaptiveThemeResources();
_recommendedColors = snapshot.MonetPalette.RecommendedColors;
_monetColors = snapshot.MonetPalette.MonetColors;
}, DispatcherPriority.Background);
}
private int CalculateDefaultShortSideCellCountFromDpi()
{
var dpi = 96d * RenderScaling;

View File

@@ -31,13 +31,224 @@
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
<ui:SettingsExpander Header="{Binding ThemeColorLabel}">
<ui:SettingsExpander Header="{Binding ThemeColorModeLabel}"
Description="{Binding ThemeColorSourceDescription}">
<ui:SettingsExpander.IconSource>
<fi:SymbolIconSource Symbol="PaintBrush" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<ComboBox Width="240"
ItemsSource="{Binding ThemeColorModes}"
SelectedItem="{Binding SelectedThemeColorMode}">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="vm:SelectionOption">
<TextBlock Text="{Binding Label}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
<ui:SettingsExpander Header="{Binding SystemMaterialLabel}"
Description="{Binding SystemMaterialDescription}">
<ui:SettingsExpander.IconSource>
<fi:SymbolIconSource Symbol="WindowDevTools" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<ComboBox Width="220"
ItemsSource="{Binding SystemMaterialModes}"
SelectedItem="{Binding SelectedSystemMaterialMode}">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="vm:SelectionOption">
<TextBlock Text="{Binding Label}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
<ui:SettingsExpander Header="{Binding ThemeColorLabel}"
Description="{Binding ThemeColorSourceDescription}">
<ui:SettingsExpander.IconSource>
<fi:SymbolIconSource Symbol="Color" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<ColorPicker Color="{Binding ThemeColorPickerValue}" />
</ui:SettingsExpander.Footer>
<ui:SettingsExpanderItem>
<StackPanel Spacing="12">
<StackPanel Orientation="Horizontal"
Spacing="12"
IsVisible="{Binding ShowNeutralPreview}">
<StackPanel Width="96"
Spacing="6">
<Border Height="54"
Background="{Binding NeutralLightPreviewBrush}"
BorderBrush="{DynamicResource AdaptiveGlassPanelBorderBrush}"
BorderThickness="1"
CornerRadius="14" />
<TextBlock Text="{Binding PreviewNeutralLightLabel}"
HorizontalAlignment="Center"
TextAlignment="Center" />
</StackPanel>
<StackPanel Width="96"
Spacing="6">
<Border Height="54"
Background="{Binding NeutralDarkPreviewBrush}"
BorderBrush="{DynamicResource AdaptiveGlassPanelBorderBrush}"
BorderThickness="1"
CornerRadius="14" />
<TextBlock Text="{Binding PreviewNeutralDarkLabel}"
HorizontalAlignment="Center"
TextAlignment="Center" />
</StackPanel>
</StackPanel>
<StackPanel Orientation="Horizontal"
Spacing="12"
IsVisible="{Binding ShowMonetPreview}">
<StackPanel Width="92"
Spacing="6">
<Border Height="54"
Background="{Binding PrimarySwatchBrush}"
BorderBrush="{DynamicResource AdaptiveGlassPanelBorderBrush}"
BorderThickness="1"
CornerRadius="14" />
<TextBlock Text="{Binding PreviewPrimaryLabel}"
HorizontalAlignment="Center"
TextAlignment="Center" />
</StackPanel>
<StackPanel Width="92"
Spacing="6">
<Border Height="54"
Background="{Binding SecondarySwatchBrush}"
BorderBrush="{DynamicResource AdaptiveGlassPanelBorderBrush}"
BorderThickness="1"
CornerRadius="14" />
<TextBlock Text="{Binding PreviewSecondaryLabel}"
HorizontalAlignment="Center"
TextAlignment="Center" />
</StackPanel>
<StackPanel Width="92"
Spacing="6">
<Border Height="54"
Background="{Binding TertiarySwatchBrush}"
BorderBrush="{DynamicResource AdaptiveGlassPanelBorderBrush}"
BorderThickness="1"
CornerRadius="14" />
<TextBlock Text="{Binding PreviewTertiaryLabel}"
HorizontalAlignment="Center"
TextAlignment="Center" />
</StackPanel>
<StackPanel Width="92"
Spacing="6">
<Border Height="54"
Background="{Binding NeutralSwatchBrush}"
BorderBrush="{DynamicResource AdaptiveGlassPanelBorderBrush}"
BorderThickness="1"
CornerRadius="14" />
<TextBlock Text="{Binding PreviewNeutralLabel}"
HorizontalAlignment="Center"
TextAlignment="Center" />
</StackPanel>
<Button x:Name="CustomSeedButton"
Width="92"
Padding="0"
Background="Transparent"
BorderThickness="0"
HorizontalAlignment="Left"
IsVisible="{Binding IsThemeColorEditable}">
<Button.Flyout>
<Flyout Placement="BottomEdgeAlignedLeft"
Closed="OnCustomSeedFlyoutClosed">
<StackPanel Width="300"
Spacing="12">
<ColorPicker Color="{Binding CustomSeedPickerValue}" />
<Button Content="{Binding SeedApplyButtonText}"
HorizontalAlignment="Right"
Click="OnApplyCustomSeedClick" />
</StackPanel>
</Flyout>
</Button.Flyout>
<StackPanel Width="92"
Spacing="6">
<Border Height="54"
Background="{Binding SeedSwatchBrush}"
BorderBrush="{DynamicResource AdaptiveGlassPanelBorderBrush}"
BorderThickness="1"
CornerRadius="14" />
<TextBlock Text="{Binding PreviewSeedLabel}"
HorizontalAlignment="Center"
TextAlignment="Center" />
</StackPanel>
</Button>
<Button x:Name="WallpaperSeedButton"
Width="92"
Padding="0"
Background="Transparent"
BorderThickness="0"
HorizontalAlignment="Left"
IsVisible="{Binding IsWallpaperMode}"
IsEnabled="{Binding IsWallpaperSeedSelectable}">
<Button.Flyout>
<Flyout Placement="BottomEdgeAlignedLeft">
<StackPanel Width="280"
Spacing="12">
<TextBlock Text="{Binding WallpaperSeedFlyoutTitle}"
FontWeight="SemiBold" />
<ItemsControl ItemsSource="{Binding WallpaperSeedCandidates}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:ThemeSeedCandidateOption">
<Button Padding="0"
Margin="0,0,8,8"
Background="Transparent"
BorderThickness="0"
Click="OnWallpaperSeedCandidateClick">
<StackPanel Width="76"
Spacing="6">
<Border Height="44"
Background="{Binding Brush}"
BorderBrush="{DynamicResource AdaptiveGlassPanelBorderBrush}"
BorderThickness="1"
CornerRadius="12" />
<TextBlock Text="{Binding Label}"
HorizontalAlignment="Center"
TextAlignment="Center"
TextWrapping="Wrap" />
<TextBlock Text="{Binding $parent[vm:AppearanceSettingsPageViewModel].WallpaperSeedCurrentText}"
HorizontalAlignment="Center"
FontSize="10"
IsVisible="{Binding IsSelected}" />
</StackPanel>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Flyout>
</Button.Flyout>
<StackPanel Width="92"
Spacing="6">
<Border Height="54"
Background="{Binding SeedSwatchBrush}"
BorderBrush="{DynamicResource AdaptiveGlassPanelBorderBrush}"
BorderThickness="1"
CornerRadius="14" />
<TextBlock Text="{Binding PreviewSeedLabel}"
HorizontalAlignment="Center"
TextAlignment="Center" />
</StackPanel>
</Button>
</StackPanel>
</StackPanel>
</ui:SettingsExpanderItem>
</ui:SettingsExpander>
</StackPanel>
</ScrollViewer>

View File

@@ -1,6 +1,10 @@
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;
@@ -15,16 +19,51 @@ namespace LanMountainDesktop.Views.SettingsPages;
public partial class AppearanceSettingsPage : SettingsPageBase
{
public AppearanceSettingsPage()
: this(new AppearanceSettingsPageViewModel(HostSettingsFacadeProvider.GetOrCreate()))
: this(new AppearanceSettingsPageViewModel(
HostSettingsFacadeProvider.GetOrCreate(),
HostAppearanceThemeProvider.GetOrCreate()))
{
}
public AppearanceSettingsPage(AppearanceSettingsPageViewModel viewModel)
{
ViewModel = viewModel;
ViewModel.RestartRequested += OnRestartRequested;
DataContext = ViewModel;
InitializeComponent();
}
public AppearanceSettingsPageViewModel ViewModel { get; }
private void OnRestartRequested(string reason)
{
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();
}
}

View File

@@ -0,0 +1,36 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:LanMountainDesktop.ViewModels"
xmlns:controls="using:LanMountainDesktop.Controls"
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:fi="using:FluentIcons.Avalonia.Fluent"
x:Class="LanMountainDesktop.Views.SettingsPages.PrivacySettingsPage"
x:DataType="vm:PrivacySettingsPageViewModel">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Classes="settings-page-container">
<controls:IconText Icon="Info"
Text="{Binding PrivacyHeader}"
Margin="0,0,0,4" />
<ui:SettingsExpander Header="{Binding CrashUploadHeader}"
Description="{Binding CrashUploadDescription}">
<ui:SettingsExpander.IconSource>
<fi:SymbolIconSource Symbol="ShieldDismiss" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<ToggleSwitch IsChecked="{Binding UploadAnonymousCrashData}" />
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
<ui:SettingsExpander Header="{Binding UsageUploadHeader}"
Description="{Binding UsageUploadDescription}">
<ui:SettingsExpander.IconSource>
<fi:SymbolIconSource Symbol="Info" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<ToggleSwitch IsChecked="{Binding UploadAnonymousUsageData}" />
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
</StackPanel>
</ScrollViewer>
</UserControl>

View File

@@ -0,0 +1,30 @@
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services.Settings;
using LanMountainDesktop.ViewModels;
namespace LanMountainDesktop.Views.SettingsPages;
[SettingsPageInfo(
"privacy",
"Privacy",
SettingsPageCategory.About,
IconKey = "Shield",
SortOrder = 34,
TitleLocalizationKey = "settings.privacy.title",
DescriptionLocalizationKey = "settings.privacy.description")]
public partial class PrivacySettingsPage : SettingsPageBase
{
public PrivacySettingsPage()
: this(new PrivacySettingsPageViewModel(HostSettingsFacadeProvider.GetOrCreate()))
{
}
public PrivacySettingsPage(PrivacySettingsPageViewModel viewModel)
{
ViewModel = viewModel;
DataContext = ViewModel;
InitializeComponent();
}
public PrivacySettingsPageViewModel ViewModel { get; }
}

View File

@@ -7,9 +7,9 @@ namespace LanMountainDesktop.Views.SettingsPages;
[SettingsPageInfo(
"status-bar",
"Status Bar",
SettingsPageCategory.Appearance,
SettingsPageCategory.Components,
IconKey = "Apps",
SortOrder = 17,
SortOrder = 15,
TitleLocalizationKey = "settings.status_bar.title",
DescriptionLocalizationKey = "settings.status_bar.description")]
public partial class StatusBarSettingsPage : SettingsPageBase

View File

@@ -13,7 +13,7 @@
WindowStartupLocation="Manual"
SystemDecorations="BorderOnly"
FontFamily="{DynamicResource AppFontFamily}"
Background="{DynamicResource AdaptiveSurfaceBaseBrush}"
Background="Transparent"
Icon="/Assets/avalonia-logo.ico"
Title="{Binding Title}">
@@ -38,13 +38,13 @@
</Window.Styles>
<Grid Classes="settings-scope"
Background="{DynamicResource AdaptiveSurfaceBaseBrush}"
RowDefinitions="Auto,Auto,*">
Background="{DynamicResource AdaptiveSettingsWindowBackgroundBrush}"
RowDefinitions="Auto,*">
<Border x:Name="WindowTitleBarHost"
Height="48"
Padding="12,0,12,0"
Background="{DynamicResource AdaptiveSurfaceBaseBrush}"
BorderBrush="{DynamicResource AdaptiveGlassPanelBorderBrush}"
Background="{DynamicResource AdaptiveSettingsWindowBackgroundBrush}"
BorderBrush="{DynamicResource AdaptiveSettingsWindowBorderBrush}"
BorderThickness="0,0,0,1"
PointerPressed="OnWindowTitleBarPointerPressed">
<Grid ColumnDefinitions="Auto,Auto,*,Auto,Auto"
@@ -113,22 +113,8 @@
</Grid>
</Border>
<ui:InfoBar x:Name="RestartInfoBar"
Grid.Row="1"
IsOpen="{Binding IsRestartRequested}"
Margin="16,8,16,0"
Severity="Informational"
IsClosable="False"
Title="{Binding RestartTitle}"
Message="{Binding RestartMessage}">
<ui:InfoBar.ActionButton>
<Button Click="OnRestartNowClick"
Content="{Binding RestartButtonText}" />
</ui:InfoBar.ActionButton>
</ui:InfoBar>
<ui:NavigationView x:Name="RootNavigationView"
Grid.Row="2"
Grid.Row="1"
Margin="0,8,0,0"
Background="Transparent"
PaneDisplayMode="Auto"

View File

@@ -33,6 +33,7 @@ public partial class SettingsWindow : Window, ISettingsPageHostContext
private readonly Dictionary<string, Control> _cachedPages = new(StringComparer.OrdinalIgnoreCase);
private readonly bool _useSystemChrome;
private bool _isResponsiveRefreshPending;
private bool _isRestartPromptVisible;
public SettingsWindow()
: this(
@@ -129,6 +130,8 @@ public partial class SettingsWindow : Window, ISettingsPageHostContext
? ViewModel.GetDefaultRestartMessage()
: reason;
ViewModel.IsRestartRequested = true;
PendingRestartStateService.SetPending(PendingRestartStateService.SettingsWindowReason, true);
ShowRestartPrompt();
}
public void ApplyChromeMode(bool useSystemChrome)
@@ -269,14 +272,11 @@ public partial class SettingsWindow : Window, ISettingsPageHostContext
}
}
private async void OnRestartNowClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
private void OnRestartNowClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
_ = sender;
_ = e;
_hostApplicationLifecycle.TryRestart(new HostApplicationLifecycleRequest(
Source: "SettingsWindow",
Reason: "User accepted restart from settings window."));
await Task.CompletedTask;
ShowRestartPrompt();
}
private void OnCloseDrawerClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
@@ -306,6 +306,58 @@ public partial class SettingsWindow : Window, ISettingsPageHostContext
ViewModel.IsRestartRequested = ViewModel.IsRestartRequested || PendingRestartStateService.HasPendingRestart;
}
private void ShowRestartPrompt()
{
void ShowPrompt()
{
UiExceptionGuard.FireAndForgetGuarded(
ShowRestartPromptCoreAsync,
"SettingsWindow.ShowRestartPrompt");
}
if (Dispatcher.UIThread.CheckAccess())
{
ShowPrompt();
return;
}
Dispatcher.UIThread.Post(ShowPrompt, DispatcherPriority.Send);
}
private async Task ShowRestartPromptCoreAsync()
{
if (_isRestartPromptVisible)
{
return;
}
_isRestartPromptVisible = true;
try
{
var dialog = new ContentDialog
{
Title = ViewModel.RestartDialogTitle,
Content = ViewModel.RestartMessage,
PrimaryButtonText = ViewModel.RestartDialogPrimaryText,
CloseButtonText = ViewModel.RestartDialogCloseText,
DefaultButton = ContentDialogButton.Primary
};
var result = await dialog.ShowAsync(this);
if (result == ContentDialogResult.Primary)
{
_hostApplicationLifecycle.TryRestart(new HostApplicationLifecycleRequest(
Source: "SettingsWindow",
Reason: "User accepted restart from settings window."));
}
}
finally
{
_isRestartPromptVisible = false;
}
}
private void OnOpened(object? sender, EventArgs e)
{
_ = sender;
@@ -642,6 +694,7 @@ public partial class SettingsWindow : Window, ISettingsPageHostContext
"GridDots" => Symbol.GridDots,
"PuzzlePiece" => Symbol.PuzzlePiece,
"ShoppingBag" => Symbol.ShoppingBag,
"Shield" => Symbol.ShieldDismiss,
"Info" => Symbol.Info,
"ArrowSync" => Symbol.ArrowSync,
_ => Symbol.Settings

View File

@@ -327,6 +327,7 @@ public sealed class PluginLoader
RegisterHostService<ISettingsFacadeService>(services, hostServices);
RegisterHostService<ISettingsService>(services, hostServices);
RegisterHostService<ISettingsCatalog>(services, hostServices);
RegisterHostService<IAppearanceThemeService>(services, hostServices);
return services;
}

View File

@@ -835,10 +835,11 @@ public sealed class PluginRuntimeService : IDisposable
{
private readonly IPluginPackageManager _packageManager;
private readonly IHostApplicationLifecycle _applicationLifecycle;
private readonly IPluginExportRegistry _exportRegistry;
private readonly ISettingsFacadeService _settingsFacade;
private readonly ISettingsService _settingsService;
private readonly ISettingsCatalog _settingsCatalog;
private readonly IPluginExportRegistry _exportRegistry;
private readonly ISettingsFacadeService _settingsFacade;
private readonly ISettingsService _settingsService;
private readonly ISettingsCatalog _settingsCatalog;
private readonly IAppearanceThemeService _appearanceThemeService;
public PluginHostServiceProvider(
IPluginPackageManager packageManager,
@@ -854,6 +855,7 @@ public sealed class PluginRuntimeService : IDisposable
_settingsFacade = settingsFacade;
_settingsService = settingsService;
_settingsCatalog = settingsCatalog;
_appearanceThemeService = HostAppearanceThemeProvider.GetOrCreate();
}
public object? GetService(Type serviceType)
@@ -888,6 +890,11 @@ public sealed class PluginRuntimeService : IDisposable
return _settingsCatalog;
}
if (serviceType == typeof(IAppearanceThemeService))
{
return _appearanceThemeService;
}
return null;
}
}