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

View File

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

View File

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

View File

@@ -54,6 +54,7 @@
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.0" /> <PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.0" />
<PackageReference Include="LibVLCSharp.Avalonia" Version="3.9.5" /> <PackageReference Include="LibVLCSharp.Avalonia" Version="3.9.5" />
<PackageReference Include="PortAudioSharp2" Version="1.0.6" /> <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.Runtime.WindowsRuntime" Version="4.7.0" />
<PackageReference Include="System.Drawing.Common" Version="10.0.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'" /> <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.weather": "Weather",
"settings.nav.region": "Region", "settings.nav.region": "Region",
"settings.nav.update": "Update", "settings.nav.update": "Update",
"settings.nav.privacy": "Privacy",
"settings.nav.launcher": "App Launcher", "settings.nav.launcher": "App Launcher",
"settings.nav.plugins": "Plugins", "settings.nav.plugins": "Plugins",
"settings.nav.about": "About", "settings.nav.about": "About",
@@ -92,6 +93,12 @@
"settings.status_bar.spacing_mode_custom": "Custom", "settings.status_bar.spacing_mode_custom": "Custom",
"settings.status_bar.spacing_custom_label": "Custom spacing (%)", "settings.status_bar.spacing_custom_label": "Custom spacing (%)",
"settings.status_bar.spacing_custom_px_format": "≈ {0:F1}px", "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.title": "Weather",
"settings.weather.description": "Configure weather location, Xiaomi weather preview, and startup positioning behavior.", "settings.weather.description": "Configure weather location, Xiaomi weather preview, and startup positioning behavior.",
"settings.weather.location_source_header": "Location Source", "settings.weather.location_source_header": "Location Source",
@@ -242,11 +249,38 @@
"settings.general.preview_date_label": "Date", "settings.general.preview_date_label": "Date",
"settings.general.render_mode_restart_message": "Rendering mode changes require restarting the app.", "settings.general.render_mode_restart_message": "Rendering mode changes require restarting the app.",
"settings.appearance.title": "Appearance", "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.appearance.theme_header": "Theme",
"settings.color.enable_night_mode_toggle": "Enable night mode", "settings.color.enable_night_mode_toggle": "Enable night mode",
"settings.color.use_system_chrome_toggle": "Use system window chrome", "settings.color.use_system_chrome_toggle": "Use system window chrome",
"settings.color.theme_color_label": "Theme accent color", "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.fill": "Fill",
"settings.wallpaper.placement.fit": "Fit", "settings.wallpaper.placement.fit": "Fit",
"settings.wallpaper.placement.stretch": "Stretch", "settings.wallpaper.placement.stretch": "Stretch",
@@ -345,6 +379,7 @@
"settings.restart_dialog.title": "Restart required", "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.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.restart": "Restart now",
"settings.restart_dialog.later": "Later",
"settings.restart_dialog.cancel": "Cancel", "settings.restart_dialog.cancel": "Cancel",
"settings.restart_dock.title": "Restart required", "settings.restart_dock.title": "Restart required",
"settings.restart_dock.description": "Some changes will take effect after restarting the app.", "settings.restart_dock.description": "Some changes will take effect after restarting the app.",

View File

@@ -26,6 +26,7 @@
"settings.nav.weather": "天气", "settings.nav.weather": "天气",
"settings.nav.region": "地区", "settings.nav.region": "地区",
"settings.nav.update": "更新", "settings.nav.update": "更新",
"settings.nav.privacy": "隐私",
"settings.nav.launcher": "应用启动台", "settings.nav.launcher": "应用启动台",
"settings.nav.plugins": "插件", "settings.nav.plugins": "插件",
"settings.nav.about": "关于", "settings.nav.about": "关于",
@@ -97,6 +98,12 @@
"settings.status_bar.spacing_mode_custom": "自定义", "settings.status_bar.spacing_mode_custom": "自定义",
"settings.status_bar.spacing_custom_label": "自定义间距(%", "settings.status_bar.spacing_custom_label": "自定义间距(%",
"settings.status_bar.spacing_custom_px_format": "≈ {0:F1}px", "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.title": "天气",
"settings.weather.description": "配置天气位置、小米天气预览和启动时的位置刷新行为。", "settings.weather.description": "配置天气位置、小米天气预览和启动时的位置刷新行为。",
"settings.weather.location_source_header": "位置来源", "settings.weather.location_source_header": "位置来源",
@@ -247,11 +254,38 @@
"settings.general.preview_date_label": "日期", "settings.general.preview_date_label": "日期",
"settings.general.render_mode_restart_message": "渲染模式变更需要重启应用。", "settings.general.render_mode_restart_message": "渲染模式变更需要重启应用。",
"settings.appearance.title": "外观", "settings.appearance.title": "外观",
"settings.appearance.description": "调整主题、壁纸与窗口外观。", "settings.appearance.description": "调整主题来源、系统材质与窗口外观。",
"settings.appearance.theme_header": "主题", "settings.appearance.theme_header": "主题",
"settings.color.enable_night_mode_toggle": "启用夜间模式", "settings.color.enable_night_mode_toggle": "启用夜间模式",
"settings.color.use_system_chrome_toggle": "使用系统窗口标题栏", "settings.color.use_system_chrome_toggle": "使用系统窗口标题栏",
"settings.color.theme_color_label": "主题强调色", "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.fill": "填充",
"settings.wallpaper.placement.fit": "适应", "settings.wallpaper.placement.fit": "适应",
"settings.wallpaper.placement.stretch": "拉伸", "settings.wallpaper.placement.stretch": "拉伸",
@@ -350,6 +384,7 @@
"settings.restart_dialog.title": "需要重启应用", "settings.restart_dialog.title": "需要重启应用",
"settings.restart_dialog.render_mode_message": "需要重启应用,才能将渲染模式从“{0}”切换到“{1}”。是否现在重启?", "settings.restart_dialog.render_mode_message": "需要重启应用,才能将渲染模式从“{0}”切换到“{1}”。是否现在重启?",
"settings.restart_dialog.restart": "立即重启", "settings.restart_dialog.restart": "立即重启",
"settings.restart_dialog.later": "稍后",
"settings.restart_dialog.cancel": "取消", "settings.restart_dialog.cancel": "取消",
"settings.restart_dock.title": "需要重启应用", "settings.restart_dock.title": "需要重启应用",
"settings.restart_dock.description": "部分更改需要在重启应用后才会生效。", "settings.restart_dock.description": "部分更改需要在重启应用后才会生效。",

View File

@@ -16,6 +16,12 @@ public sealed class AppSettingsSnapshot
public bool UseSystemChrome { get; set; } 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? WallpaperPath { get; set; }
public string WallpaperType { get; set; } = "Image"; public string WallpaperType { get; set; } = "Image";
@@ -60,6 +66,10 @@ public sealed class AppSettingsSnapshot
public bool IncludePrereleaseUpdates { get; set; } public bool IncludePrereleaseUpdates { get; set; }
public bool UploadAnonymousCrashData { get; set; }
public bool UploadAnonymousUsageData { get; set; }
public string UpdateChannel { get; set; } = "stable"; public string UpdateChannel { get; set; } = "stable";
public string UpdateMode { get; set; } = "download_then_confirm"; 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; using Avalonia.Media;
namespace LanMountainDesktop.Models; namespace LanMountainDesktop.Models;
public sealed record MonetPalette( public sealed record MonetPalette
IReadOnlyList<Color> RecommendedColors, {
IReadOnlyList<Color> MonetColors); 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); ArgumentNullException.ThrowIfNull(monetPalette);
var isNightMode = themeState.IsNightMode; var isNightMode = themeState.IsNightMode;
var monetColors = monetPalette.MonetColors?.Where(color => color.A > 0).ToArray() ?? [];
var fallbackThemeColor = TryParseColor(themeState.ThemeColor); 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 var primary = useWallpaperPalette
? monetColors[0] ? monetPalette.Primary
: fallbackThemeColor ?? monetColors.FirstOrDefault(DefaultPrimary); : fallbackThemeColor ?? monetPalette.Primary;
var secondary = ResolveSecondaryColor(primary, monetColors, isNightMode); if (primary == default)
var tertiary = ResolveTertiaryColor(primary, secondary, monetColors, isNightMode); {
primary = DefaultPrimary;
}
var secondary = ResolveSecondaryColor(primary, monetPalette, isNightMode);
var tertiary = ResolveTertiaryColor(primary, secondary, monetPalette, isNightMode);
var backgroundBase = isNightMode ? DarkBackgroundBase : LightBackgroundBase; var backgroundBase = isNightMode ? DarkBackgroundBase : LightBackgroundBase;
var surfaceBase = isNightMode ? DarkSurfaceBase : LightSurfaceBase; var surfaceBase = isNightMode ? DarkSurfaceBase : LightSurfaceBase;
@@ -120,11 +124,11 @@ internal static class ComponentEditorMaterialThemeAdapter
onPrimary); 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( return ColorMath.Blend(
@@ -136,12 +140,12 @@ internal static class ComponentEditorMaterialThemeAdapter
private static Color ResolveTertiaryColor( private static Color ResolveTertiaryColor(
Color primary, Color primary,
Color secondary, Color secondary,
IReadOnlyList<Color> monetColors, MonetPalette monetPalette,
bool isNightMode) 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"); var blendTarget = isNightMode ? Color.Parse("#FFFFFFFF") : Color.Parse("#FF2A2230");

View File

@@ -31,13 +31,16 @@ public interface IComponentEditorWindowService
internal sealed class ComponentEditorWindowService : IComponentEditorWindowService internal sealed class ComponentEditorWindowService : IComponentEditorWindowService
{ {
private readonly ISettingsFacadeService _settingsFacade; private readonly ISettingsFacadeService _settingsFacade;
private readonly IAppearanceThemeService _appearanceThemeService;
private ComponentEditorWindow? _window; private ComponentEditorWindow? _window;
private string? _currentPlacementId; private string? _currentPlacementId;
public ComponentEditorWindowService(ISettingsFacadeService settingsFacade) public ComponentEditorWindowService(ISettingsFacadeService settingsFacade)
{ {
_settingsFacade = settingsFacade ?? throw new ArgumentNullException(nameof(settingsFacade)); _settingsFacade = settingsFacade ?? throw new ArgumentNullException(nameof(settingsFacade));
_appearanceThemeService = HostAppearanceThemeProvider.GetOrCreate();
_settingsFacade.Settings.Changed += OnSettingsChanged; _settingsFacade.Settings.Changed += OnSettingsChanged;
_appearanceThemeService.Changed += OnAppearanceThemeChanged;
} }
public bool IsOpen => _window is { IsVisible: true }; public bool IsOpen => _window is { IsVisible: true };
@@ -105,12 +108,15 @@ internal sealed class ComponentEditorWindowService : IComponentEditorWindowServi
} }
var changedKeys = e.ChangedKeys?.ToArray() ?? []; var changedKeys = e.ChangedKeys?.ToArray() ?? [];
var liveAppearance = _appearanceThemeService.GetCurrent();
if (changedKeys.Length > 0 && if (changedKeys.Length > 0 &&
!changedKeys.Contains(nameof(AppSettingsSnapshot.IsNightMode), StringComparer.OrdinalIgnoreCase) && !changedKeys.Contains(nameof(AppSettingsSnapshot.IsNightMode), StringComparer.OrdinalIgnoreCase) &&
!changedKeys.Contains(nameof(AppSettingsSnapshot.ThemeColor), StringComparer.OrdinalIgnoreCase) && !(string.Equals(liveAppearance.ThemeColorMode, ThemeAppearanceValues.ColorModeSeedMonet, StringComparison.OrdinalIgnoreCase) &&
!changedKeys.Contains(nameof(AppSettingsSnapshot.WallpaperPath), StringComparer.OrdinalIgnoreCase) && changedKeys.Contains(nameof(AppSettingsSnapshot.ThemeColor), StringComparer.OrdinalIgnoreCase)) &&
!changedKeys.Contains(nameof(AppSettingsSnapshot.WallpaperType), StringComparer.OrdinalIgnoreCase) && !(string.Equals(liveAppearance.ThemeColorMode, ThemeAppearanceValues.ColorModeWallpaperMonet, StringComparison.OrdinalIgnoreCase) &&
!changedKeys.Contains(nameof(AppSettingsSnapshot.WallpaperColor), 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)) !changedKeys.Contains(nameof(AppSettingsSnapshot.UseSystemChrome), StringComparer.OrdinalIgnoreCase))
{ {
return; return;
@@ -121,21 +127,33 @@ internal sealed class ComponentEditorWindowService : IComponentEditorWindowServi
private void ApplyTheme(ComponentEditorWindow window) private void ApplyTheme(ComponentEditorWindow window)
{ {
var appearanceSnapshot = _appearanceThemeService.GetCurrent();
var themeState = _settingsFacade.Theme.Get(); var themeState = _settingsFacade.Theme.Get();
var wallpaperState = _settingsFacade.Wallpaper.Get(); var wallpaperState = _settingsFacade.Wallpaper.Get();
var wallpaperMediaType = _settingsFacade.WallpaperMedia.DetectMediaType(wallpaperState.WallpaperPath); var wallpaperMediaType = _settingsFacade.WallpaperMedia.DetectMediaType(
var monetPalette = _settingsFacade.Theme.BuildPalette( appearanceSnapshot.ResolvedWallpaperPath ?? wallpaperState.WallpaperPath);
themeState.IsNightMode,
wallpaperState.WallpaperPath,
themeState.ThemeColor);
var palette = ComponentEditorMaterialThemeAdapter.Build( var palette = ComponentEditorMaterialThemeAdapter.Build(
themeState, themeState,
wallpaperState, wallpaperState,
monetPalette, appearanceSnapshot.MonetPalette,
wallpaperMediaType); wallpaperMediaType);
window.ApplyTheme(palette); window.ApplyTheme(palette);
window.ApplyChromeMode(themeState.UseSystemChrome); 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 private sealed class HostContext : IComponentEditorHostContext

View File

@@ -9,57 +9,95 @@ public static class GlassEffectService
{ {
public static void ApplyGlassResources(IResourceDictionary resources, ThemeColorContext context) 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 monetColors = context.MonetColors?.Where(color => color.A > 0).ToArray() ?? [];
var primary = monetColors.Length > 0 ? monetColors[0] : context.AccentColor; var primary = context.UseNeutralSurfaces
var secondary = monetColors.Length > 1 ? context.AccentColor
? monetColors[1] : monetPalette?.Primary ?? (monetColors.Length > 0 ? monetColors[0] : context.AccentColor);
: ColorMath.Blend(primary, Color.Parse("#FFFFFFFF"), 0.12); var neutralButtonBase = context.IsNightMode
? Color.Parse("#FF171C24")
var panelBase = context.IsNightMode : Color.Parse("#FFFFFFFF");
? ColorMath.Blend(Color.Parse("#FF101722"), primary, 0.26) if (!context.UseNeutralSurfaces)
: ColorMath.Blend(Color.Parse("#FFF9FBFE"), primary, 0.14); {
var panelRaised = context.IsNightMode neutralButtonBase = ColorMath.Blend(
? ColorMath.Blend(Color.Parse("#FF15202C"), secondary, 0.30) neutralButtonBase,
: ColorMath.Blend(Color.Parse("#FFFFFFFF"), secondary, 0.18); primary,
var overlayBase = context.IsNightMode context.IsNightMode ? 0.08 : 0.04);
? ColorMath.Blend(Color.Parse("#FF0E1622"), primary, 0.36) }
: ColorMath.Blend(Color.Parse("#FFF3F7FD"), primary, 0.20);
var buttonBackground = Color.FromArgb( var buttonBackground = Color.FromArgb(
context.IsNightMode ? (byte)0x4D : (byte)0x52, context.IsNightMode ? (byte)0xF0 : (byte)0xFF,
panelRaised.R, neutralButtonBase.R,
panelRaised.G, neutralButtonBase.G,
panelRaised.B); neutralButtonBase.B);
var buttonBorder = Color.FromArgb( var buttonBorder = ColorMath.WithAlpha(
context.IsNightMode ? (byte)0x36 : (byte)0x26, context.IsNightMode
primary.R, ? ColorMath.Blend(neutralButtonBase, Color.Parse("#FFFFFFFF"), 0.14)
primary.G, : ColorMath.Blend(neutralButtonBase, Color.Parse("#FF334155"), 0.10),
primary.B); context.IsNightMode ? (byte)0x26 : (byte)0x14);
resources["AdaptiveButtonBackgroundBrush"] = new SolidColorBrush(buttonBackground); resources["AdaptiveButtonBackgroundBrush"] = new SolidColorBrush(buttonBackground);
resources["AdaptiveButtonBorderBrush"] = new SolidColorBrush(buttonBorder); resources["AdaptiveButtonBorderBrush"] = new SolidColorBrush(buttonBorder);
resources["AdaptiveButtonHoverBackgroundBrush"] = new SolidColorBrush( 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( 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( var windowSurface = materialSurfaceService.GetSurface(context, MaterialSurfaceRole.WindowBackground);
Color.FromArgb(context.IsNightMode ? (byte)0xF2 : (byte)0xFA, panelBase.R, panelBase.G, panelBase.B)); var settingsWindowSurface = materialSurfaceService.GetSurface(context, MaterialSurfaceRole.SettingsWindowBackground);
resources["AdaptiveGlassPanelBorderBrush"] = new SolidColorBrush( var dockSurface = materialSurfaceService.GetSurface(context, MaterialSurfaceRole.DockBackground);
Color.FromArgb(context.IsNightMode ? (byte)0x38 : (byte)0x24, primary.R, primary.G, primary.B)); var statusBarSurface = materialSurfaceService.GetSurface(context, MaterialSurfaceRole.StatusBarBackground);
resources["AdaptiveGlassStrongBackgroundBrush"] = new SolidColorBrush( var desktopComponentSurface = materialSurfaceService.GetSurface(context, MaterialSurfaceRole.DesktopComponentHost);
Color.FromArgb(context.IsNightMode ? (byte)0xF6 : (byte)0xFC, panelRaised.R, panelRaised.G, panelRaised.B)); var statusBarComponentSurface = materialSurfaceService.GetSurface(context, MaterialSurfaceRole.StatusBarComponentHost);
resources["AdaptiveGlassStrongBorderBrush"] = new SolidColorBrush( var overlaySurface = materialSurfaceService.GetSurface(context, MaterialSurfaceRole.OverlayPanel);
Color.FromArgb(context.IsNightMode ? (byte)0x4A : (byte)0x2C, secondary.R, secondary.G, secondary.B)); var strongSurfaceColor = ColorMath.Blend(
resources["AdaptiveGlassOverlayBackgroundBrush"] = new SolidColorBrush( desktopComponentSurface.BackgroundColor,
Color.FromArgb(context.IsNightMode ? (byte)0xEA : (byte)0xF4, overlayBase.R, overlayBase.G, overlayBase.B)); 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["AdaptiveWindowBackgroundBrush"] = new SolidColorBrush(windowSurface.BackgroundColor);
resources["AdaptiveGlassStrongBlurRadius"] = context.IsNightMode ? 28.0 : 34.0; resources["AdaptiveWindowBorderBrush"] = new SolidColorBrush(windowSurface.BorderColor);
resources["AdaptiveGlassOverlayBlurRadius"] = context.IsNightMode ? 34.0 : 40.0; 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["AdaptiveGlassPanelOpacity"] = 1.0;
resources["AdaptiveGlassStrongOpacity"] = 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["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.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Avalonia; using Avalonia;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
using Avalonia.Platform; using Avalonia.Platform;
using LanMountainDesktop.Models; using LanMountainDesktop.Models;
using MaterialColorUtilities.Palettes;
using MaterialColorUtilities.Utils;
using Microsoft.Win32; using Microsoft.Win32;
namespace LanMountainDesktop.Services; namespace LanMountainDesktop.Services;
public sealed class MonetColorService public sealed class MonetColorService
{ {
private static readonly Color DefaultSeedColor = Color.Parse("#FF3B82F6");
public MonetPalette BuildPalette(Bitmap? wallpaper, bool nightMode, Color? preferredSeed = null) public MonetPalette BuildPalette(Bitmap? wallpaper, bool nightMode, Color? preferredSeed = null)
{ {
var recommended = BuildRecommendedPalette(nightMode); var wallpaperCandidates = wallpaper is null
var seed = preferredSeed ?? TryExtractSeedColor(wallpaper) ?? TryGetSystemMonetSeedColor() ?? Color.Parse("#FF3B82F6"); ? []
var monet = BuildMonetPalette(seed, nightMode); : ExtractSeedCandidates(wallpaper);
return new MonetPalette(recommended, monet); 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 BuildPaletteCore(seedCandidates ?? [], nightMode, preferredSeed);
{
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")
];
} }
private static IReadOnlyList<Color> BuildMonetPalette(Color seed, bool nightMode) public IReadOnlyList<Color> ExtractSeedCandidates(Bitmap wallpaper)
{ {
var (hue, saturation, value) = ToHsv(seed); ArgumentNullException.ThrowIfNull(wallpaper);
var valueBase = nightMode ? Math.Max(0.70, value) : Math.Min(0.72, Math.Max(0.35, value)); return ExtractWallpaperSeedCandidates(wallpaper);
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;
} }
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; 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 try
{ {
var sampleWidth = Math.Clamp(wallpaper.PixelSize.Width, 1, 48); var width = Math.Clamp(wallpaper.PixelSize.Width, 1, 96);
var sampleHeight = Math.Clamp(wallpaper.PixelSize.Height, 1, 48); var height = Math.Clamp(wallpaper.PixelSize.Height, 1, 96);
using var scaledBitmap = wallpaper.CreateScaledBitmap( using var scaledBitmap = wallpaper.CreateScaledBitmap(
new PixelSize(sampleWidth, sampleHeight), new PixelSize(width, height),
BitmapInterpolationMode.MediumQuality); BitmapInterpolationMode.MediumQuality);
using var writeable = new WriteableBitmap( using var writeable = new WriteableBitmap(
scaledBitmap.PixelSize, scaledBitmap.PixelSize,
@@ -91,55 +93,52 @@ public sealed class MonetColorService
var byteCount = framebuffer.RowBytes * framebuffer.Size.Height; var byteCount = framebuffer.RowBytes * framebuffer.Size.Height;
if (byteCount <= 0 || framebuffer.Address == IntPtr.Zero) if (byteCount <= 0 || framebuffer.Address == IntPtr.Zero)
{ {
return null; return [];
} }
var pixelBuffer = new byte[byteCount]; var pixelBuffer = new byte[byteCount];
Marshal.Copy(framebuffer.Address, pixelBuffer, 0, byteCount); Marshal.Copy(framebuffer.Address, pixelBuffer, 0, byteCount);
double bestScore = double.MinValue; var argbPixels = new List<uint>(framebuffer.Size.Width * framebuffer.Size.Height);
Color? bestColor = null;
for (var y = 0; y < framebuffer.Size.Height; y++) for (var y = 0; y < framebuffer.Size.Height; y++)
{ {
var rowOffset = y * framebuffer.RowBytes; var rowOffset = y * framebuffer.RowBytes;
for (var x = 0; x < framebuffer.Size.Width; x++) for (var x = 0; x < framebuffer.Size.Width; x++)
{ {
var index = rowOffset + (x * 4); var index = rowOffset + (x * 4);
var alpha = pixelBuffer[index + 3] / 255d; var alpha = pixelBuffer[index + 3];
if (alpha <= 0.15) if (alpha <= 32)
{ {
continue; continue;
} }
var blue = (pixelBuffer[index] / 255d) / alpha; var blue = pixelBuffer[index];
var green = (pixelBuffer[index + 1] / 255d) / alpha; var green = pixelBuffer[index + 1];
var red = (pixelBuffer[index + 2] / 255d) / alpha; var red = pixelBuffer[index + 2];
red = Math.Clamp(red, 0, 1); argbPixels.Add(
green = Math.Clamp(green, 0, 1); ((uint)alpha << 24) |
blue = Math.Clamp(blue, 0, 1); ((uint)red << 16) |
((uint)green << 8) |
var color = Color.FromRgb( blue);
(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;
} }
} }
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; return null;
} }
var bytes = BitConverter.GetBytes(accentDword); var accentColor = unchecked((uint)accentDword);
var blue = bytes[0]; var a = (byte)((accentColor >> 24) & 0xFF);
var green = bytes[1]; var b = (byte)((accentColor >> 16) & 0xFF);
var red = bytes[2]; var g = (byte)((accentColor >> 8) & 0xFF);
return Color.FromRgb(red, green, blue); var r = (byte)(accentColor & 0xFF);
if (a == 0)
{
a = 0xFF;
}
return Color.FromArgb(a, r, g, b);
} }
catch 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; return
var green = color.G / 255d; ((uint)color.A << 24) |
var blue = color.B / 255d; ((uint)color.R << 16) |
var max = Math.Max(red, Math.Max(green, blue)); ((uint)color.G << 8) |
var min = Math.Min(red, Math.Min(green, blue)); color.B;
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);
} }
private static Color FromHsv(double hue, double saturation, double value) private static Color FromArgb(uint argb)
{ {
hue = NormalizeHue(hue); var a = (byte)((argb >> 24) & 0xFF);
saturation = Math.Clamp(saturation, 0, 1); var r = (byte)((argb >> 16) & 0xFF);
value = Math.Clamp(value, 0, 1); var g = (byte)((argb >> 8) & 0xFF);
var b = (byte)(argb & 0xFF);
if (saturation <= 0.0001) return Color.FromArgb(a, r, g, b);
{
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);
} }
private static double NormalizeHue(double hue) private static MonetPalette BuildPaletteCore(
IReadOnlyList<Color> wallpaperCandidates,
bool nightMode,
Color? preferredSeed)
{ {
hue %= 360; var recommendedColors = wallpaperCandidates.Count > 0
if (hue < 0) ? wallpaperCandidates
{ : BuildFallbackSeedCandidates();
hue += 360; 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 RenderModeReason = "RenderMode";
public const string PluginCatalogReason = "PluginCatalog"; public const string PluginCatalogReason = "PluginCatalog";
public const string SettingsWindowReason = "SettingsWindow";
private static readonly object Gate = new(); private static readonly object Gate = new();
private static readonly HashSet<string> PendingReasons = new(StringComparer.OrdinalIgnoreCase); private static readonly HashSet<string> PendingReasons = new(StringComparer.OrdinalIgnoreCase);

View File

@@ -4,6 +4,7 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using LanMountainDesktop.Models; using LanMountainDesktop.Models;
using LanMountainDesktop.PluginSdk; using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services;
namespace LanMountainDesktop.Services.Settings; namespace LanMountainDesktop.Services.Settings;
@@ -16,7 +17,13 @@ public enum WallpaperMediaType
public sealed record GridSettingsState(int ShortSideCells, string SpacingPreset, int EdgeInsetPercent); 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 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( public sealed record StatusBarSettingsState(
IReadOnlyList<string> TopStatusComponentIds, IReadOnlyList<string> TopStatusComponentIds,
IReadOnlyList<string> PinnedTaskbarActions, IReadOnlyList<string> PinnedTaskbarActions,
@@ -37,6 +44,9 @@ public sealed record WeatherSettingsState(
bool NoTlsRequests, bool NoTlsRequests,
string LocationQuery); string LocationQuery);
public sealed record RegionSettingsState(string LanguageCode, string? TimeZoneId); public sealed record RegionSettingsState(string LanguageCode, string? TimeZoneId);
public sealed record PrivacySettingsState(
bool UploadAnonymousCrashData,
bool UploadAnonymousUsageData);
public sealed record UpdateSettingsState( public sealed record UpdateSettingsState(
bool AutoCheckUpdates, bool AutoCheckUpdates,
bool IncludePrereleaseUpdates, bool IncludePrereleaseUpdates,
@@ -166,6 +176,12 @@ public interface IRegionSettingsService
TimeZoneService GetTimeZoneService(); TimeZoneService GetTimeZoneService();
} }
public interface IPrivacySettingsService
{
PrivacySettingsState Get();
void Save(PrivacySettingsState state);
}
public interface IUpdateSettingsService public interface IUpdateSettingsService
{ {
UpdateSettingsState Get(); UpdateSettingsState Get();
@@ -224,6 +240,7 @@ public interface ISettingsFacadeService
IStatusBarSettingsService StatusBar { get; } IStatusBarSettingsService StatusBar { get; }
IWeatherSettingsService Weather { get; } IWeatherSettingsService Weather { get; }
IRegionSettingsService Region { get; } IRegionSettingsService Region { get; }
IPrivacySettingsService Privacy { get; }
IUpdateSettingsService Update { get; } IUpdateSettingsService Update { get; }
ILauncherCatalogService LauncherCatalog { get; } ILauncherCatalogService LauncherCatalog { get; }
ILauncherPolicyService LauncherPolicy { get; } ILauncherPolicyService LauncherPolicy { get; }

View File

@@ -93,9 +93,12 @@ internal sealed class WallpaperSettingsService : IWallpaperSettingsService
public WallpaperSettingsState Get() public WallpaperSettingsState Get()
{ {
var snapshot = _settingsService.Load(); var snapshot = _settingsService.Load();
var normalizedType = snapshot.WallpaperType ?? "Image";
return new WallpaperSettingsState( return new WallpaperSettingsState(
snapshot.WallpaperPath, string.Equals(normalizedType, "SolidColor", StringComparison.OrdinalIgnoreCase)
snapshot.WallpaperType ?? "Image", ? null
: snapshot.WallpaperPath,
normalizedType,
snapshot.WallpaperColor, snapshot.WallpaperColor,
snapshot.WallpaperPlacement); snapshot.WallpaperPlacement);
} }
@@ -103,9 +106,24 @@ internal sealed class WallpaperSettingsService : IWallpaperSettingsService
public void Save(WallpaperSettingsState state) public void Save(WallpaperSettingsState state)
{ {
var snapshot = _settingsService.Load(); var snapshot = _settingsService.Load();
snapshot.WallpaperPath = state.WallpaperPath; var normalizedType = string.IsNullOrWhiteSpace(state.Type)
snapshot.WallpaperType = state.Type; ? "Image"
snapshot.WallpaperColor = state.Color; : 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) snapshot.WallpaperPlacement = string.IsNullOrWhiteSpace(state.Placement)
? "Fill" ? "Fill"
: state.Placement.Trim(); : state.Placement.Trim();
@@ -233,24 +251,68 @@ internal sealed class ThemeAppearanceService : IThemeAppearanceService
return new ThemeAppearanceSettingsState( return new ThemeAppearanceSettingsState(
snapshot.IsNightMode ?? false, snapshot.IsNightMode ?? false,
snapshot.ThemeColor, snapshot.ThemeColor,
snapshot.UseSystemChrome); snapshot.UseSystemChrome,
ThemeAppearanceValues.NormalizeThemeColorMode(snapshot.ThemeColorMode, snapshot.ThemeColor),
ThemeAppearanceValues.NormalizeSystemMaterialMode(snapshot.SystemMaterialMode),
snapshot.SelectedWallpaperSeed);
} }
public void Save(ThemeAppearanceSettingsState state) public void Save(ThemeAppearanceSettingsState state)
{ {
var snapshot = _settingsService.Load(); var snapshot = _settingsService.Load();
snapshot.IsNightMode = state.IsNightMode; var changedKeys = new List<string>();
snapshot.ThemeColor = state.ThemeColor; var normalizedThemeColor = string.IsNullOrWhiteSpace(state.ThemeColor) ? null : state.ThemeColor;
snapshot.UseSystemChrome = state.UseSystemChrome; 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( _settingsService.SaveSnapshot(
SettingsScope.App, SettingsScope.App,
snapshot, snapshot,
changedKeys: changedKeys: changedKeys);
[
nameof(AppSettingsSnapshot.IsNightMode),
nameof(AppSettingsSnapshot.ThemeColor),
nameof(AppSettingsSnapshot.UseSystemChrome)
]);
} }
public MonetPalette BuildPalette(bool nightMode, string? wallpaperPath, string? preferredSeedColor = null) 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 internal sealed class UpdateSettingsService : IUpdateSettingsService, IDisposable
{ {
private readonly ISettingsService _settingsService; private readonly ISettingsService _settingsService;
@@ -920,6 +1015,7 @@ internal sealed class SettingsFacadeService : ISettingsFacadeService, IDisposabl
_weatherSettingsService = new WeatherSettingsService(Settings); _weatherSettingsService = new WeatherSettingsService(Settings);
Weather = _weatherSettingsService; Weather = _weatherSettingsService;
Region = new RegionSettingsService(Settings); Region = new RegionSettingsService(Settings);
Privacy = new PrivacySettingsService(Settings);
_updateSettingsService = new UpdateSettingsService(Settings); _updateSettingsService = new UpdateSettingsService(Settings);
Update = _updateSettingsService; Update = _updateSettingsService;
LauncherCatalog = new LauncherCatalogService(); LauncherCatalog = new LauncherCatalogService();
@@ -949,6 +1045,8 @@ internal sealed class SettingsFacadeService : ISettingsFacadeService, IDisposabl
public IRegionSettingsService Region { get; } public IRegionSettingsService Region { get; }
public IPrivacySettingsService Privacy { get; }
public IUpdateSettingsService Update { get; } public IUpdateSettingsService Update { get; }
public ILauncherCatalogService LauncherCatalog { get; } public ILauncherCatalogService LauncherCatalog { get; }

View File

@@ -177,6 +177,7 @@ internal sealed class SettingsPageRegistry : ISettingsPageRegistry, IDisposable
services.AddSingleton(_settingsFacade); services.AddSingleton(_settingsFacade);
services.AddSingleton(_settingsFacade.Settings); services.AddSingleton(_settingsFacade.Settings);
services.AddSingleton(_settingsFacade.Catalog); services.AddSingleton(_settingsFacade.Catalog);
services.AddSingleton<IAppearanceThemeService>(_ => HostAppearanceThemeProvider.GetOrCreate());
services.AddSingleton(_hostApplicationLifecycle); services.AddSingleton(_hostApplicationLifecycle);
services.AddSingleton(_localizationService); services.AddSingleton(_localizationService);
services.AddSingleton<ILocationService>(_ => HostLocationServiceProvider.GetOrCreate()); services.AddSingleton<ILocationService>(_ => HostLocationServiceProvider.GetOrCreate());

View File

@@ -52,10 +52,10 @@ public interface ISettingsWindowService
internal sealed class SettingsWindowService : ISettingsWindowService internal sealed class SettingsWindowService : ISettingsWindowService
{ {
private static readonly Color DefaultAccentColor = Color.Parse("#FF3B82F6");
private readonly ISettingsPageRegistry _pageRegistry; private readonly ISettingsPageRegistry _pageRegistry;
private readonly IHostApplicationLifecycle _hostApplicationLifecycle; private readonly IHostApplicationLifecycle _hostApplicationLifecycle;
private readonly ISettingsFacadeService _settingsFacade; private readonly ISettingsFacadeService _settingsFacade;
private readonly IAppearanceThemeService _appearanceThemeService;
private readonly LocalizationService _localizationService; private readonly LocalizationService _localizationService;
private SettingsWindowViewModel _viewModel = null!; private SettingsWindowViewModel _viewModel = null!;
private SettingsWindow? _window; private SettingsWindow? _window;
@@ -68,8 +68,10 @@ internal sealed class SettingsWindowService : ISettingsWindowService
_pageRegistry = pageRegistry; _pageRegistry = pageRegistry;
_hostApplicationLifecycle = hostApplicationLifecycle; _hostApplicationLifecycle = hostApplicationLifecycle;
_settingsFacade = settingsFacade; _settingsFacade = settingsFacade;
_appearanceThemeService = HostAppearanceThemeProvider.GetOrCreate();
_localizationService = new(); _localizationService = new();
_settingsFacade.Settings.Changed += OnSettingsChanged; _settingsFacade.Settings.Changed += OnSettingsChanged;
_appearanceThemeService.Changed += OnAppearanceThemeChanged;
} }
private string L(string key) private string L(string key)
@@ -86,9 +88,9 @@ internal sealed class SettingsWindowService : ISettingsWindowService
{ {
_pageRegistry.Rebuild(); _pageRegistry.Rebuild();
_window ??= CreateWindow(); _window ??= CreateWindow();
var themeState = _settingsFacade.Theme.Get(); var appearanceSnapshot = _appearanceThemeService.GetCurrent();
_window.ApplyChromeMode(themeState.UseSystemChrome); _window.ApplyChromeMode(appearanceSnapshot.UseSystemChrome);
ApplyTheme(_window, themeState); ApplyTheme(_window);
_window.ReloadPages(request.PageId); _window.ReloadPages(request.PageId);
PositionWindow(_window, request); PositionWindow(_window, request);
@@ -135,15 +137,15 @@ internal sealed class SettingsWindowService : ISettingsWindowService
_viewModel = new SettingsWindowViewModel(_localizationService, languageCode).Initialize(); _viewModel = new SettingsWindowViewModel(_localizationService, languageCode).Initialize();
var themeState = _settingsFacade.Theme.Get(); var appearanceSnapshot = _appearanceThemeService.GetCurrent();
var useSystemChrome = themeState.UseSystemChrome; var useSystemChrome = appearanceSnapshot.UseSystemChrome;
var window = new SettingsWindow( var window = new SettingsWindow(
_viewModel, _viewModel,
_pageRegistry, _pageRegistry,
_hostApplicationLifecycle, _hostApplicationLifecycle,
useSystemChrome); useSystemChrome);
ApplyTheme(window, themeState); ApplyTheme(window);
window.ShowInTaskbar = false; window.ShowInTaskbar = false;
window.Closed += (_, _) => window.Closed += (_, _) =>
{ {
@@ -277,13 +279,16 @@ internal sealed class SettingsWindowService : ISettingsWindowService
var changedKeys = e.ChangedKeys?.ToArray(); var changedKeys = e.ChangedKeys?.ToArray();
var refreshAll = changedKeys is null || changedKeys.Length == 0; var refreshAll = changedKeys is null || changedKeys.Length == 0;
var languageChanged = refreshAll || changedKeys.Contains(nameof(AppSettingsSnapshot.LanguageCode), StringComparer.OrdinalIgnoreCase); var languageChanged = refreshAll || changedKeys.Contains(nameof(AppSettingsSnapshot.LanguageCode), StringComparer.OrdinalIgnoreCase);
var liveAppearance = _appearanceThemeService.GetCurrent();
var themeChanged = var themeChanged =
refreshAll || refreshAll ||
changedKeys.Contains(nameof(AppSettingsSnapshot.IsNightMode), StringComparer.OrdinalIgnoreCase) || changedKeys.Contains(nameof(AppSettingsSnapshot.IsNightMode), StringComparer.OrdinalIgnoreCase) ||
changedKeys.Contains(nameof(AppSettingsSnapshot.ThemeColor), StringComparer.OrdinalIgnoreCase) || (string.Equals(liveAppearance.ThemeColorMode, ThemeAppearanceValues.ColorModeSeedMonet, StringComparison.OrdinalIgnoreCase) &&
changedKeys.Contains(nameof(AppSettingsSnapshot.WallpaperPath), StringComparer.OrdinalIgnoreCase) || changedKeys.Contains(nameof(AppSettingsSnapshot.ThemeColor), StringComparer.OrdinalIgnoreCase)) ||
changedKeys.Contains(nameof(AppSettingsSnapshot.WallpaperType), StringComparer.OrdinalIgnoreCase) || (string.Equals(liveAppearance.ThemeColorMode, ThemeAppearanceValues.ColorModeWallpaperMonet, StringComparison.OrdinalIgnoreCase) &&
changedKeys.Contains(nameof(AppSettingsSnapshot.WallpaperColor), 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); changedKeys.Contains(nameof(AppSettingsSnapshot.UseSystemChrome), StringComparer.OrdinalIgnoreCase);
if (languageChanged) if (languageChanged)
@@ -297,59 +302,36 @@ internal sealed class SettingsWindowService : ISettingsWindowService
if (themeChanged) if (themeChanged)
{ {
var themeState = _settingsFacade.Theme.Get(); var appearanceSnapshot = _appearanceThemeService.GetCurrent();
_window.ApplyChromeMode(themeState.UseSystemChrome); _window.ApplyChromeMode(appearanceSnapshot.UseSystemChrome);
ApplyTheme(_window, themeState); ApplyTheme(_window);
} }
}, DispatcherPriority.Background); }, 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.Dark
: ThemeVariant.Light; : ThemeVariant.Light;
_appearanceThemeService.ApplyThemeResources(window.Resources);
var settingsFacade = HostSettingsFacadeProvider.GetOrCreate(); _appearanceThemeService.ApplyWindowMaterial(window, MaterialSurfaceRole.SettingsWindowBackground);
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);
} }
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]; if (_window is null || _viewModel is null)
}
return TryParseThemeColor(colorText);
}
private static Color TryParseThemeColor(string? colorText)
{
if (!string.IsNullOrWhiteSpace(colorText))
{
try
{ {
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 System.Linq;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Media; using Avalonia.Media;
@@ -68,11 +69,19 @@ public static class ThemeColorSystemService
private static AppThemePalette BuildPalette(ThemeColorContext context) private static AppThemePalette BuildPalette(ThemeColorContext context)
{ {
var monetPalette = context.MonetPalette;
var monetColors = context.MonetColors?.Where(color => color.A > 0).ToArray() ?? []; var monetColors = context.MonetColors?.Where(color => color.A > 0).ToArray() ?? [];
var accent = monetColors.Length > 0 ? monetColors[0] : context.AccentColor; var accent = context.UseNeutralSurfaces
var secondarySeed = monetColors.Length > 1 ? context.AccentColor
? monetColors[1] : monetPalette?.Primary ?? GetColorOrDefault(monetColors, 0, context.AccentColor);
: ColorMath.Blend(accent, Color.Parse("#FFFFFFFF"), 0.14); 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 accentLight1 = ColorMath.Blend(accent, Color.Parse("#FFFFFFFF"), 0.22);
var accentLight2 = ColorMath.Blend(accent, Color.Parse("#FFFFFFFF"), 0.38); 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 primary = context.IsNightMode ? accentLight1 : accentDark1;
var secondary = context.IsNightMode var secondary = context.IsNightMode
? ColorMath.Blend(secondarySeed, Color.Parse("#FFFFFFFF"), 0.16) ? ColorMath.Blend(secondarySeed, Color.Parse("#FFFFFFFF"), 0.12)
: ColorMath.Blend(secondarySeed, Color.Parse("#FF111827"), 0.14); : ColorMath.Blend(secondarySeed, Color.Parse("#FF111827"), 0.10);
var surfaceBase = context.IsNightMode var baseSurface = context.IsNightMode ? Color.Parse("#FF0B0F14") : Color.Parse("#FFF7F8FA");
? ColorMath.Blend(Color.Parse("#FF0A1018"), accent, 0.18) var raisedSurface = context.IsNightMode ? Color.Parse("#FF131922") : Color.Parse("#FFFFFFFF");
: ColorMath.Blend(Color.Parse("#FFF7F9FD"), accent, 0.09); var overlaySurface = context.IsNightMode ? Color.Parse("#FF171E28") : Color.Parse("#FFF1F4F8");
var surfaceRaised = context.IsNightMode var navSurfaceBase = context.IsLightNavBackground ? Color.Parse("#FFF8FAFC") : Color.Parse("#FF111827");
? ColorMath.Blend(Color.Parse("#FF121A24"), secondarySeed, 0.24)
: ColorMath.Blend(Color.Parse("#FFFCFEFF"), secondarySeed, 0.12); var surfaceBase = context.UseNeutralSurfaces
var surfaceOverlayBase = context.IsNightMode ? baseSurface
? ColorMath.Blend(Color.Parse("#FF18212D"), accent, 0.28) : ColorMath.Blend(baseSurface, neutralSeed, context.IsNightMode ? 0.84 : 0.78);
: ColorMath.Blend(Color.Parse("#FFF1F5FB"), accent, 0.16); 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( var surfaceOverlay = Color.FromArgb(
context.IsNightMode ? (byte)0xE8 : (byte)0xF2, context.IsNightMode ? (byte)0xE8 : (byte)0xF2,
surfaceOverlayBase.R, 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("#FF0B1220"), 0.20), surfaceRaised, WcagNormalTextContrast)
: ColorMath.EnsureContrast(ColorMath.Blend(accent, Color.Parse("#FFFFFFFF"), 0.16), surfaceRaised, WcagNormalTextContrast); : ColorMath.EnsureContrast(ColorMath.Blend(accent, Color.Parse("#FFFFFFFF"), 0.16), surfaceRaised, WcagNormalTextContrast);
var navSurface = context.IsLightNavBackground var navSurface = context.UseNeutralSurfaces
? ColorMath.Blend(surfaceRaised, accentLight2, 0.08) ? navSurfaceBase
: ColorMath.Blend(Color.Parse("#FF111827"), accentDark2, 0.24); : ColorMath.Blend(navSurfaceBase, neutralSeed, context.IsNightMode ? 0.66 : 0.70);
var navText = ColorMath.EnsureContrast( var navText = ColorMath.EnsureContrast(
context.IsLightNavBackground ? Color.Parse("#FF0B1220") : Color.Parse("#FFF8FAFC"), context.IsLightNavBackground ? Color.Parse("#FF0B1220") : Color.Parse("#FFF8FAFC"),
navSurface, navSurface,
@@ -129,18 +143,23 @@ public static class ThemeColorSystemService
? Color.FromArgb(0x33, surfaceRaised.R, surfaceRaised.G, surfaceRaised.B) ? Color.FromArgb(0x33, surfaceRaised.R, surfaceRaised.G, surfaceRaised.B)
: Color.FromArgb(0x38, navSurface.R, navSurface.G, navSurface.B); : Color.FromArgb(0x38, navSurface.R, navSurface.G, navSurface.B);
var navItemHoverBackground = context.IsLightNavBackground var navItemHoverBackground = context.IsLightNavBackground
? ColorMath.WithAlpha(ColorMath.Blend(accentLight2, surfaceRaised, 0.30), 0x7A) ? ColorMath.WithAlpha(ColorMath.Blend(accentLight2, navSurface, 0.12), 0x64)
: ColorMath.WithAlpha(ColorMath.Blend(accentDark1, navSurface, 0.26), 0x88); : ColorMath.WithAlpha(ColorMath.Blend(accentDark1, navSurface, 0.18), 0x74);
var navItemSelectedBackground = ColorMath.WithAlpha(accent, context.IsNightMode ? (byte)0xCE : (byte)0xD9); var navItemSelectedBackground = context.UseNeutralSurfaces
var navSelectionIndicator = ColorMath.EnsureContrast(accentLight1, navSurface, WcagLargeTextContrast); ? 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 toggleOn = context.IsNightMode ? accent : accentDark1;
var toggleOff = context.IsNightMode var toggleOff = context.IsNightMode
? Color.FromArgb(0x88, accentDark2.R, accentDark2.G, accentDark2.B) ? Color.FromArgb(0x88, neutralVariantSeed.R, neutralVariantSeed.G, neutralVariantSeed.B)
: Color.FromArgb(0x88, accentLight2.R, accentLight2.G, accentLight2.B); : Color.FromArgb(0x88, neutralVariantSeed.R, neutralVariantSeed.G, neutralVariantSeed.B);
var toggleBorder = context.IsNightMode var toggleBorder = context.IsNightMode
? ColorMath.WithAlpha(ColorMath.Blend(accentLight2, Color.Parse("#FFF8FAFC"), 0.28), 0x8C) ? ColorMath.WithAlpha(ColorMath.Blend(neutralVariantSeed, Color.Parse("#FFF8FAFC"), 0.20), 0x8C)
: ColorMath.WithAlpha(ColorMath.Blend(accentDark2, Color.Parse("#FF334155"), 0.26), 0x78); : ColorMath.WithAlpha(ColorMath.Blend(tertiarySeed, Color.Parse("#FF334155"), 0.18), 0x78);
var onAccent = ColorMath.EnsureContrast(Color.Parse("#FFFFFFFF"), accent, WcagNormalTextContrast); var onAccent = ColorMath.EnsureContrast(Color.Parse("#FFFFFFFF"), accent, WcagNormalTextContrast);
return new AppThemePalette( return new AppThemePalette(
@@ -171,4 +190,11 @@ public static class ThemeColorSystemService
toggleOff, toggleOff,
toggleBorder); 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 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 XiaomiWeatherApiOptions _options;
private readonly HttpClient _httpClient; private readonly HttpClient _httpClient;
private readonly bool _ownsHttpClient; private readonly bool _ownsHttpClient;
@@ -412,9 +376,7 @@ public sealed class XiaomiWeatherService : IWeatherDataService, IDisposable
TryGetNode(payload, "hourly") ?? TryGetNode(payload, "hourly") ??
TryGetNode(payload, "hourlyForecast"); TryGetNode(payload, "hourlyForecast");
var weatherCode = ReadInt(currentNode, "weather", "value") ?? var weatherCode = ReadWeatherCode(currentNode);
ReadInt(currentNode, "weatherCode") ??
ReadInt(currentNode, "code");
var weatherText = ReadString(currentNode, "weather", "desc") ?? var weatherText = ReadString(currentNode, "weather", "desc") ??
ReadString(currentNode, "weather", "text") ?? ReadString(currentNode, "weather", "text") ??
@@ -497,8 +459,12 @@ public sealed class XiaomiWeatherService : IWeatherDataService, IDisposable
var sunItem = GetArrayItem(sunArray, i); var sunItem = GetArrayItem(sunArray, i);
var precipitationItem = GetArrayItem(precipitationArray, i); var precipitationItem = GetArrayItem(precipitationArray, i);
var dayCode = ReadInt(weatherItem, "from") ?? ReadInt(weatherItem, "day"); var dayCode = ReadInt(weatherItem, "from") ??
var nightCode = ReadInt(weatherItem, "to") ?? ReadInt(weatherItem, "night"); ReadInt(weatherItem, "day") ??
ReadWeatherCode(weatherItem);
var nightCode = ReadInt(weatherItem, "to") ??
ReadInt(weatherItem, "night") ??
ReadWeatherCode(weatherItem);
var dayText = ResolveWeatherDescription(dayCode, locale); var dayText = ResolveWeatherDescription(dayCode, locale);
var nightText = ResolveWeatherDescription(nightCode, locale); var nightText = ResolveWeatherDescription(nightCode, locale);
@@ -591,11 +557,7 @@ public sealed class XiaomiWeatherService : IWeatherDataService, IDisposable
continue; continue;
} }
var code = ReadInt(weatherItem, "value") ?? var code = ReadInt(weatherItem, "from") ?? ReadWeatherCode(weatherItem);
ReadInt(weatherItem, "code") ??
ReadInt(weatherItem, "weatherCode") ??
ReadInt(weatherItem, "from") ??
ReadInt(weatherItem);
var weatherText = ReadString(weatherItem, "text") ?? var weatherText = ReadString(weatherItem, "text") ??
ReadString(weatherItem, "desc") ?? ReadString(weatherItem, "desc") ??
ResolveWeatherDescription(code, locale); ResolveWeatherDescription(code, locale);
@@ -640,11 +602,7 @@ public sealed class XiaomiWeatherService : IWeatherDataService, IDisposable
continue; continue;
} }
var code = ReadInt(item, "weatherCode") ?? var code = ReadInt(item, "from") ?? ReadWeatherCode(item);
ReadInt(item, "code") ??
ReadInt(item, "weather", "value") ??
ReadInt(item, "weather") ??
ReadInt(item, "from");
var weatherText = ReadString(item, "weatherText") ?? var weatherText = ReadString(item, "weatherText") ??
ReadString(item, "weather", "desc") ?? ReadString(item, "weather", "desc") ??
ReadString(item, "weather", "text") ?? ReadString(item, "weather", "text") ??
@@ -903,6 +861,16 @@ public sealed class XiaomiWeatherService : IWeatherDataService, IDisposable
return null; 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) private static double? ReadDouble(JsonElement? node, params string[] path)
{ {
if (!node.HasValue) if (!node.HasValue)
@@ -995,19 +963,7 @@ public sealed class XiaomiWeatherService : IWeatherDataService, IDisposable
private static string? ResolveWeatherDescription(int? code, string locale) private static string? ResolveWeatherDescription(int? code, string locale)
{ {
if (!code.HasValue) return XiaomiWeatherCodeMapper.ResolveDisplayText(code, locale);
{
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}";
} }
private static string Truncate(string? text, int maxLength) private static string Truncate(string? text, int maxLength)

View File

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

View File

@@ -1,5 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using Avalonia.Media; using Avalonia.Media;
using LanMountainDesktop.Models;
using LanMountainDesktop.Services;
namespace LanMountainDesktop.Theme; namespace LanMountainDesktop.Theme;
@@ -8,4 +10,7 @@ public sealed record ThemeColorContext(
bool IsLightBackground, bool IsLightBackground,
bool IsLightNavBackground, bool IsLightNavBackground,
bool IsNightMode, 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] [ObservableProperty]
private string _restartButtonText = string.Empty; private string _restartButtonText = string.Empty;
[ObservableProperty]
private string _restartDialogTitle = string.Empty;
[ObservableProperty]
private string _restartDialogPrimaryText = string.Empty;
[ObservableProperty]
private string _restartDialogCloseText = string.Empty;
[ObservableProperty] [ObservableProperty]
private string? _drawerTitle; private string? _drawerTitle;
@@ -84,6 +93,12 @@ public sealed partial class SettingsWindowViewModel : ViewModelBase
Title = L("settings.title"); Title = L("settings.title");
RestartTitle = L("settings.restart_dock.title"); RestartTitle = L("settings.restart_dock.title");
RestartButtonText = L("settings.restart_dock.button"); 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"); DrawerFallbackTitle = L("settings.window.drawer_default");
var nextDefaultRestartMessage = L("settings.restart_dock.description"); var nextDefaultRestartMessage = L("settings.restart_dock.description");
@@ -113,6 +128,28 @@ public sealed class SelectionOption
public string Label { get; } 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 sealed class TimeZoneOption
{ {
public TimeZoneOption(string? id, string label) public TimeZoneOption(string? id, string label)
@@ -384,27 +421,37 @@ public sealed partial class GeneralSettingsPageViewModel : ViewModelBase
public sealed partial class AppearanceSettingsPageViewModel : 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 ISettingsFacadeService _settingsFacade;
private readonly IAppearanceThemeService _appearanceThemeService;
private readonly LocalizationService _localizationService = new(); private readonly LocalizationService _localizationService = new();
private readonly string _languageCode; private readonly string _languageCode;
private bool _isInitializing; private bool _isInitializing;
private string? _selectedWallpaperSeed;
public AppearanceSettingsPageViewModel(ISettingsFacadeService settingsFacade) public AppearanceSettingsPageViewModel(
ISettingsFacadeService settingsFacade,
IAppearanceThemeService appearanceThemeService)
{ {
_settingsFacade = settingsFacade; _settingsFacade = settingsFacade;
_appearanceThemeService = appearanceThemeService;
_languageCode = _localizationService.NormalizeLanguageCode(_settingsFacade.Region.Get().LanguageCode); _languageCode = _localizationService.NormalizeLanguageCode(_settingsFacade.Region.Get().LanguageCode);
WallpaperPlacements = CreateWallpaperPlacements();
ClockFormats = CreateClockFormats();
RefreshLocalizedText(); RefreshLocalizedText();
ThemeColorModes = CreateThemeColorModes();
_isInitializing = true; _isInitializing = true;
Load(); Load();
_isInitializing = false; _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] [ObservableProperty]
private bool _isNightMode; private bool _isNightMode;
@@ -413,32 +460,66 @@ public sealed partial class AppearanceSettingsPageViewModel : ViewModelBase
private string _themeColor = string.Empty; private string _themeColor = string.Empty;
[ObservableProperty] [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; return;
} }
ThemeColor = value.ToString(); UpdatePreview(BuildPendingState(usePickerSeed: true));
} }
[ObservableProperty] [ObservableProperty]
private bool _useSystemChrome; private bool _useSystemChrome;
[ObservableProperty] [ObservableProperty]
private string _wallpaperPath = string.Empty; private SelectionOption _selectedThemeColorMode = new(ThemeAppearanceValues.ColorModeSeedMonet, "User theme color Monet");
[ObservableProperty] [ObservableProperty]
private SelectionOption _selectedWallpaperPlacement = new("Fill", "Fill"); private SelectionOption _selectedSystemMaterialMode = new(ThemeAppearanceValues.MaterialNone, "None");
[ObservableProperty] [ObservableProperty]
private bool _showClock = true; private bool _isThemeColorEditable;
[ObservableProperty] [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] [ObservableProperty]
private string _pageTitle = string.Empty; private string _pageTitle = string.Empty;
@@ -455,75 +536,111 @@ public sealed partial class AppearanceSettingsPageViewModel : ViewModelBase
[ObservableProperty] [ObservableProperty]
private string _themeColorLabel = string.Empty; private string _themeColorLabel = string.Empty;
[ObservableProperty]
private string _themeColorModeLabel = string.Empty;
[ObservableProperty]
private string _systemMaterialLabel = string.Empty;
[ObservableProperty] [ObservableProperty]
private string _themeHeader = string.Empty; private string _themeHeader = string.Empty;
[ObservableProperty] [ObservableProperty]
private string _wallpaperHeader = string.Empty; private string _themeSourceNeutralText = string.Empty;
[ObservableProperty] [ObservableProperty]
private string _wallpaperPathLabel = string.Empty; private string _themeSourceUserColorText = string.Empty;
[ObservableProperty] [ObservableProperty]
private string _wallpaperPlacementLabel = string.Empty; private string _themeSourceWallpaperText = string.Empty;
[ObservableProperty] [ObservableProperty]
private string _importWallpaperButtonText = string.Empty; private string _themeSourceDefaultDescription = string.Empty;
[ObservableProperty] [ObservableProperty]
private string _clockHeader = string.Empty; private string _themeSourceUserColorDescription = string.Empty;
[ObservableProperty] [ObservableProperty]
private string _clockDescription = string.Empty; private string _themeSourceWallpaperDescription = string.Empty;
[ObservableProperty] [ObservableProperty]
private string _clockFormatLabel = string.Empty; private string _themeSourceWallpaperAppDescription = string.Empty;
[ObservableProperty] [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() public void Load()
{ {
var theme = _settingsFacade.Theme.Get(); var theme = _settingsFacade.Theme.Get();
IsNightMode = theme.IsNightMode; var liveSnapshot = _appearanceThemeService.GetCurrent();
ThemeColor = theme.ThemeColor ?? string.Empty; RefreshMaterialModeOptions(liveSnapshot);
if (Color.TryParse(ThemeColor, out var color))
{
ThemeColorPickerValue = color;
}
else
{
ThemeColorPickerValue = Color.Parse("#FF3B82F6");
}
UseSystemChrome = theme.UseSystemChrome;
var wallpaper = _settingsFacade.Wallpaper.Get(); _isInitializing = true;
WallpaperPath = wallpaper.WallpaperPath ?? string.Empty; try
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))
{ {
WallpaperPath = importedPath; ApplySavedState(theme);
} }
finally
{
_isInitializing = false;
}
UpdatePreview(theme);
} }
partial void OnIsNightModeChanged(bool value) partial void OnIsNightModeChanged(bool value)
@@ -533,20 +650,7 @@ public sealed partial class AppearanceSettingsPageViewModel : ViewModelBase
return; return;
} }
SaveTheme(); PersistCurrentState(restartRequired: false);
}
partial void OnThemeColorChanged(string value)
{
if (_isInitializing)
{
return;
}
if (string.IsNullOrWhiteSpace(value) || Color.TryParse(value, out _))
{
SaveTheme();
}
} }
partial void OnUseSystemChromeChanged(bool value) partial void OnUseSystemChromeChanged(bool value)
@@ -556,126 +660,254 @@ public sealed partial class AppearanceSettingsPageViewModel : ViewModelBase
return; return;
} }
SaveTheme(); PersistCurrentState(restartRequired: false);
} }
partial void OnWallpaperPathChanged(string value) partial void OnSelectedThemeColorModeChanged(SelectionOption value)
{
if (_isInitializing)
{
return;
}
SaveWallpaper();
}
partial void OnSelectedWallpaperPlacementChanged(SelectionOption value)
{ {
if (_isInitializing || value is null) if (_isInitializing || value is null)
{ {
return; return;
} }
SaveWallpaper(); PersistCurrentState(restartRequired: true);
} }
partial void OnShowClockChanged(bool value) partial void OnSelectedSystemMaterialModeChanged(SelectionOption value)
{
if (_isInitializing)
{
return;
}
SaveStatusBar();
}
partial void OnSelectedClockFormatChanged(SelectionOption value)
{ {
if (_isInitializing || value is null) if (_isInitializing || value is null)
{ {
return; return;
} }
SaveStatusBar(); PersistCurrentState(restartRequired: true);
} }
private void SaveTheme() [RelayCommand]
private void ApplyCustomSeed()
{ {
_settingsFacade.Theme.Save(new ThemeAppearanceSettingsState( if (!IsThemeColorEditable)
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)
{ {
topComponents.Add(BuiltInComponentIds.Clock); return;
} }
_settingsFacade.StatusBar.Save(new StatusBarSettingsState( ThemeColor = CustomSeedPickerValue.ToString();
topComponents, PersistCurrentState(restartRequired: false);
state.PinnedTaskbarActions,
state.EnableDynamicTaskbarActions,
state.TaskbarLayoutMode,
SelectedClockFormat.Value,
state.SpacingMode,
state.CustomSpacingPercent));
} }
private IReadOnlyList<SelectionOption> CreateWallpaperPlacements() public void CancelCustomSeedPreview()
{ {
return if (_isInitializing)
[ {
new SelectionOption("Fill", L("settings.wallpaper.placement.fill", "Fill")), return;
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")), SyncCustomSeedPickerWithSavedThemeColor();
new SelectionOption("Tile", L("settings.wallpaper.placement.tile", "Tile")) UpdatePreview(BuildPendingState(usePickerSeed: false));
];
} }
private IReadOnlyList<SelectionOption> CreateClockFormats() public void SelectWallpaperSeed(string value)
{ {
return if (!IsWallpaperMode || string.IsNullOrWhiteSpace(value))
[ {
new SelectionOption("HourMinute", L("settings.status_bar.clock_format.hm", "Hour:Minute")), return;
new SelectionOption("HourMinuteSecond", L("settings.status_bar.clock_format.hms", "Hour:Minute:Second")) }
];
_selectedWallpaperSeed = value;
PersistCurrentState(restartRequired: true);
} }
private void RefreshLocalizedText() private void RefreshLocalizedText()
{ {
PageTitle = L("settings.appearance.title", "Appearance"); 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"); ThemeHeader = L("settings.appearance.theme_header", "Theme");
NightModeLabel = L("settings.color.enable_night_mode_toggle", "Enable night mode"); NightModeLabel = L("settings.color.enable_night_mode_toggle", "Enable night mode");
UseSystemChromeLabel = L("settings.color.use_system_chrome_toggle", "Use system window chrome"); UseSystemChromeLabel = L("settings.color.use_system_chrome_toggle", "Use system window chrome");
ThemeColorLabel = L("settings.color.theme_color_label", "Theme Accent Color"); ThemeColorLabel = L("settings.color.theme_color_label", "Theme Accent Color");
WallpaperHeader = L("settings.wallpaper.title", "Wallpaper"); ThemeColorModeLabel = L("settings.appearance.theme_color_mode_label", "Theme color source");
WallpaperPathLabel = L("settings.wallpaper.current_label", "Current Wallpaper"); SystemMaterialLabel = L("settings.appearance.system_material_label", "System material");
WallpaperPlacementLabel = L("settings.wallpaper.placement_label", "Placement"); ThemeSourceNeutralText = L("settings.appearance.theme_color_mode.neutral", "Default neutral");
ImportWallpaperButtonText = L("settings.wallpaper.pick_button", "Import Wallpaper"); ThemeSourceUserColorText = L("settings.appearance.theme_color_mode.user", "User theme color Monet");
ClockHeader = L("settings.status_bar.clock_header", "Clock Component"); ThemeSourceWallpaperText = L("settings.appearance.theme_color_mode.wallpaper", "Wallpaper Monet");
ClockDescription = L("settings.status_bar.clock_description", "Display a clock on the top status bar."); ThemeSourceDefaultDescription = L("settings.appearance.theme_color_mode_desc.neutral", "Use the standard light and dark neutral surfaces.");
ClockFormatLabel = L("settings.status_bar.clock_format_label", "Clock Format"); ThemeSourceUserColorDescription = L("settings.appearance.theme_color_mode_desc.user", "Use the selected theme color as the Monet seed.");
FilePickerTitle = L("filepicker.title", "Select wallpaper"); 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) private string L(string key, string fallback)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,15 +3,21 @@ using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using LanMountainDesktop.Models; using LanMountainDesktop.Models;
using LanMountainDesktop.Services;
namespace LanMountainDesktop.Views.Components; namespace LanMountainDesktop.Views.Components;
public enum HyperOS3WeatherVisualKind public enum HyperOS3WeatherVisualKind
{ {
Unknown,
ClearDay, ClearDay,
ClearNight, ClearNight,
PartlyCloudyDay,
PartlyCloudyNight,
CloudyDay, CloudyDay,
CloudyNight, CloudyNight,
Haze,
Sleet,
RainLight, RainLight,
RainHeavy, RainHeavy,
Storm, Storm,
@@ -91,38 +97,53 @@ public static class HyperOS3WeatherTheme
private static readonly IReadOnlyDictionary<HyperOS3WeatherVisualKind, string> BackgroundAssets = private static readonly IReadOnlyDictionary<HyperOS3WeatherVisualKind, string> BackgroundAssets =
new Dictionary<HyperOS3WeatherVisualKind, string> 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.ClearDay] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/hyper_cross_sky_day.png",
[HyperOS3WeatherVisualKind.ClearNight] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/hyper_cross_sky_night.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.CloudyDay] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/hyper_sky_front.png",
[HyperOS3WeatherVisualKind.CloudyNight] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/hyper_sky_back.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.RainLight] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/hyper_sky_front.png",
[HyperOS3WeatherVisualKind.RainHeavy] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/hyper_sky_back.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.Storm] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/hyper_cross_sky_night.png",
[HyperOS3WeatherVisualKind.Snow] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/hyper_sky_top.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 = private static readonly IReadOnlyDictionary<HyperOS3WeatherVisualKind, string> HeroIconAssets =
new Dictionary<HyperOS3WeatherVisualKind, string> 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.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.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.PartlyCloudyDay] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_partly_cloudy_day.webp",
[HyperOS3WeatherVisualKind.CloudyNight] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_hero_moon_soft.png", [HyperOS3WeatherVisualKind.PartlyCloudyNight] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_partly_cloudy_night.webp",
[HyperOS3WeatherVisualKind.RainLight] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_rain_light_soft.png", [HyperOS3WeatherVisualKind.CloudyDay] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_cloudy.webp",
[HyperOS3WeatherVisualKind.RainHeavy] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_rain_heavy_soft.png", [HyperOS3WeatherVisualKind.CloudyNight] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_cloudy.webp",
[HyperOS3WeatherVisualKind.Storm] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_storm_soft.png", [HyperOS3WeatherVisualKind.Haze] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_haze.webp",
[HyperOS3WeatherVisualKind.Snow] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_snow_soft.png", [HyperOS3WeatherVisualKind.Sleet] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_sleet.webp",
[HyperOS3WeatherVisualKind.Fog] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_hero_sun_soft.png" [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 = private static readonly IReadOnlyDictionary<HyperOS3WeatherVisualKind, string> MiniIconAssets =
new Dictionary<HyperOS3WeatherVisualKind, string> 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.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.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.PartlyCloudyDay] = "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.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.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.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.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 = private static readonly IReadOnlyDictionary<HyperOS3WeatherVisualKind, HyperOS3WeatherPalette> Palettes =
new Dictionary<HyperOS3WeatherVisualKind, HyperOS3WeatherPalette> new Dictionary<HyperOS3WeatherVisualKind, HyperOS3WeatherPalette>
{ {
[HyperOS3WeatherVisualKind.Unknown] = new(
GradientFrom: "#6B7785",
GradientTo: "#98A4B3",
Tint: "#55606E",
PrimaryText: "#F8FBFF",
SecondaryText: "#E1E8F0",
TertiaryText: "#C2CCD8",
ParticleColor: "#24FFFFFF"),
[HyperOS3WeatherVisualKind.ClearDay] = new( [HyperOS3WeatherVisualKind.ClearDay] = new(
GradientFrom: "#5F7FA3", GradientFrom: "#5F7FA3",
GradientTo: "#9BB4CF", GradientTo: "#9BB4CF",
@@ -149,6 +178,22 @@ public static class HyperOS3WeatherTheme
SecondaryText: "#D9E4F0", SecondaryText: "#D9E4F0",
TertiaryText: "#B4C3D6", TertiaryText: "#B4C3D6",
ParticleColor: "#00FFFFFF"), 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( [HyperOS3WeatherVisualKind.CloudyDay] = new(
GradientFrom: "#5D799A", GradientFrom: "#5D799A",
GradientTo: "#95ADC6", GradientTo: "#95ADC6",
@@ -165,6 +210,22 @@ public static class HyperOS3WeatherTheme
SecondaryText: "#D4E0ED", SecondaryText: "#D4E0ED",
TertiaryText: "#B0BFD2", TertiaryText: "#B0BFD2",
ParticleColor: "#30F0F5FF"), 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( [HyperOS3WeatherVisualKind.RainLight] = new(
GradientFrom: "#4F6786", GradientFrom: "#4F6786",
GradientTo: "#7A92AF", GradientTo: "#7A92AF",
@@ -210,6 +271,14 @@ public static class HyperOS3WeatherTheme
private static readonly IReadOnlyDictionary<HyperOS3WeatherVisualKind, HyperOS3WeatherMotion> Motions = private static readonly IReadOnlyDictionary<HyperOS3WeatherVisualKind, HyperOS3WeatherMotion> Motions =
new Dictionary<HyperOS3WeatherVisualKind, HyperOS3WeatherMotion> 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( [HyperOS3WeatherVisualKind.ClearDay] = new(
DriftX: 8.0, DriftY: 4.0, ZoomBase: 1.055, ZoomAmplitude: 0.012, DriftX: 8.0, DriftY: 4.0, ZoomBase: 1.055, ZoomAmplitude: 0.012,
MotionOpacityBase: 0.22, MotionOpacityPulse: 0.05, MotionOpacityBase: 0.22, MotionOpacityPulse: 0.05,
@@ -226,6 +295,22 @@ public static class HyperOS3WeatherTheme
PhaseStep: 0.018, ParticleCount: 0, PhaseStep: 0.018, ParticleCount: 0,
ParticleSpeedMin: 0, ParticleSpeedMax: 0, ParticleSpeedMin: 0, ParticleSpeedMax: 0,
ParticleLengthMin: 0, ParticleLengthMax: 0, ParticleDriftPerTick: 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( [HyperOS3WeatherVisualKind.CloudyDay] = new(
DriftX: 12.0, DriftY: 7.0, ZoomBase: 1.060, ZoomAmplitude: 0.013, DriftX: 12.0, DriftY: 7.0, ZoomBase: 1.060, ZoomAmplitude: 0.013,
MotionOpacityBase: 0.32, MotionOpacityPulse: 0.06, MotionOpacityBase: 0.32, MotionOpacityPulse: 0.06,
@@ -242,6 +327,22 @@ public static class HyperOS3WeatherTheme
PhaseStep: 0.021, ParticleCount: 0, PhaseStep: 0.021, ParticleCount: 0,
ParticleSpeedMin: 0.35, ParticleSpeedMax: 0.80, ParticleSpeedMin: 0.35, ParticleSpeedMax: 0.80,
ParticleLengthMin: 16, ParticleLengthMax: 30, ParticleDriftPerTick: 0.12), 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( [HyperOS3WeatherVisualKind.RainLight] = new(
DriftX: 6.0, DriftY: 10.0, ZoomBase: 1.050, ZoomAmplitude: 0.010, DriftX: 6.0, DriftY: 10.0, ZoomBase: 1.050, ZoomAmplitude: 0.010,
MotionOpacityBase: 0.30, MotionOpacityPulse: 0.08, MotionOpacityBase: 0.30, MotionOpacityPulse: 0.08,
@@ -296,16 +397,20 @@ public static class HyperOS3WeatherTheme
public static HyperOS3WeatherVisualKind ResolveVisualKind(int? weatherCode, bool isNight) public static HyperOS3WeatherVisualKind ResolveVisualKind(int? weatherCode, bool isNight)
{ {
return weatherCode switch return XiaomiWeatherCodeMapper.ResolveBucket(weatherCode) switch
{ {
0 => isNight ? HyperOS3WeatherVisualKind.ClearNight : HyperOS3WeatherVisualKind.ClearDay, WeatherConditionBucket.Unknown => HyperOS3WeatherVisualKind.Unknown,
1 or 2 => isNight ? HyperOS3WeatherVisualKind.CloudyNight : HyperOS3WeatherVisualKind.CloudyDay, WeatherConditionBucket.Clear => isNight ? HyperOS3WeatherVisualKind.ClearNight : HyperOS3WeatherVisualKind.ClearDay,
3 or 7 => HyperOS3WeatherVisualKind.RainLight, WeatherConditionBucket.PartlyCloudy => isNight ? HyperOS3WeatherVisualKind.PartlyCloudyNight : HyperOS3WeatherVisualKind.PartlyCloudyDay,
8 or 9 => HyperOS3WeatherVisualKind.RainHeavy, WeatherConditionBucket.Cloudy => isNight ? HyperOS3WeatherVisualKind.CloudyNight : HyperOS3WeatherVisualKind.CloudyDay,
4 => HyperOS3WeatherVisualKind.Storm, WeatherConditionBucket.Haze => HyperOS3WeatherVisualKind.Haze,
13 or 14 or 15 or 16 => HyperOS3WeatherVisualKind.Snow, WeatherConditionBucket.Sleet => HyperOS3WeatherVisualKind.Sleet,
18 or 32 => HyperOS3WeatherVisualKind.Fog, WeatherConditionBucket.RainLight => HyperOS3WeatherVisualKind.RainLight,
_ => isNight ? HyperOS3WeatherVisualKind.CloudyNight : HyperOS3WeatherVisualKind.CloudyDay 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 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", => "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 HyperOS3WeatherVisualKind.Snow
=> "avares://LanMountainDesktop/Assets/Weather/HyperOS3/hyper_snow_flake.png", => "avares://LanMountainDesktop/Assets/Weather/HyperOS3/hyper_snow_flake.png",
HyperOS3WeatherVisualKind.Fog
=> "avares://LanMountainDesktop/Assets/Weather/HyperOS3/hyper_haze.png",
_ => null _ => null
}; };
} }

View File

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

View File

@@ -22,10 +22,15 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
{ {
private enum WeatherVisualKind private enum WeatherVisualKind
{ {
Unknown,
ClearDay, ClearDay,
ClearNight, ClearNight,
PartlyCloudyDay,
PartlyCloudyNight,
CloudyDay, CloudyDay,
CloudyNight, CloudyNight,
Haze,
Sleet,
RainLight, RainLight,
RainHeavy, RainHeavy,
Storm, Storm,
@@ -427,7 +432,12 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
private void ApplySnapshot(WeatherSnapshot snapshot, string fallbackLocationName) private void ApplySnapshot(WeatherSnapshot snapshot, string fallbackLocationName)
{ {
var isNight = ResolveIsNight(snapshot); 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); ApplyVisualTheme(visualKind);
var rawLocation = string.IsNullOrWhiteSpace(snapshot.LocationName) var rawLocation = string.IsNullOrWhiteSpace(snapshot.LocationName)
@@ -435,8 +445,8 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
: snapshot.LocationName; : snapshot.LocationName;
CityTextBlock.Text = ResolvePreciseDisplayLocation(rawLocation, _languageCode, L("weather.widget.location_unknown", "Unknown location")); CityTextBlock.Text = ResolvePreciseDisplayLocation(rawLocation, _languageCode, L("weather.widget.location_unknown", "Unknown location"));
ConditionTextBlock.Text = ResolveWeatherConditionText(snapshot.Current.WeatherText, visualKind); ConditionTextBlock.Text = visual.DisplayText;
SetWeatherIcon(visualKind); SetWeatherIcon(visual.PrimaryIconAsset, visualKind);
SetLoadingSkeleton(false); SetLoadingSkeleton(false);
TemperatureTextBlock.Text = FormatTemperature(snapshot.Current.TemperatureC); TemperatureTextBlock.Text = FormatTemperature(snapshot.Current.TemperatureC);
@@ -449,7 +459,7 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
{ {
var fallbackKind = ResolveFallbackVisualKind(); var fallbackKind = ResolveFallbackVisualKind();
ApplyVisualTheme(fallbackKind); ApplyVisualTheme(fallbackKind);
SetWeatherIcon(fallbackKind); SetWeatherIcon(null, fallbackKind);
SetLoadingSkeleton(false); SetLoadingSkeleton(false);
CityTextBlock.Text = L("weather.widget.location_not_configured", "Weather location is not configured"); 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"); 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; var loadingKind = IsNightNow() ? WeatherVisualKind.CloudyNight : WeatherVisualKind.CloudyDay;
ApplyVisualTheme(loadingKind); ApplyVisualTheme(loadingKind);
SetWeatherIcon(loadingKind); SetWeatherIcon(null, loadingKind);
SetLoadingSkeleton(true); SetLoadingSkeleton(true);
CityTextBlock.Text = ResolvePreciseDisplayLocation( CityTextBlock.Text = ResolvePreciseDisplayLocation(
locationName, locationName,
@@ -477,8 +487,8 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
private void ApplyFailedState(string locationName) private void ApplyFailedState(string locationName)
{ {
ApplyVisualTheme(WeatherVisualKind.Fog); ApplyVisualTheme(WeatherVisualKind.Unknown);
SetWeatherIcon(WeatherVisualKind.Fog); SetWeatherIcon(HyperOS3WeatherTheme.ResolveHeroIconAsset(HyperOS3WeatherVisualKind.Unknown), WeatherVisualKind.Unknown);
SetLoadingSkeleton(false); SetLoadingSkeleton(false);
CityTextBlock.Text = ResolvePreciseDisplayLocation( CityTextBlock.Text = ResolvePreciseDisplayLocation(
locationName, locationName,
@@ -500,7 +510,7 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
BackgroundMotionLayer.Background = ResolveWeatherBackgroundBrush(kind, palette); BackgroundMotionLayer.Background = ResolveWeatherBackgroundBrush(kind, palette);
BackgroundTintLayer.Background = CreateSolidBrush(palette.Tint); 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( var backgroundSamples = WeatherTypographyAccessibility.BuildBackgroundSamples(
palette.GradientFrom, palette.GradientFrom,
palette.GradientTo, palette.GradientTo,
@@ -610,17 +620,28 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
private static WeatherVisualKind ResolveVisualKind(int? weatherCode, bool isNight) 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.ClearDay => WeatherVisualKind.ClearDay,
HyperOS3WeatherVisualKind.ClearNight => WeatherVisualKind.ClearNight, HyperOS3WeatherVisualKind.ClearNight => WeatherVisualKind.ClearNight,
HyperOS3WeatherVisualKind.PartlyCloudyDay => WeatherVisualKind.PartlyCloudyDay,
HyperOS3WeatherVisualKind.PartlyCloudyNight => WeatherVisualKind.PartlyCloudyNight,
HyperOS3WeatherVisualKind.CloudyDay => WeatherVisualKind.CloudyDay, HyperOS3WeatherVisualKind.CloudyDay => WeatherVisualKind.CloudyDay,
HyperOS3WeatherVisualKind.CloudyNight => WeatherVisualKind.CloudyNight, HyperOS3WeatherVisualKind.CloudyNight => WeatherVisualKind.CloudyNight,
HyperOS3WeatherVisualKind.Haze => WeatherVisualKind.Haze,
HyperOS3WeatherVisualKind.Sleet => WeatherVisualKind.Sleet,
HyperOS3WeatherVisualKind.RainLight => WeatherVisualKind.RainLight, HyperOS3WeatherVisualKind.RainLight => WeatherVisualKind.RainLight,
HyperOS3WeatherVisualKind.RainHeavy => WeatherVisualKind.RainHeavy, HyperOS3WeatherVisualKind.RainHeavy => WeatherVisualKind.RainHeavy,
HyperOS3WeatherVisualKind.Storm => WeatherVisualKind.Storm, HyperOS3WeatherVisualKind.Storm => WeatherVisualKind.Storm,
HyperOS3WeatherVisualKind.Snow => WeatherVisualKind.Snow, 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 return kind switch
{ {
WeatherVisualKind.Unknown => HyperOS3WeatherVisualKind.Unknown,
WeatherVisualKind.ClearDay => HyperOS3WeatherVisualKind.ClearDay, WeatherVisualKind.ClearDay => HyperOS3WeatherVisualKind.ClearDay,
WeatherVisualKind.ClearNight => HyperOS3WeatherVisualKind.ClearNight, WeatherVisualKind.ClearNight => HyperOS3WeatherVisualKind.ClearNight,
WeatherVisualKind.PartlyCloudyDay => HyperOS3WeatherVisualKind.PartlyCloudyDay,
WeatherVisualKind.PartlyCloudyNight => HyperOS3WeatherVisualKind.PartlyCloudyNight,
WeatherVisualKind.CloudyDay => HyperOS3WeatherVisualKind.CloudyDay, WeatherVisualKind.CloudyDay => HyperOS3WeatherVisualKind.CloudyDay,
WeatherVisualKind.CloudyNight => HyperOS3WeatherVisualKind.CloudyNight, WeatherVisualKind.CloudyNight => HyperOS3WeatherVisualKind.CloudyNight,
WeatherVisualKind.Haze => HyperOS3WeatherVisualKind.Haze,
WeatherVisualKind.Sleet => HyperOS3WeatherVisualKind.Sleet,
WeatherVisualKind.RainLight => HyperOS3WeatherVisualKind.RainLight, WeatherVisualKind.RainLight => HyperOS3WeatherVisualKind.RainLight,
WeatherVisualKind.RainHeavy => HyperOS3WeatherVisualKind.RainHeavy, WeatherVisualKind.RainHeavy => HyperOS3WeatherVisualKind.RainHeavy,
WeatherVisualKind.Storm => HyperOS3WeatherVisualKind.Storm, WeatherVisualKind.Storm => HyperOS3WeatherVisualKind.Storm,
WeatherVisualKind.Snow => HyperOS3WeatherVisualKind.Snow, 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)) _ = kind;
{ return XiaomiWeatherVisualResolver.ResolveDisplayText(weatherText, weatherCode, _languageCode);
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")
};
} }
private static (double? Low, double? High) ResolveTemperatureRange(WeatherSnapshot snapshot) private static (double? Low, double? High) ResolveTemperatureRange(WeatherSnapshot snapshot)
@@ -965,15 +979,16 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
return kind switch return kind switch
{ {
WeatherVisualKind.RainLight or WeatherVisualKind.RainHeavy or WeatherVisualKind.Storm or WeatherVisualKind.Snow => 1.16, 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 _ => 1.0
}; };
} }
private void SetWeatherIcon(WeatherVisualKind kind) private void SetWeatherIcon(string? assetUri, WeatherVisualKind fallbackKind)
{ {
WeatherIconImage.Source = HyperOS3WeatherAssetLoader.LoadImage( WeatherIconImage.Source = HyperOS3WeatherAssetLoader.LoadImage(
HyperOS3WeatherTheme.ResolveHeroIconAsset(ToThemeKind(kind))); assetUri ?? HyperOS3WeatherTheme.ResolveHeroIconAsset(ToThemeKind(fallbackKind)));
} }
private void SetLoadingSkeleton(bool isLoading) private void SetLoadingSkeleton(bool isLoading)
@@ -1190,7 +1205,7 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
var sway = _activeVisualKind == WeatherVisualKind.Snow var sway = _activeVisualKind == WeatherVisualKind.Snow
? Math.Sin(_animationPhase + (i * 0.45)) * 0.55 ? 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 ? Math.Sin((_animationPhase * 0.7) + (i * 0.31)) * 0.18
: 0; : 0;
@@ -1220,16 +1235,16 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
var thickness = _activeVisualKind switch var thickness = _activeVisualKind switch
{ {
WeatherVisualKind.Snow => NextRange(2.2, 4.3), 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) _ => NextRange(1.0, 2.2)
}; };
var opacity = _activeVisualKind switch var opacity = _activeVisualKind switch
{ {
WeatherVisualKind.Storm => NextRange(0.26, 0.52), WeatherVisualKind.Storm => NextRange(0.26, 0.52),
WeatherVisualKind.RainHeavy => NextRange(0.24, 0.46), 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.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) _ => NextRange(0.10, 0.24)
}; };
@@ -1241,7 +1256,7 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
{ {
WeatherVisualKind.Storm => -24, WeatherVisualKind.Storm => -24,
WeatherVisualKind.RainHeavy => -20, WeatherVisualKind.RainHeavy => -20,
WeatherVisualKind.RainLight => -14, WeatherVisualKind.RainLight or WeatherVisualKind.Sleet => -14,
WeatherVisualKind.Snow => -6, WeatherVisualKind.Snow => -6,
_ => 0 _ => 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() private void ApplyTopStatusComponentVisibility()
{ {
var showClock = _topStatusComponentIds.Contains(BuiltInComponentIds.Clock); var showClock = _topStatusComponentIds.Contains(BuiltInComponentIds.Clock);
var hasVisibleTopStatusComponent = false;
if (ClockWidget is not null) if (ClockWidget is not null)
{ {
@@ -248,8 +249,33 @@ public partial class MainWindow
ClockWidget.SetDisplayFormat(_clockDisplayFormat); ClockWidget.SetDisplayFormat(_clockDisplayFormat);
var columnSpan = _clockDisplayFormat == ClockDisplayFormat.HourMinute ? 2 : 3; var columnSpan = _clockDisplayFormat == ClockDisplayFormat.HourMinute ? 2 : 3;
Grid.SetColumnSpan(ClockWidget, columnSpan); 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() private TaskbarContext GetCurrentTaskbarContext()

View File

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

View File

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

View File

@@ -78,6 +78,7 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
TaskbarActionId.MinimizeToWindows TaskbarActionId.MinimizeToWindows
]; ];
private readonly ISettingsFacadeService _settingsFacade = HostSettingsFacadeProvider.GetOrCreate(); private readonly ISettingsFacadeService _settingsFacade = HostSettingsFacadeProvider.GetOrCreate();
private readonly IAppearanceThemeService _appearanceThemeService = HostAppearanceThemeProvider.GetOrCreate();
private readonly IGridSettingsService _gridSettingsService; private readonly IGridSettingsService _gridSettingsService;
private readonly IThemeAppearanceService _themeSettingsService; private readonly IThemeAppearanceService _themeSettingsService;
private readonly IWeatherSettingsService _weatherSettingsService; private readonly IWeatherSettingsService _weatherSettingsService;
@@ -206,6 +207,7 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
_componentEditorWindowService = new ComponentEditorWindowService(_settingsFacade); _componentEditorWindowService = new ComponentEditorWindowService(_settingsFacade);
_fluentAvaloniaTheme = Application.Current?.Styles.OfType<FluentAvaloniaTheme>().FirstOrDefault(); _fluentAvaloniaTheme = Application.Current?.Styles.OfType<FluentAvaloniaTheme>().FirstOrDefault();
_settingsService.Changed += OnSettingsChanged; _settingsService.Changed += OnSettingsChanged;
_appearanceThemeService.Changed += OnAppearanceThemeChanged;
PropertyChanged += OnWindowPropertyChanged; PropertyChanged += OnWindowPropertyChanged;
InitializeDesktopSurfaceSwipeHandlers(); InitializeDesktopSurfaceSwipeHandlers();
InitializeDesktopComponentDragHandlers(); InitializeDesktopComponentDragHandlers();
@@ -340,6 +342,7 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
_wallpaperBitmap?.Dispose(); _wallpaperBitmap?.Dispose();
_wallpaperBitmap = null; _wallpaperBitmap = null;
_settingsService.Changed -= OnSettingsChanged; _settingsService.Changed -= OnSettingsChanged;
_appearanceThemeService.Changed -= OnAppearanceThemeChanged;
PropertyChanged -= OnWindowPropertyChanged; PropertyChanged -= OnWindowPropertyChanged;
DesktopHost.SizeChanged -= OnDesktopHostSizeChanged; DesktopHost.SizeChanged -= OnDesktopHostSizeChanged;
if (Application.Current is App app && app.SettingsWindowService is { } settingsWindowService) if (Application.Current is App app && app.SettingsWindowService is { } settingsWindowService)
@@ -349,6 +352,23 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
base.OnClosed(e); 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() private int CalculateDefaultShortSideCellCountFromDpi()
{ {
var dpi = 96d * RenderScaling; var dpi = 96d * RenderScaling;

View File

@@ -31,13 +31,224 @@
</ui:SettingsExpander.Footer> </ui:SettingsExpander.Footer>
</ui:SettingsExpander> </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> <ui:SettingsExpander.IconSource>
<fi:SymbolIconSource Symbol="Color" /> <fi:SymbolIconSource Symbol="Color" />
</ui:SettingsExpander.IconSource> </ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer> <ui:SettingsExpanderItem>
<ColorPicker Color="{Binding ThemeColorPickerValue}" /> <StackPanel Spacing="12">
</ui:SettingsExpander.Footer> <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> </ui:SettingsExpander>
</StackPanel> </StackPanel>
</ScrollViewer> </ScrollViewer>

View File

@@ -1,6 +1,10 @@
using System;
using LanMountainDesktop.PluginSdk; using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services;
using LanMountainDesktop.Services.Settings; using LanMountainDesktop.Services.Settings;
using LanMountainDesktop.ViewModels; using LanMountainDesktop.ViewModels;
using Avalonia.Controls;
using Avalonia.Interactivity;
namespace LanMountainDesktop.Views.SettingsPages; namespace LanMountainDesktop.Views.SettingsPages;
@@ -15,16 +19,51 @@ namespace LanMountainDesktop.Views.SettingsPages;
public partial class AppearanceSettingsPage : SettingsPageBase public partial class AppearanceSettingsPage : SettingsPageBase
{ {
public AppearanceSettingsPage() public AppearanceSettingsPage()
: this(new AppearanceSettingsPageViewModel(HostSettingsFacadeProvider.GetOrCreate())) : this(new AppearanceSettingsPageViewModel(
HostSettingsFacadeProvider.GetOrCreate(),
HostAppearanceThemeProvider.GetOrCreate()))
{ {
} }
public AppearanceSettingsPage(AppearanceSettingsPageViewModel viewModel) public AppearanceSettingsPage(AppearanceSettingsPageViewModel viewModel)
{ {
ViewModel = viewModel; ViewModel = viewModel;
ViewModel.RestartRequested += OnRestartRequested;
DataContext = ViewModel; DataContext = ViewModel;
InitializeComponent(); InitializeComponent();
} }
public AppearanceSettingsPageViewModel ViewModel { get; } 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( [SettingsPageInfo(
"status-bar", "status-bar",
"Status Bar", "Status Bar",
SettingsPageCategory.Appearance, SettingsPageCategory.Components,
IconKey = "Apps", IconKey = "Apps",
SortOrder = 17, SortOrder = 15,
TitleLocalizationKey = "settings.status_bar.title", TitleLocalizationKey = "settings.status_bar.title",
DescriptionLocalizationKey = "settings.status_bar.description")] DescriptionLocalizationKey = "settings.status_bar.description")]
public partial class StatusBarSettingsPage : SettingsPageBase public partial class StatusBarSettingsPage : SettingsPageBase

View File

@@ -13,7 +13,7 @@
WindowStartupLocation="Manual" WindowStartupLocation="Manual"
SystemDecorations="BorderOnly" SystemDecorations="BorderOnly"
FontFamily="{DynamicResource AppFontFamily}" FontFamily="{DynamicResource AppFontFamily}"
Background="{DynamicResource AdaptiveSurfaceBaseBrush}" Background="Transparent"
Icon="/Assets/avalonia-logo.ico" Icon="/Assets/avalonia-logo.ico"
Title="{Binding Title}"> Title="{Binding Title}">
@@ -38,13 +38,13 @@
</Window.Styles> </Window.Styles>
<Grid Classes="settings-scope" <Grid Classes="settings-scope"
Background="{DynamicResource AdaptiveSurfaceBaseBrush}" Background="{DynamicResource AdaptiveSettingsWindowBackgroundBrush}"
RowDefinitions="Auto,Auto,*"> RowDefinitions="Auto,*">
<Border x:Name="WindowTitleBarHost" <Border x:Name="WindowTitleBarHost"
Height="48" Height="48"
Padding="12,0,12,0" Padding="12,0,12,0"
Background="{DynamicResource AdaptiveSurfaceBaseBrush}" Background="{DynamicResource AdaptiveSettingsWindowBackgroundBrush}"
BorderBrush="{DynamicResource AdaptiveGlassPanelBorderBrush}" BorderBrush="{DynamicResource AdaptiveSettingsWindowBorderBrush}"
BorderThickness="0,0,0,1" BorderThickness="0,0,0,1"
PointerPressed="OnWindowTitleBarPointerPressed"> PointerPressed="OnWindowTitleBarPointerPressed">
<Grid ColumnDefinitions="Auto,Auto,*,Auto,Auto" <Grid ColumnDefinitions="Auto,Auto,*,Auto,Auto"
@@ -113,22 +113,8 @@
</Grid> </Grid>
</Border> </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" <ui:NavigationView x:Name="RootNavigationView"
Grid.Row="2" Grid.Row="1"
Margin="0,8,0,0" Margin="0,8,0,0"
Background="Transparent" Background="Transparent"
PaneDisplayMode="Auto" 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 Dictionary<string, Control> _cachedPages = new(StringComparer.OrdinalIgnoreCase);
private readonly bool _useSystemChrome; private readonly bool _useSystemChrome;
private bool _isResponsiveRefreshPending; private bool _isResponsiveRefreshPending;
private bool _isRestartPromptVisible;
public SettingsWindow() public SettingsWindow()
: this( : this(
@@ -129,6 +130,8 @@ public partial class SettingsWindow : Window, ISettingsPageHostContext
? ViewModel.GetDefaultRestartMessage() ? ViewModel.GetDefaultRestartMessage()
: reason; : reason;
ViewModel.IsRestartRequested = true; ViewModel.IsRestartRequested = true;
PendingRestartStateService.SetPending(PendingRestartStateService.SettingsWindowReason, true);
ShowRestartPrompt();
} }
public void ApplyChromeMode(bool useSystemChrome) 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; _ = sender;
_ = e; _ = e;
_hostApplicationLifecycle.TryRestart(new HostApplicationLifecycleRequest( ShowRestartPrompt();
Source: "SettingsWindow",
Reason: "User accepted restart from settings window."));
await Task.CompletedTask;
} }
private void OnCloseDrawerClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e) 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; 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) private void OnOpened(object? sender, EventArgs e)
{ {
_ = sender; _ = sender;
@@ -642,6 +694,7 @@ public partial class SettingsWindow : Window, ISettingsPageHostContext
"GridDots" => Symbol.GridDots, "GridDots" => Symbol.GridDots,
"PuzzlePiece" => Symbol.PuzzlePiece, "PuzzlePiece" => Symbol.PuzzlePiece,
"ShoppingBag" => Symbol.ShoppingBag, "ShoppingBag" => Symbol.ShoppingBag,
"Shield" => Symbol.ShieldDismiss,
"Info" => Symbol.Info, "Info" => Symbol.Info,
"ArrowSync" => Symbol.ArrowSync, "ArrowSync" => Symbol.ArrowSync,
_ => Symbol.Settings _ => Symbol.Settings

View File

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

View File

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