mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-20 23:54:26 +08:00
settings_re11
This commit is contained in:
@@ -43,6 +43,7 @@ public partial class App : Application
|
||||
}
|
||||
|
||||
private readonly ISettingsFacadeService _settingsFacade = HostSettingsFacadeProvider.GetOrCreate();
|
||||
private readonly IAppearanceThemeService _appearanceThemeService = HostAppearanceThemeProvider.GetOrCreate();
|
||||
private readonly LocalizationService _localizationService = new();
|
||||
private readonly IHostApplicationLifecycle _hostApplicationLifecycle = new HostApplicationLifecycleService();
|
||||
private readonly IDetachedComponentLibraryWindowService _detachedComponentLibraryWindowService = new DetachedComponentLibraryWindowService();
|
||||
@@ -84,6 +85,7 @@ public partial class App : Application
|
||||
public App()
|
||||
{
|
||||
_settingsFacade.Settings.Changed += OnSettingsChanged;
|
||||
_appearanceThemeService.Changed += OnAppearanceThemeChanged;
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
@@ -104,7 +106,6 @@ public partial class App : Application
|
||||
RegisterUiUnhandledExceptionGuard();
|
||||
LinuxDesktopEntryInstaller.EnsureInstalled();
|
||||
InitializePluginRuntime();
|
||||
AppSettingsService.SettingsSaved += OnAppSettingsSaved;
|
||||
InitializeTrayIcon();
|
||||
|
||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
@@ -337,11 +338,11 @@ public partial class App : Application
|
||||
|
||||
private void ApplyThemeFromSettings()
|
||||
{
|
||||
var themeState = _settingsFacade.Theme.Get();
|
||||
RequestedThemeVariant = themeState.IsNightMode
|
||||
var snapshot = _appearanceThemeService.GetCurrent();
|
||||
RequestedThemeVariant = snapshot.IsNightMode
|
||||
? ThemeVariant.Dark
|
||||
: ThemeVariant.Light;
|
||||
ApplyAdaptiveThemeResources(themeState);
|
||||
ApplyAdaptiveThemeResources();
|
||||
}
|
||||
|
||||
private void ApplyCurrentCultureFromSettings()
|
||||
@@ -464,19 +465,6 @@ public partial class App : Application
|
||||
Dispatcher.UIThread.InvokeAsync(Reset, DispatcherPriority.Send).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
private void OnAppSettingsSaved(string _)
|
||||
{
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
ApplyThemeFromSettings();
|
||||
ApplyCurrentCultureFromSettings();
|
||||
if (_trayIcons is not null)
|
||||
{
|
||||
InitializeTrayIcon();
|
||||
}
|
||||
}, DispatcherPriority.Background);
|
||||
}
|
||||
|
||||
private void OnSettingsChanged(object? sender, SettingsChangedEvent e)
|
||||
{
|
||||
_ = sender;
|
||||
@@ -490,13 +478,17 @@ public partial class App : Application
|
||||
{
|
||||
var changedKeys = e.ChangedKeys?.ToArray();
|
||||
var refreshAll = changedKeys is null || changedKeys.Length == 0;
|
||||
var liveAppearance = _appearanceThemeService.GetCurrent();
|
||||
var themeChanged =
|
||||
refreshAll ||
|
||||
changedKeys.Contains(nameof(AppSettingsSnapshot.IsNightMode), StringComparer.OrdinalIgnoreCase) ||
|
||||
changedKeys.Contains(nameof(AppSettingsSnapshot.ThemeColor), StringComparer.OrdinalIgnoreCase) ||
|
||||
changedKeys.Contains(nameof(AppSettingsSnapshot.WallpaperPath), StringComparer.OrdinalIgnoreCase) ||
|
||||
changedKeys.Contains(nameof(AppSettingsSnapshot.WallpaperType), StringComparer.OrdinalIgnoreCase) ||
|
||||
changedKeys.Contains(nameof(AppSettingsSnapshot.WallpaperColor), StringComparer.OrdinalIgnoreCase);
|
||||
changedKeys.Contains(nameof(AppSettingsSnapshot.UseSystemChrome), StringComparer.OrdinalIgnoreCase) ||
|
||||
(string.Equals(liveAppearance.ThemeColorMode, ThemeAppearanceValues.ColorModeSeedMonet, StringComparison.OrdinalIgnoreCase) &&
|
||||
changedKeys.Contains(nameof(AppSettingsSnapshot.ThemeColor), StringComparer.OrdinalIgnoreCase)) ||
|
||||
(string.Equals(liveAppearance.ThemeColorMode, ThemeAppearanceValues.ColorModeWallpaperMonet, StringComparison.OrdinalIgnoreCase) &&
|
||||
(changedKeys.Contains(nameof(AppSettingsSnapshot.WallpaperPath), StringComparer.OrdinalIgnoreCase) ||
|
||||
changedKeys.Contains(nameof(AppSettingsSnapshot.WallpaperType), StringComparer.OrdinalIgnoreCase) ||
|
||||
changedKeys.Contains(nameof(AppSettingsSnapshot.WallpaperColor), StringComparer.OrdinalIgnoreCase)));
|
||||
var languageChanged =
|
||||
refreshAll ||
|
||||
changedKeys.Contains(nameof(AppSettingsSnapshot.LanguageCode), StringComparer.OrdinalIgnoreCase);
|
||||
@@ -517,48 +509,17 @@ public partial class App : Application
|
||||
}, DispatcherPriority.Background);
|
||||
}
|
||||
|
||||
private void ApplyAdaptiveThemeResources(ThemeAppearanceSettingsState themeState)
|
||||
private void OnAppearanceThemeChanged(object? sender, AppearanceThemeSnapshot e)
|
||||
{
|
||||
var wallpaperState = _settingsFacade.Wallpaper.Get();
|
||||
var monetPalette = _settingsFacade.Theme.BuildPalette(
|
||||
themeState.IsNightMode,
|
||||
wallpaperState.WallpaperPath,
|
||||
themeState.ThemeColor);
|
||||
var accentColor = ResolveAccentColor(themeState.ThemeColor, monetPalette);
|
||||
var context = new ThemeColorContext(
|
||||
accentColor,
|
||||
IsLightBackground: !themeState.IsNightMode,
|
||||
IsLightNavBackground: !themeState.IsNightMode,
|
||||
IsNightMode: themeState.IsNightMode,
|
||||
MonetColors: monetPalette.MonetColors);
|
||||
ThemeColorSystemService.ApplyThemeResources(Resources, context);
|
||||
GlassEffectService.ApplyGlassResources(Resources, context);
|
||||
_ = sender;
|
||||
_ = e;
|
||||
|
||||
Dispatcher.UIThread.Post(ApplyThemeFromSettings, DispatcherPriority.Background);
|
||||
}
|
||||
|
||||
private static Color ResolveAccentColor(string? colorText, MonetPalette monetPalette)
|
||||
private void ApplyAdaptiveThemeResources()
|
||||
{
|
||||
if (monetPalette.MonetColors is { Count: > 0 })
|
||||
{
|
||||
return monetPalette.MonetColors[0];
|
||||
}
|
||||
|
||||
return TryParseThemeColor(colorText);
|
||||
}
|
||||
|
||||
private static Color TryParseThemeColor(string? colorText)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(colorText))
|
||||
{
|
||||
try
|
||||
{
|
||||
return Color.Parse(colorText);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
return DefaultAccentColor;
|
||||
_appearanceThemeService.ApplyThemeResources(Resources);
|
||||
}
|
||||
|
||||
private void RegisterUiUnhandledExceptionGuard()
|
||||
@@ -606,8 +567,8 @@ public partial class App : Application
|
||||
}
|
||||
|
||||
_exitCleanupCompleted = true;
|
||||
AppSettingsService.SettingsSaved -= OnAppSettingsSaved;
|
||||
_settingsFacade.Settings.Changed -= OnSettingsChanged;
|
||||
_appearanceThemeService.Changed -= OnAppearanceThemeChanged;
|
||||
try
|
||||
{
|
||||
HostUpdateWorkflowServiceProvider.GetOrCreate().TryApplyPendingUpdateOnExit();
|
||||
|
||||
@@ -9,5 +9,6 @@ public sealed record DesktopComponentRuntimeContext(
|
||||
string? PlacementId,
|
||||
ISettingsFacadeService SettingsFacade,
|
||||
ISettingsService SettingsService,
|
||||
IAppearanceThemeService AppearanceTheme,
|
||||
IComponentSettingsAccessor ComponentSettingsAccessor,
|
||||
IComponentInstanceSettingsStore ComponentSettingsStore);
|
||||
|
||||
@@ -9,6 +9,7 @@ public sealed record DesktopComponentSettingsContext(
|
||||
string? PlacementId,
|
||||
ISettingsFacadeService SettingsFacade,
|
||||
ISettingsService SettingsService,
|
||||
IAppearanceThemeService AppearanceTheme,
|
||||
IComponentSettingsAccessor ComponentSettingsAccessor,
|
||||
IComponentInstanceSettingsStore ComponentSettingsStore);
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.0" />
|
||||
<PackageReference Include="LibVLCSharp.Avalonia" Version="3.9.5" />
|
||||
<PackageReference Include="PortAudioSharp2" Version="1.0.6" />
|
||||
<PackageReference Include="MaterialColorUtilities" Version="0.3.0" />
|
||||
<PackageReference Include="System.Runtime.WindowsRuntime" Version="4.7.0" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="10.0.0" />
|
||||
<PackageReference Include="VideoLAN.LibVLC.Windows" Version="3.0.23" Condition="('$(RuntimeIdentifier)' == '' and $([MSBuild]::IsOSPlatform('Windows')))
 or '$(RuntimeIdentifier)' == 'win-x64'
 or '$(RuntimeIdentifier)' == 'win-x86'" />
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
"settings.nav.weather": "Weather",
|
||||
"settings.nav.region": "Region",
|
||||
"settings.nav.update": "Update",
|
||||
"settings.nav.privacy": "Privacy",
|
||||
"settings.nav.launcher": "App Launcher",
|
||||
"settings.nav.plugins": "Plugins",
|
||||
"settings.nav.about": "About",
|
||||
@@ -92,6 +93,12 @@
|
||||
"settings.status_bar.spacing_mode_custom": "Custom",
|
||||
"settings.status_bar.spacing_custom_label": "Custom spacing (%)",
|
||||
"settings.status_bar.spacing_custom_px_format": "≈ {0:F1}px",
|
||||
"settings.privacy.title": "Privacy",
|
||||
"settings.privacy.description": "Manage optional anonymous uploads that help improve the app over time.",
|
||||
"settings.privacy.crash_upload_title": "Anonymous crash data uploads",
|
||||
"settings.privacy.crash_upload_description": "Help us improve application stability.",
|
||||
"settings.privacy.usage_upload_title": "Anonymous usage data uploads",
|
||||
"settings.privacy.usage_upload_description": "Help us improve application features.",
|
||||
"settings.weather.title": "Weather",
|
||||
"settings.weather.description": "Configure weather location, Xiaomi weather preview, and startup positioning behavior.",
|
||||
"settings.weather.location_source_header": "Location Source",
|
||||
@@ -242,11 +249,38 @@
|
||||
"settings.general.preview_date_label": "Date",
|
||||
"settings.general.render_mode_restart_message": "Rendering mode changes require restarting the app.",
|
||||
"settings.appearance.title": "Appearance",
|
||||
"settings.appearance.description": "Adjust theme, wallpaper, and window chrome.",
|
||||
"settings.appearance.description": "Adjust theme source, system material, and window chrome.",
|
||||
"settings.appearance.theme_header": "Theme",
|
||||
"settings.color.enable_night_mode_toggle": "Enable night mode",
|
||||
"settings.color.use_system_chrome_toggle": "Use system window chrome",
|
||||
"settings.color.theme_color_label": "Theme accent color",
|
||||
"settings.appearance.theme_color_mode_label": "Theme color source",
|
||||
"settings.appearance.system_material_label": "System material",
|
||||
"settings.appearance.theme_color_mode.neutral": "Default neutral",
|
||||
"settings.appearance.theme_color_mode.user": "User theme color Monet",
|
||||
"settings.appearance.theme_color_mode.wallpaper": "Wallpaper Monet",
|
||||
"settings.appearance.theme_color_mode_desc.neutral": "Use the default white and black neutral surfaces for light and dark mode.",
|
||||
"settings.appearance.theme_color_mode_desc.user": "Use the selected theme color as the Monet seed for the whole shell.",
|
||||
"settings.appearance.theme_color_mode_desc.wallpaper": "Use wallpaper colors. The app wallpaper is preferred, then the system wallpaper.",
|
||||
"settings.appearance.theme_color_preview.app": "Currently previewing colors extracted from the app wallpaper.",
|
||||
"settings.appearance.theme_color_preview.system": "Currently previewing colors extracted from the system wallpaper.",
|
||||
"settings.appearance.theme_color_preview.fallback": "No usable wallpaper was found. The app is using a fallback accent.",
|
||||
"settings.appearance.system_material.none": "None",
|
||||
"settings.appearance.system_material.mica": "Mica",
|
||||
"settings.appearance.system_material.acrylic": "Acrylic",
|
||||
"settings.appearance.system_material_desc.switchable": "Apply the selected material to windows, Dock, status bar, and component hosts.",
|
||||
"settings.appearance.system_material_desc.fixed": "Your current system only exposes the material modes listed here.",
|
||||
"settings.appearance.restart_message": "Theme source and system material changes require restarting the app.",
|
||||
"settings.appearance.preview.primary": "Primary",
|
||||
"settings.appearance.preview.secondary": "Secondary",
|
||||
"settings.appearance.preview.tertiary": "Tertiary",
|
||||
"settings.appearance.preview.neutral": "Neutral",
|
||||
"settings.appearance.preview.seed": "Seed",
|
||||
"settings.appearance.preview.neutral_light": "White",
|
||||
"settings.appearance.preview.neutral_dark": "Black",
|
||||
"settings.appearance.preview.apply_seed": "Apply",
|
||||
"settings.appearance.preview.wallpaper_candidates": "Wallpaper seed candidates",
|
||||
"settings.appearance.preview.wallpaper_current": "Current",
|
||||
"settings.wallpaper.placement.fill": "Fill",
|
||||
"settings.wallpaper.placement.fit": "Fit",
|
||||
"settings.wallpaper.placement.stretch": "Stretch",
|
||||
@@ -345,6 +379,7 @@
|
||||
"settings.restart_dialog.title": "Restart required",
|
||||
"settings.restart_dialog.render_mode_message": "Restart the app to switch the rendering mode from \"{0}\" to \"{1}\". Restart now?",
|
||||
"settings.restart_dialog.restart": "Restart now",
|
||||
"settings.restart_dialog.later": "Later",
|
||||
"settings.restart_dialog.cancel": "Cancel",
|
||||
"settings.restart_dock.title": "Restart required",
|
||||
"settings.restart_dock.description": "Some changes will take effect after restarting the app.",
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
"settings.nav.weather": "天气",
|
||||
"settings.nav.region": "地区",
|
||||
"settings.nav.update": "更新",
|
||||
"settings.nav.privacy": "隐私",
|
||||
"settings.nav.launcher": "应用启动台",
|
||||
"settings.nav.plugins": "插件",
|
||||
"settings.nav.about": "关于",
|
||||
@@ -97,6 +98,12 @@
|
||||
"settings.status_bar.spacing_mode_custom": "自定义",
|
||||
"settings.status_bar.spacing_custom_label": "自定义间距(%)",
|
||||
"settings.status_bar.spacing_custom_px_format": "≈ {0:F1}px",
|
||||
"settings.privacy.title": "隐私",
|
||||
"settings.privacy.description": "管理可选的匿名上传设置,帮助我们逐步改进应用体验。",
|
||||
"settings.privacy.crash_upload_title": "匿名上传崩溃数据",
|
||||
"settings.privacy.crash_upload_description": "帮助我们提高应用稳定性。",
|
||||
"settings.privacy.usage_upload_title": "匿名上传使用数据",
|
||||
"settings.privacy.usage_upload_description": "帮助我们改善应用功能。",
|
||||
"settings.weather.title": "天气",
|
||||
"settings.weather.description": "配置天气位置、小米天气预览和启动时的位置刷新行为。",
|
||||
"settings.weather.location_source_header": "位置来源",
|
||||
@@ -247,11 +254,38 @@
|
||||
"settings.general.preview_date_label": "日期",
|
||||
"settings.general.render_mode_restart_message": "渲染模式变更需要重启应用。",
|
||||
"settings.appearance.title": "外观",
|
||||
"settings.appearance.description": "调整主题、壁纸与窗口外观。",
|
||||
"settings.appearance.description": "调整主题来源、系统材质与窗口外观。",
|
||||
"settings.appearance.theme_header": "主题",
|
||||
"settings.color.enable_night_mode_toggle": "启用夜间模式",
|
||||
"settings.color.use_system_chrome_toggle": "使用系统窗口标题栏",
|
||||
"settings.color.theme_color_label": "主题强调色",
|
||||
"settings.appearance.theme_color_mode_label": "主题色来源",
|
||||
"settings.appearance.system_material_label": "系统材质",
|
||||
"settings.appearance.theme_color_mode.neutral": "默认中性",
|
||||
"settings.appearance.theme_color_mode.user": "用户主题色 Monet",
|
||||
"settings.appearance.theme_color_mode.wallpaper": "壁纸 Monet 取色",
|
||||
"settings.appearance.theme_color_mode_desc.neutral": "使用标准的日间白底黑字与夜间黑底白字中性色表面。",
|
||||
"settings.appearance.theme_color_mode_desc.user": "使用用户选择的主题色作为整个桌面壳层的 Monet 种子色。",
|
||||
"settings.appearance.theme_color_mode_desc.wallpaper": "使用壁纸颜色。优先取应用壁纸,失败后回退系统桌面壁纸。",
|
||||
"settings.appearance.theme_color_preview.app": "当前正在预览从应用壁纸提取的颜色。",
|
||||
"settings.appearance.theme_color_preview.system": "当前正在预览从系统壁纸提取的颜色。",
|
||||
"settings.appearance.theme_color_preview.fallback": "没有可用壁纸,当前使用回退强调色。",
|
||||
"settings.appearance.system_material.none": "无",
|
||||
"settings.appearance.system_material.mica": "Mica",
|
||||
"settings.appearance.system_material.acrylic": "Acrylic",
|
||||
"settings.appearance.system_material_desc.switchable": "将所选材质应用到窗口、Dock、状态栏和组件宿主背板。",
|
||||
"settings.appearance.system_material_desc.fixed": "当前系统仅提供这里列出的材质模式。",
|
||||
"settings.appearance.restart_message": "主题色来源和系统材质更改需要重启应用。",
|
||||
"settings.appearance.preview.primary": "主色",
|
||||
"settings.appearance.preview.secondary": "次色",
|
||||
"settings.appearance.preview.tertiary": "三次色",
|
||||
"settings.appearance.preview.neutral": "中性色",
|
||||
"settings.appearance.preview.seed": "种子色",
|
||||
"settings.appearance.preview.neutral_light": "白色",
|
||||
"settings.appearance.preview.neutral_dark": "黑色",
|
||||
"settings.appearance.preview.apply_seed": "应用",
|
||||
"settings.appearance.preview.wallpaper_candidates": "壁纸候选主题色",
|
||||
"settings.appearance.preview.wallpaper_current": "当前",
|
||||
"settings.wallpaper.placement.fill": "填充",
|
||||
"settings.wallpaper.placement.fit": "适应",
|
||||
"settings.wallpaper.placement.stretch": "拉伸",
|
||||
@@ -350,6 +384,7 @@
|
||||
"settings.restart_dialog.title": "需要重启应用",
|
||||
"settings.restart_dialog.render_mode_message": "需要重启应用,才能将渲染模式从“{0}”切换到“{1}”。是否现在重启?",
|
||||
"settings.restart_dialog.restart": "立即重启",
|
||||
"settings.restart_dialog.later": "稍后",
|
||||
"settings.restart_dialog.cancel": "取消",
|
||||
"settings.restart_dock.title": "需要重启应用",
|
||||
"settings.restart_dock.description": "部分更改需要在重启应用后才会生效。",
|
||||
|
||||
@@ -16,6 +16,12 @@ public sealed class AppSettingsSnapshot
|
||||
|
||||
public bool UseSystemChrome { get; set; }
|
||||
|
||||
public string ThemeColorMode { get; set; } = "default_neutral";
|
||||
|
||||
public string SystemMaterialMode { get; set; } = "none";
|
||||
|
||||
public string? SelectedWallpaperSeed { get; set; }
|
||||
|
||||
public string? WallpaperPath { get; set; }
|
||||
|
||||
public string WallpaperType { get; set; } = "Image";
|
||||
@@ -60,6 +66,10 @@ public sealed class AppSettingsSnapshot
|
||||
|
||||
public bool IncludePrereleaseUpdates { get; set; }
|
||||
|
||||
public bool UploadAnonymousCrashData { get; set; }
|
||||
|
||||
public bool UploadAnonymousUsageData { get; set; }
|
||||
|
||||
public string UpdateChannel { get; set; } = "stable";
|
||||
|
||||
public string UpdateMode { get; set; } = "download_then_confirm";
|
||||
|
||||
@@ -1,8 +1,49 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using Avalonia.Media;
|
||||
|
||||
namespace LanMountainDesktop.Models;
|
||||
|
||||
public sealed record MonetPalette(
|
||||
IReadOnlyList<Color> RecommendedColors,
|
||||
IReadOnlyList<Color> MonetColors);
|
||||
public sealed record MonetPalette
|
||||
{
|
||||
public MonetPalette(
|
||||
IReadOnlyList<Color> recommendedColors,
|
||||
Color seed,
|
||||
Color primary,
|
||||
Color secondary,
|
||||
Color tertiary,
|
||||
Color neutral,
|
||||
Color neutralVariant)
|
||||
{
|
||||
RecommendedColors = recommendedColors;
|
||||
Seed = seed;
|
||||
Primary = primary;
|
||||
Secondary = secondary;
|
||||
Tertiary = tertiary;
|
||||
Neutral = neutral;
|
||||
NeutralVariant = neutralVariant;
|
||||
MonetColors =
|
||||
[
|
||||
primary,
|
||||
secondary,
|
||||
tertiary,
|
||||
neutral,
|
||||
neutralVariant
|
||||
];
|
||||
}
|
||||
|
||||
public IReadOnlyList<Color> RecommendedColors { get; }
|
||||
|
||||
public IReadOnlyList<Color> MonetColors { get; }
|
||||
|
||||
public Color Seed { get; }
|
||||
|
||||
public Color Primary { get; }
|
||||
|
||||
public Color Secondary { get; }
|
||||
|
||||
public Color Tertiary { get; }
|
||||
|
||||
public Color Neutral { get; }
|
||||
|
||||
public Color NeutralVariant { get; }
|
||||
}
|
||||
|
||||
1069
LanMountainDesktop/Services/AppearanceThemeService.cs
Normal file
1069
LanMountainDesktop/Services/AppearanceThemeService.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -49,15 +49,19 @@ internal static class ComponentEditorMaterialThemeAdapter
|
||||
ArgumentNullException.ThrowIfNull(monetPalette);
|
||||
|
||||
var isNightMode = themeState.IsNightMode;
|
||||
var monetColors = monetPalette.MonetColors?.Where(color => color.A > 0).ToArray() ?? [];
|
||||
var fallbackThemeColor = TryParseColor(themeState.ThemeColor);
|
||||
var useWallpaperPalette = wallpaperMediaType == WallpaperMediaType.Image && monetColors.Length > 0;
|
||||
var useWallpaperPalette = wallpaperMediaType == WallpaperMediaType.Image && monetPalette.Primary.A > 0;
|
||||
|
||||
var primary = useWallpaperPalette
|
||||
? monetColors[0]
|
||||
: fallbackThemeColor ?? monetColors.FirstOrDefault(DefaultPrimary);
|
||||
var secondary = ResolveSecondaryColor(primary, monetColors, isNightMode);
|
||||
var tertiary = ResolveTertiaryColor(primary, secondary, monetColors, isNightMode);
|
||||
? monetPalette.Primary
|
||||
: fallbackThemeColor ?? monetPalette.Primary;
|
||||
if (primary == default)
|
||||
{
|
||||
primary = DefaultPrimary;
|
||||
}
|
||||
|
||||
var secondary = ResolveSecondaryColor(primary, monetPalette, isNightMode);
|
||||
var tertiary = ResolveTertiaryColor(primary, secondary, monetPalette, isNightMode);
|
||||
|
||||
var backgroundBase = isNightMode ? DarkBackgroundBase : LightBackgroundBase;
|
||||
var surfaceBase = isNightMode ? DarkSurfaceBase : LightSurfaceBase;
|
||||
@@ -120,11 +124,11 @@ internal static class ComponentEditorMaterialThemeAdapter
|
||||
onPrimary);
|
||||
}
|
||||
|
||||
private static Color ResolveSecondaryColor(Color primary, IReadOnlyList<Color> monetColors, bool isNightMode)
|
||||
private static Color ResolveSecondaryColor(Color primary, MonetPalette monetPalette, bool isNightMode)
|
||||
{
|
||||
if (monetColors.Count > 1)
|
||||
if (monetPalette.Secondary != default)
|
||||
{
|
||||
return monetColors[1];
|
||||
return monetPalette.Secondary;
|
||||
}
|
||||
|
||||
return ColorMath.Blend(
|
||||
@@ -136,12 +140,12 @@ internal static class ComponentEditorMaterialThemeAdapter
|
||||
private static Color ResolveTertiaryColor(
|
||||
Color primary,
|
||||
Color secondary,
|
||||
IReadOnlyList<Color> monetColors,
|
||||
MonetPalette monetPalette,
|
||||
bool isNightMode)
|
||||
{
|
||||
if (monetColors.Count > 2)
|
||||
if (monetPalette.Tertiary != default)
|
||||
{
|
||||
return monetColors[2];
|
||||
return monetPalette.Tertiary;
|
||||
}
|
||||
|
||||
var blendTarget = isNightMode ? Color.Parse("#FFFFFFFF") : Color.Parse("#FF2A2230");
|
||||
|
||||
@@ -31,13 +31,16 @@ public interface IComponentEditorWindowService
|
||||
internal sealed class ComponentEditorWindowService : IComponentEditorWindowService
|
||||
{
|
||||
private readonly ISettingsFacadeService _settingsFacade;
|
||||
private readonly IAppearanceThemeService _appearanceThemeService;
|
||||
private ComponentEditorWindow? _window;
|
||||
private string? _currentPlacementId;
|
||||
|
||||
public ComponentEditorWindowService(ISettingsFacadeService settingsFacade)
|
||||
{
|
||||
_settingsFacade = settingsFacade ?? throw new ArgumentNullException(nameof(settingsFacade));
|
||||
_appearanceThemeService = HostAppearanceThemeProvider.GetOrCreate();
|
||||
_settingsFacade.Settings.Changed += OnSettingsChanged;
|
||||
_appearanceThemeService.Changed += OnAppearanceThemeChanged;
|
||||
}
|
||||
|
||||
public bool IsOpen => _window is { IsVisible: true };
|
||||
@@ -105,12 +108,15 @@ internal sealed class ComponentEditorWindowService : IComponentEditorWindowServi
|
||||
}
|
||||
|
||||
var changedKeys = e.ChangedKeys?.ToArray() ?? [];
|
||||
var liveAppearance = _appearanceThemeService.GetCurrent();
|
||||
if (changedKeys.Length > 0 &&
|
||||
!changedKeys.Contains(nameof(AppSettingsSnapshot.IsNightMode), StringComparer.OrdinalIgnoreCase) &&
|
||||
!changedKeys.Contains(nameof(AppSettingsSnapshot.ThemeColor), StringComparer.OrdinalIgnoreCase) &&
|
||||
!changedKeys.Contains(nameof(AppSettingsSnapshot.WallpaperPath), StringComparer.OrdinalIgnoreCase) &&
|
||||
!changedKeys.Contains(nameof(AppSettingsSnapshot.WallpaperType), StringComparer.OrdinalIgnoreCase) &&
|
||||
!changedKeys.Contains(nameof(AppSettingsSnapshot.WallpaperColor), StringComparer.OrdinalIgnoreCase) &&
|
||||
!(string.Equals(liveAppearance.ThemeColorMode, ThemeAppearanceValues.ColorModeSeedMonet, StringComparison.OrdinalIgnoreCase) &&
|
||||
changedKeys.Contains(nameof(AppSettingsSnapshot.ThemeColor), StringComparer.OrdinalIgnoreCase)) &&
|
||||
!(string.Equals(liveAppearance.ThemeColorMode, ThemeAppearanceValues.ColorModeWallpaperMonet, StringComparison.OrdinalIgnoreCase) &&
|
||||
(changedKeys.Contains(nameof(AppSettingsSnapshot.WallpaperPath), StringComparer.OrdinalIgnoreCase) ||
|
||||
changedKeys.Contains(nameof(AppSettingsSnapshot.WallpaperType), StringComparer.OrdinalIgnoreCase) ||
|
||||
changedKeys.Contains(nameof(AppSettingsSnapshot.WallpaperColor), StringComparer.OrdinalIgnoreCase))) &&
|
||||
!changedKeys.Contains(nameof(AppSettingsSnapshot.UseSystemChrome), StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
return;
|
||||
@@ -121,21 +127,33 @@ internal sealed class ComponentEditorWindowService : IComponentEditorWindowServi
|
||||
|
||||
private void ApplyTheme(ComponentEditorWindow window)
|
||||
{
|
||||
var appearanceSnapshot = _appearanceThemeService.GetCurrent();
|
||||
var themeState = _settingsFacade.Theme.Get();
|
||||
var wallpaperState = _settingsFacade.Wallpaper.Get();
|
||||
var wallpaperMediaType = _settingsFacade.WallpaperMedia.DetectMediaType(wallpaperState.WallpaperPath);
|
||||
var monetPalette = _settingsFacade.Theme.BuildPalette(
|
||||
themeState.IsNightMode,
|
||||
wallpaperState.WallpaperPath,
|
||||
themeState.ThemeColor);
|
||||
var wallpaperMediaType = _settingsFacade.WallpaperMedia.DetectMediaType(
|
||||
appearanceSnapshot.ResolvedWallpaperPath ?? wallpaperState.WallpaperPath);
|
||||
var palette = ComponentEditorMaterialThemeAdapter.Build(
|
||||
themeState,
|
||||
wallpaperState,
|
||||
monetPalette,
|
||||
appearanceSnapshot.MonetPalette,
|
||||
wallpaperMediaType);
|
||||
|
||||
window.ApplyTheme(palette);
|
||||
window.ApplyChromeMode(themeState.UseSystemChrome);
|
||||
_appearanceThemeService.ApplyWindowMaterial(window, MaterialSurfaceRole.WindowBackground);
|
||||
}
|
||||
|
||||
private void OnAppearanceThemeChanged(object? sender, AppearanceThemeSnapshot e)
|
||||
{
|
||||
_ = sender;
|
||||
_ = e;
|
||||
|
||||
if (_window is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ApplyTheme(_window);
|
||||
}
|
||||
|
||||
private sealed class HostContext : IComponentEditorHostContext
|
||||
|
||||
@@ -9,57 +9,95 @@ public static class GlassEffectService
|
||||
{
|
||||
public static void ApplyGlassResources(IResourceDictionary resources, ThemeColorContext context)
|
||||
{
|
||||
var materialSurfaceService = new MaterialSurfaceService();
|
||||
var monetPalette = context.MonetPalette;
|
||||
var monetColors = context.MonetColors?.Where(color => color.A > 0).ToArray() ?? [];
|
||||
var primary = monetColors.Length > 0 ? monetColors[0] : context.AccentColor;
|
||||
var secondary = monetColors.Length > 1
|
||||
? monetColors[1]
|
||||
: ColorMath.Blend(primary, Color.Parse("#FFFFFFFF"), 0.12);
|
||||
|
||||
var panelBase = context.IsNightMode
|
||||
? ColorMath.Blend(Color.Parse("#FF101722"), primary, 0.26)
|
||||
: ColorMath.Blend(Color.Parse("#FFF9FBFE"), primary, 0.14);
|
||||
var panelRaised = context.IsNightMode
|
||||
? ColorMath.Blend(Color.Parse("#FF15202C"), secondary, 0.30)
|
||||
: ColorMath.Blend(Color.Parse("#FFFFFFFF"), secondary, 0.18);
|
||||
var overlayBase = context.IsNightMode
|
||||
? ColorMath.Blend(Color.Parse("#FF0E1622"), primary, 0.36)
|
||||
: ColorMath.Blend(Color.Parse("#FFF3F7FD"), primary, 0.20);
|
||||
var primary = context.UseNeutralSurfaces
|
||||
? context.AccentColor
|
||||
: monetPalette?.Primary ?? (monetColors.Length > 0 ? monetColors[0] : context.AccentColor);
|
||||
var neutralButtonBase = context.IsNightMode
|
||||
? Color.Parse("#FF171C24")
|
||||
: Color.Parse("#FFFFFFFF");
|
||||
if (!context.UseNeutralSurfaces)
|
||||
{
|
||||
neutralButtonBase = ColorMath.Blend(
|
||||
neutralButtonBase,
|
||||
primary,
|
||||
context.IsNightMode ? 0.08 : 0.04);
|
||||
}
|
||||
|
||||
var buttonBackground = Color.FromArgb(
|
||||
context.IsNightMode ? (byte)0x4D : (byte)0x52,
|
||||
panelRaised.R,
|
||||
panelRaised.G,
|
||||
panelRaised.B);
|
||||
var buttonBorder = Color.FromArgb(
|
||||
context.IsNightMode ? (byte)0x36 : (byte)0x26,
|
||||
primary.R,
|
||||
primary.G,
|
||||
primary.B);
|
||||
context.IsNightMode ? (byte)0xF0 : (byte)0xFF,
|
||||
neutralButtonBase.R,
|
||||
neutralButtonBase.G,
|
||||
neutralButtonBase.B);
|
||||
var buttonBorder = ColorMath.WithAlpha(
|
||||
context.IsNightMode
|
||||
? ColorMath.Blend(neutralButtonBase, Color.Parse("#FFFFFFFF"), 0.14)
|
||||
: ColorMath.Blend(neutralButtonBase, Color.Parse("#FF334155"), 0.10),
|
||||
context.IsNightMode ? (byte)0x26 : (byte)0x14);
|
||||
|
||||
resources["AdaptiveButtonBackgroundBrush"] = new SolidColorBrush(buttonBackground);
|
||||
resources["AdaptiveButtonBorderBrush"] = new SolidColorBrush(buttonBorder);
|
||||
resources["AdaptiveButtonHoverBackgroundBrush"] = new SolidColorBrush(
|
||||
ColorMath.WithAlpha(ColorMath.Blend(buttonBackground, primary, 0.18), context.IsNightMode ? (byte)0x72 : (byte)0x7A));
|
||||
ColorMath.WithAlpha(
|
||||
ColorMath.Blend(buttonBackground, primary, context.IsNightMode ? 0.14 : 0.08),
|
||||
context.IsNightMode ? (byte)0xF4 : (byte)0xFF));
|
||||
resources["AdaptiveButtonPressedBackgroundBrush"] = new SolidColorBrush(
|
||||
ColorMath.WithAlpha(ColorMath.Blend(buttonBackground, primary, 0.30), context.IsNightMode ? (byte)0x8A : (byte)0x8C));
|
||||
ColorMath.WithAlpha(
|
||||
ColorMath.Blend(buttonBackground, primary, context.IsNightMode ? 0.24 : 0.16),
|
||||
context.IsNightMode ? (byte)0xF8 : (byte)0xFF));
|
||||
|
||||
resources["AdaptiveGlassPanelBackgroundBrush"] = new SolidColorBrush(
|
||||
Color.FromArgb(context.IsNightMode ? (byte)0xF2 : (byte)0xFA, panelBase.R, panelBase.G, panelBase.B));
|
||||
resources["AdaptiveGlassPanelBorderBrush"] = new SolidColorBrush(
|
||||
Color.FromArgb(context.IsNightMode ? (byte)0x38 : (byte)0x24, primary.R, primary.G, primary.B));
|
||||
resources["AdaptiveGlassStrongBackgroundBrush"] = new SolidColorBrush(
|
||||
Color.FromArgb(context.IsNightMode ? (byte)0xF6 : (byte)0xFC, panelRaised.R, panelRaised.G, panelRaised.B));
|
||||
resources["AdaptiveGlassStrongBorderBrush"] = new SolidColorBrush(
|
||||
Color.FromArgb(context.IsNightMode ? (byte)0x4A : (byte)0x2C, secondary.R, secondary.G, secondary.B));
|
||||
resources["AdaptiveGlassOverlayBackgroundBrush"] = new SolidColorBrush(
|
||||
Color.FromArgb(context.IsNightMode ? (byte)0xEA : (byte)0xF4, overlayBase.R, overlayBase.G, overlayBase.B));
|
||||
var windowSurface = materialSurfaceService.GetSurface(context, MaterialSurfaceRole.WindowBackground);
|
||||
var settingsWindowSurface = materialSurfaceService.GetSurface(context, MaterialSurfaceRole.SettingsWindowBackground);
|
||||
var dockSurface = materialSurfaceService.GetSurface(context, MaterialSurfaceRole.DockBackground);
|
||||
var statusBarSurface = materialSurfaceService.GetSurface(context, MaterialSurfaceRole.StatusBarBackground);
|
||||
var desktopComponentSurface = materialSurfaceService.GetSurface(context, MaterialSurfaceRole.DesktopComponentHost);
|
||||
var statusBarComponentSurface = materialSurfaceService.GetSurface(context, MaterialSurfaceRole.StatusBarComponentHost);
|
||||
var overlaySurface = materialSurfaceService.GetSurface(context, MaterialSurfaceRole.OverlayPanel);
|
||||
var strongSurfaceColor = ColorMath.Blend(
|
||||
desktopComponentSurface.BackgroundColor,
|
||||
overlaySurface.BackgroundColor,
|
||||
context.IsNightMode ? 0.18 : 0.12);
|
||||
var strongBorderColor = ColorMath.WithAlpha(
|
||||
desktopComponentSurface.BorderColor,
|
||||
context.IsNightMode ? (byte)0x20 : (byte)0x12);
|
||||
var panelBorderColor = ColorMath.WithAlpha(
|
||||
desktopComponentSurface.BorderColor,
|
||||
context.IsNightMode ? (byte)0x18 : (byte)0x10);
|
||||
|
||||
resources["AdaptiveGlassPanelBlurRadius"] = context.IsNightMode ? 22.0 : 28.0;
|
||||
resources["AdaptiveGlassStrongBlurRadius"] = context.IsNightMode ? 28.0 : 34.0;
|
||||
resources["AdaptiveGlassOverlayBlurRadius"] = context.IsNightMode ? 34.0 : 40.0;
|
||||
resources["AdaptiveWindowBackgroundBrush"] = new SolidColorBrush(windowSurface.BackgroundColor);
|
||||
resources["AdaptiveWindowBorderBrush"] = new SolidColorBrush(windowSurface.BorderColor);
|
||||
resources["AdaptiveSettingsWindowBackgroundBrush"] = new SolidColorBrush(settingsWindowSurface.BackgroundColor);
|
||||
resources["AdaptiveSettingsWindowBorderBrush"] = new SolidColorBrush(settingsWindowSurface.BorderColor);
|
||||
resources["AdaptiveDockBackgroundBrush"] = new SolidColorBrush(dockSurface.BackgroundColor);
|
||||
resources["AdaptiveDockBorderBrush"] = new SolidColorBrush(dockSurface.BorderColor);
|
||||
resources["AdaptiveStatusBarBackgroundBrush"] = new SolidColorBrush(statusBarSurface.BackgroundColor);
|
||||
resources["AdaptiveStatusBarBorderBrush"] = new SolidColorBrush(statusBarSurface.BorderColor);
|
||||
resources["AdaptiveDesktopComponentHostBackgroundBrush"] = new SolidColorBrush(desktopComponentSurface.BackgroundColor);
|
||||
resources["AdaptiveDesktopComponentHostBorderBrush"] = new SolidColorBrush(desktopComponentSurface.BorderColor);
|
||||
resources["AdaptiveStatusBarComponentHostBackgroundBrush"] = new SolidColorBrush(statusBarComponentSurface.BackgroundColor);
|
||||
resources["AdaptiveStatusBarComponentHostBorderBrush"] = new SolidColorBrush(statusBarComponentSurface.BorderColor);
|
||||
|
||||
resources["AdaptiveGlassPanelBackgroundBrush"] = new SolidColorBrush(desktopComponentSurface.BackgroundColor);
|
||||
resources["AdaptiveGlassPanelBorderBrush"] = new SolidColorBrush(panelBorderColor);
|
||||
resources["AdaptiveGlassStrongBackgroundBrush"] = new SolidColorBrush(strongSurfaceColor);
|
||||
resources["AdaptiveGlassStrongBorderBrush"] = new SolidColorBrush(strongBorderColor);
|
||||
resources["AdaptiveDockGlassBackgroundBrush"] = new SolidColorBrush(dockSurface.BackgroundColor);
|
||||
resources["AdaptiveDockGlassBorderBrush"] = new SolidColorBrush(dockSurface.BorderColor);
|
||||
resources["AdaptiveGlassOverlayBackgroundBrush"] = new SolidColorBrush(overlaySurface.BackgroundColor);
|
||||
|
||||
resources["AdaptiveGlassPanelBlurRadius"] = desktopComponentSurface.BlurRadius;
|
||||
resources["AdaptiveGlassStrongBlurRadius"] = dockSurface.BlurRadius;
|
||||
resources["AdaptiveGlassOverlayBlurRadius"] = overlaySurface.BlurRadius;
|
||||
resources["AdaptiveGlassPanelOpacity"] = 1.0;
|
||||
resources["AdaptiveGlassStrongOpacity"] = 1.0;
|
||||
resources["AdaptiveGlassOverlayOpacity"] = context.IsNightMode ? 0.95 : 0.98;
|
||||
resources["AdaptiveGlassOverlayOpacity"] = overlaySurface.Opacity;
|
||||
resources["AdaptiveGlassNoiseOpacity"] = context.IsNightMode ? 0.012 : 0.008;
|
||||
|
||||
resources["AdaptiveDockOpacity"] = dockSurface.Opacity;
|
||||
resources["AdaptiveStatusBarOpacity"] = statusBarSurface.Opacity;
|
||||
resources["AdaptiveDesktopComponentHostOpacity"] = desktopComponentSurface.Opacity;
|
||||
resources["AdaptiveStatusBarComponentHostOpacity"] = statusBarComponentSurface.Opacity;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,84 +1,86 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using Avalonia;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Media.Imaging;
|
||||
using Avalonia.Platform;
|
||||
using LanMountainDesktop.Models;
|
||||
using MaterialColorUtilities.Palettes;
|
||||
using MaterialColorUtilities.Utils;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace LanMountainDesktop.Services;
|
||||
|
||||
public sealed class MonetColorService
|
||||
{
|
||||
private static readonly Color DefaultSeedColor = Color.Parse("#FF3B82F6");
|
||||
|
||||
public MonetPalette BuildPalette(Bitmap? wallpaper, bool nightMode, Color? preferredSeed = null)
|
||||
{
|
||||
var recommended = BuildRecommendedPalette(nightMode);
|
||||
var seed = preferredSeed ?? TryExtractSeedColor(wallpaper) ?? TryGetSystemMonetSeedColor() ?? Color.Parse("#FF3B82F6");
|
||||
var monet = BuildMonetPalette(seed, nightMode);
|
||||
return new MonetPalette(recommended, monet);
|
||||
var wallpaperCandidates = wallpaper is null
|
||||
? []
|
||||
: ExtractSeedCandidates(wallpaper);
|
||||
return BuildPaletteCore(wallpaperCandidates, nightMode, preferredSeed);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<Color> BuildRecommendedPalette(bool nightMode)
|
||||
public MonetPalette BuildPaletteFromSeedCandidates(
|
||||
IReadOnlyList<Color>? seedCandidates,
|
||||
bool nightMode,
|
||||
Color? preferredSeed = null)
|
||||
{
|
||||
if (nightMode)
|
||||
{
|
||||
return
|
||||
[
|
||||
Color.Parse("#FF3B82F6"),
|
||||
Color.Parse("#FF22C55E"),
|
||||
Color.Parse("#FFF59E0B"),
|
||||
Color.Parse("#FFF97316"),
|
||||
Color.Parse("#FFA855F7"),
|
||||
Color.Parse("#FFEF4444")
|
||||
];
|
||||
}
|
||||
|
||||
return
|
||||
[
|
||||
Color.Parse("#FF1D4ED8"),
|
||||
Color.Parse("#FF15803D"),
|
||||
Color.Parse("#FFB45309"),
|
||||
Color.Parse("#FFC2410C"),
|
||||
Color.Parse("#FF7E22CE"),
|
||||
Color.Parse("#FFB91C1C")
|
||||
];
|
||||
return BuildPaletteCore(seedCandidates ?? [], nightMode, preferredSeed);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<Color> BuildMonetPalette(Color seed, bool nightMode)
|
||||
public IReadOnlyList<Color> ExtractSeedCandidates(Bitmap wallpaper)
|
||||
{
|
||||
var (hue, saturation, value) = ToHsv(seed);
|
||||
var valueBase = nightMode ? Math.Max(0.70, value) : Math.Min(0.72, Math.Max(0.35, value));
|
||||
var saturationBase = Math.Clamp(saturation, 0.22, 0.74);
|
||||
var offsets = new[] { 0d, 16d, -16d, 36d, -36d, 180d };
|
||||
var palette = new List<Color>(offsets.Length);
|
||||
|
||||
for (var i = 0; i < offsets.Length; i++)
|
||||
{
|
||||
var hueShift = NormalizeHue(hue + offsets[i]);
|
||||
var sat = Math.Clamp(saturationBase + ((i % 2 == 0) ? 0.05 : -0.05), 0.18, 0.86);
|
||||
var val = Math.Clamp(valueBase + ((i < 3) ? 0.06 : -0.04), 0.32, 0.92);
|
||||
palette.Add(FromHsv(hueShift, sat, val));
|
||||
}
|
||||
|
||||
return palette;
|
||||
ArgumentNullException.ThrowIfNull(wallpaper);
|
||||
return ExtractWallpaperSeedCandidates(wallpaper);
|
||||
}
|
||||
|
||||
private static Color? TryExtractSeedColor(Bitmap? wallpaper)
|
||||
private static Color? ResolveSeedColor(
|
||||
IReadOnlyList<Color> wallpaperCandidates,
|
||||
Color? preferredSeed)
|
||||
{
|
||||
if (wallpaper is null)
|
||||
if (wallpaperCandidates.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (preferredSeed is { } explicitSeed)
|
||||
{
|
||||
var exact = wallpaperCandidates.FirstOrDefault(candidate => candidate == explicitSeed);
|
||||
if (exact != default)
|
||||
{
|
||||
return exact;
|
||||
}
|
||||
}
|
||||
|
||||
return wallpaperCandidates[0];
|
||||
}
|
||||
|
||||
private static IReadOnlyList<Color> BuildFallbackSeedCandidates()
|
||||
{
|
||||
return
|
||||
[
|
||||
Color.Parse("#FF3B82F6"),
|
||||
Color.Parse("#FF22C55E"),
|
||||
Color.Parse("#FFF59E0B"),
|
||||
Color.Parse("#FFF97316"),
|
||||
Color.Parse("#FFA855F7")
|
||||
];
|
||||
}
|
||||
|
||||
private static IReadOnlyList<Color> ExtractWallpaperSeedCandidates(Bitmap wallpaper)
|
||||
{
|
||||
try
|
||||
{
|
||||
var sampleWidth = Math.Clamp(wallpaper.PixelSize.Width, 1, 48);
|
||||
var sampleHeight = Math.Clamp(wallpaper.PixelSize.Height, 1, 48);
|
||||
var width = Math.Clamp(wallpaper.PixelSize.Width, 1, 96);
|
||||
var height = Math.Clamp(wallpaper.PixelSize.Height, 1, 96);
|
||||
|
||||
using var scaledBitmap = wallpaper.CreateScaledBitmap(
|
||||
new PixelSize(sampleWidth, sampleHeight),
|
||||
new PixelSize(width, height),
|
||||
BitmapInterpolationMode.MediumQuality);
|
||||
using var writeable = new WriteableBitmap(
|
||||
scaledBitmap.PixelSize,
|
||||
@@ -91,55 +93,52 @@ public sealed class MonetColorService
|
||||
var byteCount = framebuffer.RowBytes * framebuffer.Size.Height;
|
||||
if (byteCount <= 0 || framebuffer.Address == IntPtr.Zero)
|
||||
{
|
||||
return null;
|
||||
return [];
|
||||
}
|
||||
|
||||
var pixelBuffer = new byte[byteCount];
|
||||
Marshal.Copy(framebuffer.Address, pixelBuffer, 0, byteCount);
|
||||
|
||||
double bestScore = double.MinValue;
|
||||
Color? bestColor = null;
|
||||
|
||||
var argbPixels = new List<uint>(framebuffer.Size.Width * framebuffer.Size.Height);
|
||||
for (var y = 0; y < framebuffer.Size.Height; y++)
|
||||
{
|
||||
var rowOffset = y * framebuffer.RowBytes;
|
||||
for (var x = 0; x < framebuffer.Size.Width; x++)
|
||||
{
|
||||
var index = rowOffset + (x * 4);
|
||||
var alpha = pixelBuffer[index + 3] / 255d;
|
||||
if (alpha <= 0.15)
|
||||
var alpha = pixelBuffer[index + 3];
|
||||
if (alpha <= 32)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var blue = (pixelBuffer[index] / 255d) / alpha;
|
||||
var green = (pixelBuffer[index + 1] / 255d) / alpha;
|
||||
var red = (pixelBuffer[index + 2] / 255d) / alpha;
|
||||
red = Math.Clamp(red, 0, 1);
|
||||
green = Math.Clamp(green, 0, 1);
|
||||
blue = Math.Clamp(blue, 0, 1);
|
||||
|
||||
var color = Color.FromRgb(
|
||||
(byte)Math.Round(red * 255),
|
||||
(byte)Math.Round(green * 255),
|
||||
(byte)Math.Round(blue * 255));
|
||||
var (_, saturation, value) = ToHsv(color);
|
||||
var score = (saturation * 1.8) + (value * 0.6);
|
||||
if (score <= bestScore)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
bestScore = score;
|
||||
bestColor = color;
|
||||
var blue = pixelBuffer[index];
|
||||
var green = pixelBuffer[index + 1];
|
||||
var red = pixelBuffer[index + 2];
|
||||
argbPixels.Add(
|
||||
((uint)alpha << 24) |
|
||||
((uint)red << 16) |
|
||||
((uint)green << 8) |
|
||||
blue);
|
||||
}
|
||||
}
|
||||
|
||||
return bestColor;
|
||||
if (argbPixels.Count == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var extracted = ImageUtils.ColorsFromImage(argbPixels.ToArray());
|
||||
return extracted
|
||||
.Select(FromArgb)
|
||||
.Distinct()
|
||||
.Take(6)
|
||||
.ToArray();
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
return null;
|
||||
AppLogger.Warn("Appearance.WallpaperPalette", "Failed to extract wallpaper seed candidates.", ex);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,11 +160,17 @@ public sealed class MonetColorService
|
||||
return null;
|
||||
}
|
||||
|
||||
var bytes = BitConverter.GetBytes(accentDword);
|
||||
var blue = bytes[0];
|
||||
var green = bytes[1];
|
||||
var red = bytes[2];
|
||||
return Color.FromRgb(red, green, blue);
|
||||
var accentColor = unchecked((uint)accentDword);
|
||||
var a = (byte)((accentColor >> 24) & 0xFF);
|
||||
var b = (byte)((accentColor >> 16) & 0xFF);
|
||||
var g = (byte)((accentColor >> 8) & 0xFF);
|
||||
var r = (byte)(accentColor & 0xFF);
|
||||
if (a == 0)
|
||||
{
|
||||
a = 0xFF;
|
||||
}
|
||||
|
||||
return Color.FromArgb(a, r, g, b);
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -173,78 +178,51 @@ public sealed class MonetColorService
|
||||
}
|
||||
}
|
||||
|
||||
private static (double Hue, double Saturation, double Value) ToHsv(Color color)
|
||||
private static uint ToArgb(Color color)
|
||||
{
|
||||
var red = color.R / 255d;
|
||||
var green = color.G / 255d;
|
||||
var blue = color.B / 255d;
|
||||
var max = Math.Max(red, Math.Max(green, blue));
|
||||
var min = Math.Min(red, Math.Min(green, blue));
|
||||
var delta = max - min;
|
||||
|
||||
double hue;
|
||||
if (delta < 0.0001)
|
||||
{
|
||||
hue = 0;
|
||||
}
|
||||
else if (Math.Abs(max - red) < 0.0001)
|
||||
{
|
||||
hue = 60 * (((green - blue) / delta) % 6);
|
||||
}
|
||||
else if (Math.Abs(max - green) < 0.0001)
|
||||
{
|
||||
hue = 60 * (((blue - red) / delta) + 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
hue = 60 * (((red - green) / delta) + 4);
|
||||
}
|
||||
|
||||
hue = NormalizeHue(hue);
|
||||
var saturation = max <= 0.0001 ? 0 : delta / max;
|
||||
return (hue, saturation, max);
|
||||
return
|
||||
((uint)color.A << 24) |
|
||||
((uint)color.R << 16) |
|
||||
((uint)color.G << 8) |
|
||||
color.B;
|
||||
}
|
||||
|
||||
private static Color FromHsv(double hue, double saturation, double value)
|
||||
private static Color FromArgb(uint argb)
|
||||
{
|
||||
hue = NormalizeHue(hue);
|
||||
saturation = Math.Clamp(saturation, 0, 1);
|
||||
value = Math.Clamp(value, 0, 1);
|
||||
|
||||
if (saturation <= 0.0001)
|
||||
{
|
||||
var gray = (byte)Math.Round(value * 255);
|
||||
return Color.FromRgb(gray, gray, gray);
|
||||
}
|
||||
|
||||
var chroma = value * saturation;
|
||||
var x = chroma * (1 - Math.Abs(((hue / 60d) % 2) - 1));
|
||||
var m = value - chroma;
|
||||
|
||||
(double r, double g, double b) = hue switch
|
||||
{
|
||||
>= 0 and < 60 => (chroma, x, 0d),
|
||||
>= 60 and < 120 => (x, chroma, 0d),
|
||||
>= 120 and < 180 => (0d, chroma, x),
|
||||
>= 180 and < 240 => (0d, x, chroma),
|
||||
>= 240 and < 300 => (x, 0d, chroma),
|
||||
_ => (chroma, 0d, x)
|
||||
};
|
||||
|
||||
var red = (byte)Math.Round((r + m) * 255);
|
||||
var green = (byte)Math.Round((g + m) * 255);
|
||||
var blue = (byte)Math.Round((b + m) * 255);
|
||||
return Color.FromRgb(red, green, blue);
|
||||
var a = (byte)((argb >> 24) & 0xFF);
|
||||
var r = (byte)((argb >> 16) & 0xFF);
|
||||
var g = (byte)((argb >> 8) & 0xFF);
|
||||
var b = (byte)(argb & 0xFF);
|
||||
return Color.FromArgb(a, r, g, b);
|
||||
}
|
||||
|
||||
private static double NormalizeHue(double hue)
|
||||
private static MonetPalette BuildPaletteCore(
|
||||
IReadOnlyList<Color> wallpaperCandidates,
|
||||
bool nightMode,
|
||||
Color? preferredSeed)
|
||||
{
|
||||
hue %= 360;
|
||||
if (hue < 0)
|
||||
{
|
||||
hue += 360;
|
||||
}
|
||||
var recommendedColors = wallpaperCandidates.Count > 0
|
||||
? wallpaperCandidates
|
||||
: BuildFallbackSeedCandidates();
|
||||
var seed = ResolveSeedColor(wallpaperCandidates, preferredSeed)
|
||||
?? preferredSeed
|
||||
?? TryGetSystemMonetSeedColor()
|
||||
?? DefaultSeedColor;
|
||||
|
||||
return hue;
|
||||
var corePalette = CorePalette.Of(ToArgb(seed), Style.TonalSpot);
|
||||
var primary = FromArgb(corePalette.Primary.Tone(nightMode ? 80u : 40u));
|
||||
var secondary = FromArgb(corePalette.Secondary.Tone(nightMode ? 80u : 40u));
|
||||
var tertiary = FromArgb(corePalette.Tertiary.Tone(nightMode ? 80u : 40u));
|
||||
var neutral = FromArgb(corePalette.Neutral.Tone(nightMode ? 20u : 94u));
|
||||
var neutralVariant = FromArgb(corePalette.NeutralVariant.Tone(nightMode ? 30u : 90u));
|
||||
|
||||
return new MonetPalette(
|
||||
recommendedColors,
|
||||
seed,
|
||||
primary,
|
||||
secondary,
|
||||
tertiary,
|
||||
neutral,
|
||||
neutralVariant);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ public static class PendingRestartStateService
|
||||
{
|
||||
public const string RenderModeReason = "RenderMode";
|
||||
public const string PluginCatalogReason = "PluginCatalog";
|
||||
public const string SettingsWindowReason = "SettingsWindow";
|
||||
|
||||
private static readonly object Gate = new();
|
||||
private static readonly HashSet<string> PendingReasons = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using LanMountainDesktop.Models;
|
||||
using LanMountainDesktop.PluginSdk;
|
||||
using LanMountainDesktop.Services;
|
||||
|
||||
namespace LanMountainDesktop.Services.Settings;
|
||||
|
||||
@@ -16,7 +17,13 @@ public enum WallpaperMediaType
|
||||
|
||||
public sealed record GridSettingsState(int ShortSideCells, string SpacingPreset, int EdgeInsetPercent);
|
||||
public sealed record WallpaperSettingsState(string? WallpaperPath, string Type, string? Color, string Placement);
|
||||
public sealed record ThemeAppearanceSettingsState(bool IsNightMode, string? ThemeColor, bool UseSystemChrome);
|
||||
public sealed record ThemeAppearanceSettingsState(
|
||||
bool IsNightMode,
|
||||
string? ThemeColor,
|
||||
bool UseSystemChrome,
|
||||
string ThemeColorMode = ThemeAppearanceValues.ColorModeDefaultNeutral,
|
||||
string SystemMaterialMode = ThemeAppearanceValues.MaterialNone,
|
||||
string? SelectedWallpaperSeed = null);
|
||||
public sealed record StatusBarSettingsState(
|
||||
IReadOnlyList<string> TopStatusComponentIds,
|
||||
IReadOnlyList<string> PinnedTaskbarActions,
|
||||
@@ -37,6 +44,9 @@ public sealed record WeatherSettingsState(
|
||||
bool NoTlsRequests,
|
||||
string LocationQuery);
|
||||
public sealed record RegionSettingsState(string LanguageCode, string? TimeZoneId);
|
||||
public sealed record PrivacySettingsState(
|
||||
bool UploadAnonymousCrashData,
|
||||
bool UploadAnonymousUsageData);
|
||||
public sealed record UpdateSettingsState(
|
||||
bool AutoCheckUpdates,
|
||||
bool IncludePrereleaseUpdates,
|
||||
@@ -166,6 +176,12 @@ public interface IRegionSettingsService
|
||||
TimeZoneService GetTimeZoneService();
|
||||
}
|
||||
|
||||
public interface IPrivacySettingsService
|
||||
{
|
||||
PrivacySettingsState Get();
|
||||
void Save(PrivacySettingsState state);
|
||||
}
|
||||
|
||||
public interface IUpdateSettingsService
|
||||
{
|
||||
UpdateSettingsState Get();
|
||||
@@ -224,6 +240,7 @@ public interface ISettingsFacadeService
|
||||
IStatusBarSettingsService StatusBar { get; }
|
||||
IWeatherSettingsService Weather { get; }
|
||||
IRegionSettingsService Region { get; }
|
||||
IPrivacySettingsService Privacy { get; }
|
||||
IUpdateSettingsService Update { get; }
|
||||
ILauncherCatalogService LauncherCatalog { get; }
|
||||
ILauncherPolicyService LauncherPolicy { get; }
|
||||
|
||||
@@ -93,9 +93,12 @@ internal sealed class WallpaperSettingsService : IWallpaperSettingsService
|
||||
public WallpaperSettingsState Get()
|
||||
{
|
||||
var snapshot = _settingsService.Load();
|
||||
var normalizedType = snapshot.WallpaperType ?? "Image";
|
||||
return new WallpaperSettingsState(
|
||||
snapshot.WallpaperPath,
|
||||
snapshot.WallpaperType ?? "Image",
|
||||
string.Equals(normalizedType, "SolidColor", StringComparison.OrdinalIgnoreCase)
|
||||
? null
|
||||
: snapshot.WallpaperPath,
|
||||
normalizedType,
|
||||
snapshot.WallpaperColor,
|
||||
snapshot.WallpaperPlacement);
|
||||
}
|
||||
@@ -103,9 +106,24 @@ internal sealed class WallpaperSettingsService : IWallpaperSettingsService
|
||||
public void Save(WallpaperSettingsState state)
|
||||
{
|
||||
var snapshot = _settingsService.Load();
|
||||
snapshot.WallpaperPath = state.WallpaperPath;
|
||||
snapshot.WallpaperType = state.Type;
|
||||
snapshot.WallpaperColor = state.Color;
|
||||
var normalizedType = string.IsNullOrWhiteSpace(state.Type)
|
||||
? "Image"
|
||||
: state.Type.Trim();
|
||||
var normalizedPath = string.IsNullOrWhiteSpace(state.WallpaperPath)
|
||||
? null
|
||||
: state.WallpaperPath.Trim();
|
||||
var normalizedColor = string.IsNullOrWhiteSpace(state.Color)
|
||||
? null
|
||||
: state.Color.Trim();
|
||||
|
||||
if (string.Equals(normalizedType, "SolidColor", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
normalizedPath = null;
|
||||
}
|
||||
|
||||
snapshot.WallpaperPath = normalizedPath;
|
||||
snapshot.WallpaperType = normalizedType;
|
||||
snapshot.WallpaperColor = normalizedColor;
|
||||
snapshot.WallpaperPlacement = string.IsNullOrWhiteSpace(state.Placement)
|
||||
? "Fill"
|
||||
: state.Placement.Trim();
|
||||
@@ -233,24 +251,68 @@ internal sealed class ThemeAppearanceService : IThemeAppearanceService
|
||||
return new ThemeAppearanceSettingsState(
|
||||
snapshot.IsNightMode ?? false,
|
||||
snapshot.ThemeColor,
|
||||
snapshot.UseSystemChrome);
|
||||
snapshot.UseSystemChrome,
|
||||
ThemeAppearanceValues.NormalizeThemeColorMode(snapshot.ThemeColorMode, snapshot.ThemeColor),
|
||||
ThemeAppearanceValues.NormalizeSystemMaterialMode(snapshot.SystemMaterialMode),
|
||||
snapshot.SelectedWallpaperSeed);
|
||||
}
|
||||
|
||||
public void Save(ThemeAppearanceSettingsState state)
|
||||
{
|
||||
var snapshot = _settingsService.Load();
|
||||
snapshot.IsNightMode = state.IsNightMode;
|
||||
snapshot.ThemeColor = state.ThemeColor;
|
||||
snapshot.UseSystemChrome = state.UseSystemChrome;
|
||||
var changedKeys = new List<string>();
|
||||
var normalizedThemeColor = string.IsNullOrWhiteSpace(state.ThemeColor) ? null : state.ThemeColor;
|
||||
var normalizedThemeColorMode = ThemeAppearanceValues.NormalizeThemeColorMode(state.ThemeColorMode, state.ThemeColor);
|
||||
var normalizedSystemMaterialMode = ThemeAppearanceValues.NormalizeSystemMaterialMode(state.SystemMaterialMode);
|
||||
var normalizedSelectedWallpaperSeed = string.IsNullOrWhiteSpace(state.SelectedWallpaperSeed)
|
||||
? null
|
||||
: state.SelectedWallpaperSeed;
|
||||
|
||||
if ((snapshot.IsNightMode ?? false) != state.IsNightMode)
|
||||
{
|
||||
snapshot.IsNightMode = state.IsNightMode;
|
||||
changedKeys.Add(nameof(AppSettingsSnapshot.IsNightMode));
|
||||
}
|
||||
|
||||
if (!string.Equals(snapshot.ThemeColor, normalizedThemeColor, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
snapshot.ThemeColor = normalizedThemeColor;
|
||||
changedKeys.Add(nameof(AppSettingsSnapshot.ThemeColor));
|
||||
}
|
||||
|
||||
if (snapshot.UseSystemChrome != state.UseSystemChrome)
|
||||
{
|
||||
snapshot.UseSystemChrome = state.UseSystemChrome;
|
||||
changedKeys.Add(nameof(AppSettingsSnapshot.UseSystemChrome));
|
||||
}
|
||||
|
||||
if (!string.Equals(snapshot.ThemeColorMode, normalizedThemeColorMode, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
snapshot.ThemeColorMode = normalizedThemeColorMode;
|
||||
changedKeys.Add(nameof(AppSettingsSnapshot.ThemeColorMode));
|
||||
}
|
||||
|
||||
if (!string.Equals(snapshot.SystemMaterialMode, normalizedSystemMaterialMode, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
snapshot.SystemMaterialMode = normalizedSystemMaterialMode;
|
||||
changedKeys.Add(nameof(AppSettingsSnapshot.SystemMaterialMode));
|
||||
}
|
||||
|
||||
if (!string.Equals(snapshot.SelectedWallpaperSeed, normalizedSelectedWallpaperSeed, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
snapshot.SelectedWallpaperSeed = normalizedSelectedWallpaperSeed;
|
||||
changedKeys.Add(nameof(AppSettingsSnapshot.SelectedWallpaperSeed));
|
||||
}
|
||||
|
||||
if (changedKeys.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_settingsService.SaveSnapshot(
|
||||
SettingsScope.App,
|
||||
snapshot,
|
||||
changedKeys:
|
||||
[
|
||||
nameof(AppSettingsSnapshot.IsNightMode),
|
||||
nameof(AppSettingsSnapshot.ThemeColor),
|
||||
nameof(AppSettingsSnapshot.UseSystemChrome)
|
||||
]);
|
||||
changedKeys: changedKeys);
|
||||
}
|
||||
|
||||
public MonetPalette BuildPalette(bool nightMode, string? wallpaperPath, string? preferredSeedColor = null)
|
||||
@@ -525,6 +587,39 @@ internal sealed class RegionSettingsService : IRegionSettingsService
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class PrivacySettingsService : IPrivacySettingsService
|
||||
{
|
||||
private readonly ISettingsService _settingsService;
|
||||
|
||||
public PrivacySettingsService(ISettingsService settingsService)
|
||||
{
|
||||
_settingsService = settingsService ?? throw new ArgumentNullException(nameof(settingsService));
|
||||
}
|
||||
|
||||
public PrivacySettingsState Get()
|
||||
{
|
||||
var snapshot = _settingsService.Load();
|
||||
return new PrivacySettingsState(
|
||||
snapshot.UploadAnonymousCrashData,
|
||||
snapshot.UploadAnonymousUsageData);
|
||||
}
|
||||
|
||||
public void Save(PrivacySettingsState state)
|
||||
{
|
||||
var snapshot = _settingsService.Load();
|
||||
snapshot.UploadAnonymousCrashData = state.UploadAnonymousCrashData;
|
||||
snapshot.UploadAnonymousUsageData = state.UploadAnonymousUsageData;
|
||||
_settingsService.SaveSnapshot(
|
||||
SettingsScope.App,
|
||||
snapshot,
|
||||
changedKeys:
|
||||
[
|
||||
nameof(AppSettingsSnapshot.UploadAnonymousCrashData),
|
||||
nameof(AppSettingsSnapshot.UploadAnonymousUsageData)
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class UpdateSettingsService : IUpdateSettingsService, IDisposable
|
||||
{
|
||||
private readonly ISettingsService _settingsService;
|
||||
@@ -920,6 +1015,7 @@ internal sealed class SettingsFacadeService : ISettingsFacadeService, IDisposabl
|
||||
_weatherSettingsService = new WeatherSettingsService(Settings);
|
||||
Weather = _weatherSettingsService;
|
||||
Region = new RegionSettingsService(Settings);
|
||||
Privacy = new PrivacySettingsService(Settings);
|
||||
_updateSettingsService = new UpdateSettingsService(Settings);
|
||||
Update = _updateSettingsService;
|
||||
LauncherCatalog = new LauncherCatalogService();
|
||||
@@ -949,6 +1045,8 @@ internal sealed class SettingsFacadeService : ISettingsFacadeService, IDisposabl
|
||||
|
||||
public IRegionSettingsService Region { get; }
|
||||
|
||||
public IPrivacySettingsService Privacy { get; }
|
||||
|
||||
public IUpdateSettingsService Update { get; }
|
||||
|
||||
public ILauncherCatalogService LauncherCatalog { get; }
|
||||
|
||||
@@ -177,6 +177,7 @@ internal sealed class SettingsPageRegistry : ISettingsPageRegistry, IDisposable
|
||||
services.AddSingleton(_settingsFacade);
|
||||
services.AddSingleton(_settingsFacade.Settings);
|
||||
services.AddSingleton(_settingsFacade.Catalog);
|
||||
services.AddSingleton<IAppearanceThemeService>(_ => HostAppearanceThemeProvider.GetOrCreate());
|
||||
services.AddSingleton(_hostApplicationLifecycle);
|
||||
services.AddSingleton(_localizationService);
|
||||
services.AddSingleton<ILocationService>(_ => HostLocationServiceProvider.GetOrCreate());
|
||||
|
||||
@@ -52,10 +52,10 @@ public interface ISettingsWindowService
|
||||
|
||||
internal sealed class SettingsWindowService : ISettingsWindowService
|
||||
{
|
||||
private static readonly Color DefaultAccentColor = Color.Parse("#FF3B82F6");
|
||||
private readonly ISettingsPageRegistry _pageRegistry;
|
||||
private readonly IHostApplicationLifecycle _hostApplicationLifecycle;
|
||||
private readonly ISettingsFacadeService _settingsFacade;
|
||||
private readonly IAppearanceThemeService _appearanceThemeService;
|
||||
private readonly LocalizationService _localizationService;
|
||||
private SettingsWindowViewModel _viewModel = null!;
|
||||
private SettingsWindow? _window;
|
||||
@@ -68,8 +68,10 @@ internal sealed class SettingsWindowService : ISettingsWindowService
|
||||
_pageRegistry = pageRegistry;
|
||||
_hostApplicationLifecycle = hostApplicationLifecycle;
|
||||
_settingsFacade = settingsFacade;
|
||||
_appearanceThemeService = HostAppearanceThemeProvider.GetOrCreate();
|
||||
_localizationService = new();
|
||||
_settingsFacade.Settings.Changed += OnSettingsChanged;
|
||||
_appearanceThemeService.Changed += OnAppearanceThemeChanged;
|
||||
}
|
||||
|
||||
private string L(string key)
|
||||
@@ -86,9 +88,9 @@ internal sealed class SettingsWindowService : ISettingsWindowService
|
||||
{
|
||||
_pageRegistry.Rebuild();
|
||||
_window ??= CreateWindow();
|
||||
var themeState = _settingsFacade.Theme.Get();
|
||||
_window.ApplyChromeMode(themeState.UseSystemChrome);
|
||||
ApplyTheme(_window, themeState);
|
||||
var appearanceSnapshot = _appearanceThemeService.GetCurrent();
|
||||
_window.ApplyChromeMode(appearanceSnapshot.UseSystemChrome);
|
||||
ApplyTheme(_window);
|
||||
_window.ReloadPages(request.PageId);
|
||||
PositionWindow(_window, request);
|
||||
|
||||
@@ -135,15 +137,15 @@ internal sealed class SettingsWindowService : ISettingsWindowService
|
||||
|
||||
_viewModel = new SettingsWindowViewModel(_localizationService, languageCode).Initialize();
|
||||
|
||||
var themeState = _settingsFacade.Theme.Get();
|
||||
var useSystemChrome = themeState.UseSystemChrome;
|
||||
var appearanceSnapshot = _appearanceThemeService.GetCurrent();
|
||||
var useSystemChrome = appearanceSnapshot.UseSystemChrome;
|
||||
|
||||
var window = new SettingsWindow(
|
||||
_viewModel,
|
||||
_pageRegistry,
|
||||
_hostApplicationLifecycle,
|
||||
useSystemChrome);
|
||||
ApplyTheme(window, themeState);
|
||||
ApplyTheme(window);
|
||||
window.ShowInTaskbar = false;
|
||||
window.Closed += (_, _) =>
|
||||
{
|
||||
@@ -277,13 +279,16 @@ internal sealed class SettingsWindowService : ISettingsWindowService
|
||||
var changedKeys = e.ChangedKeys?.ToArray();
|
||||
var refreshAll = changedKeys is null || changedKeys.Length == 0;
|
||||
var languageChanged = refreshAll || changedKeys.Contains(nameof(AppSettingsSnapshot.LanguageCode), StringComparer.OrdinalIgnoreCase);
|
||||
var liveAppearance = _appearanceThemeService.GetCurrent();
|
||||
var themeChanged =
|
||||
refreshAll ||
|
||||
changedKeys.Contains(nameof(AppSettingsSnapshot.IsNightMode), StringComparer.OrdinalIgnoreCase) ||
|
||||
changedKeys.Contains(nameof(AppSettingsSnapshot.ThemeColor), StringComparer.OrdinalIgnoreCase) ||
|
||||
changedKeys.Contains(nameof(AppSettingsSnapshot.WallpaperPath), StringComparer.OrdinalIgnoreCase) ||
|
||||
changedKeys.Contains(nameof(AppSettingsSnapshot.WallpaperType), StringComparer.OrdinalIgnoreCase) ||
|
||||
changedKeys.Contains(nameof(AppSettingsSnapshot.WallpaperColor), StringComparer.OrdinalIgnoreCase) ||
|
||||
(string.Equals(liveAppearance.ThemeColorMode, ThemeAppearanceValues.ColorModeSeedMonet, StringComparison.OrdinalIgnoreCase) &&
|
||||
changedKeys.Contains(nameof(AppSettingsSnapshot.ThemeColor), StringComparer.OrdinalIgnoreCase)) ||
|
||||
(string.Equals(liveAppearance.ThemeColorMode, ThemeAppearanceValues.ColorModeWallpaperMonet, StringComparison.OrdinalIgnoreCase) &&
|
||||
(changedKeys.Contains(nameof(AppSettingsSnapshot.WallpaperPath), StringComparer.OrdinalIgnoreCase) ||
|
||||
changedKeys.Contains(nameof(AppSettingsSnapshot.WallpaperType), StringComparer.OrdinalIgnoreCase) ||
|
||||
changedKeys.Contains(nameof(AppSettingsSnapshot.WallpaperColor), StringComparer.OrdinalIgnoreCase))) ||
|
||||
changedKeys.Contains(nameof(AppSettingsSnapshot.UseSystemChrome), StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
if (languageChanged)
|
||||
@@ -297,59 +302,36 @@ internal sealed class SettingsWindowService : ISettingsWindowService
|
||||
|
||||
if (themeChanged)
|
||||
{
|
||||
var themeState = _settingsFacade.Theme.Get();
|
||||
_window.ApplyChromeMode(themeState.UseSystemChrome);
|
||||
ApplyTheme(_window, themeState);
|
||||
var appearanceSnapshot = _appearanceThemeService.GetCurrent();
|
||||
_window.ApplyChromeMode(appearanceSnapshot.UseSystemChrome);
|
||||
ApplyTheme(_window);
|
||||
}
|
||||
}, DispatcherPriority.Background);
|
||||
}
|
||||
|
||||
private static void ApplyTheme(SettingsWindow window, ThemeAppearanceSettingsState themeState)
|
||||
private void ApplyTheme(SettingsWindow window)
|
||||
{
|
||||
window.RequestedThemeVariant = themeState.IsNightMode
|
||||
var appearanceSnapshot = _appearanceThemeService.GetCurrent();
|
||||
window.RequestedThemeVariant = appearanceSnapshot.IsNightMode
|
||||
? ThemeVariant.Dark
|
||||
: ThemeVariant.Light;
|
||||
|
||||
var settingsFacade = HostSettingsFacadeProvider.GetOrCreate();
|
||||
var wallpaperState = settingsFacade.Wallpaper.Get();
|
||||
var monetPalette = settingsFacade.Theme.BuildPalette(
|
||||
themeState.IsNightMode,
|
||||
wallpaperState.WallpaperPath,
|
||||
themeState.ThemeColor);
|
||||
var accentColor = ResolveAccentColor(themeState.ThemeColor, monetPalette);
|
||||
var context = new ThemeColorContext(
|
||||
accentColor,
|
||||
IsLightBackground: !themeState.IsNightMode,
|
||||
IsLightNavBackground: !themeState.IsNightMode,
|
||||
IsNightMode: themeState.IsNightMode,
|
||||
MonetColors: monetPalette.MonetColors);
|
||||
ThemeColorSystemService.ApplyThemeResources(window.Resources, context);
|
||||
GlassEffectService.ApplyGlassResources(window.Resources, context);
|
||||
_appearanceThemeService.ApplyThemeResources(window.Resources);
|
||||
_appearanceThemeService.ApplyWindowMaterial(window, MaterialSurfaceRole.SettingsWindowBackground);
|
||||
}
|
||||
|
||||
private static Color ResolveAccentColor(string? colorText, MonetPalette monetPalette)
|
||||
private void OnAppearanceThemeChanged(object? sender, AppearanceThemeSnapshot e)
|
||||
{
|
||||
if (monetPalette.MonetColors is { Count: > 0 })
|
||||
_ = sender;
|
||||
_ = e;
|
||||
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
return monetPalette.MonetColors[0];
|
||||
}
|
||||
|
||||
return TryParseThemeColor(colorText);
|
||||
}
|
||||
|
||||
private static Color TryParseThemeColor(string? colorText)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(colorText))
|
||||
{
|
||||
try
|
||||
if (_window is null || _viewModel is null)
|
||||
{
|
||||
return Color.Parse(colorText);
|
||||
return;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
return DefaultAccentColor;
|
||||
ApplyTheme(_window);
|
||||
}, DispatcherPriority.Background);
|
||||
}
|
||||
}
|
||||
|
||||
87
LanMountainDesktop/Services/ThemeAppearanceValues.cs
Normal file
87
LanMountainDesktop/Services/ThemeAppearanceValues.cs
Normal file
@@ -0,0 +1,87 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace LanMountainDesktop.Services;
|
||||
|
||||
public static class ThemeAppearanceValues
|
||||
{
|
||||
public const string ColorModeDefaultNeutral = "default_neutral";
|
||||
public const string ColorModeSeedMonet = "seed_monet";
|
||||
public const string ColorModeWallpaperMonet = "wallpaper_monet";
|
||||
|
||||
public const string MaterialNone = "none";
|
||||
public const string MaterialMica = "mica";
|
||||
public const string MaterialAcrylic = "acrylic";
|
||||
|
||||
public static readonly IReadOnlyList<string> AllColorModes =
|
||||
[
|
||||
ColorModeDefaultNeutral,
|
||||
ColorModeSeedMonet,
|
||||
ColorModeWallpaperMonet
|
||||
];
|
||||
|
||||
public static readonly IReadOnlyList<string> AllMaterialModes =
|
||||
[
|
||||
MaterialNone,
|
||||
MaterialMica,
|
||||
MaterialAcrylic
|
||||
];
|
||||
|
||||
public static string NormalizeThemeColorMode(string? value, string? themeColor = null)
|
||||
{
|
||||
if (string.Equals(value, ColorModeDefaultNeutral, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ColorModeDefaultNeutral;
|
||||
}
|
||||
|
||||
if (string.Equals(value, ColorModeWallpaperMonet, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ColorModeWallpaperMonet;
|
||||
}
|
||||
|
||||
if (string.Equals(value, ColorModeSeedMonet, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ColorModeSeedMonet;
|
||||
}
|
||||
|
||||
return string.IsNullOrWhiteSpace(themeColor)
|
||||
? ColorModeDefaultNeutral
|
||||
: ColorModeSeedMonet;
|
||||
}
|
||||
|
||||
public static string NormalizeSystemMaterialMode(string? value)
|
||||
{
|
||||
if (string.Equals(value, MaterialMica, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return MaterialMica;
|
||||
}
|
||||
|
||||
if (string.Equals(value, MaterialAcrylic, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return MaterialAcrylic;
|
||||
}
|
||||
|
||||
return MaterialNone;
|
||||
}
|
||||
|
||||
public static IReadOnlyList<string> NormalizeAvailableMaterialModes(IEnumerable<string>? values)
|
||||
{
|
||||
if (values is null)
|
||||
{
|
||||
return [MaterialNone];
|
||||
}
|
||||
|
||||
var normalized = values
|
||||
.Select(NormalizeSystemMaterialMode)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
if (!normalized.Contains(MaterialNone, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
normalized.Insert(0, MaterialNone);
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Media;
|
||||
@@ -68,11 +69,19 @@ public static class ThemeColorSystemService
|
||||
|
||||
private static AppThemePalette BuildPalette(ThemeColorContext context)
|
||||
{
|
||||
var monetPalette = context.MonetPalette;
|
||||
var monetColors = context.MonetColors?.Where(color => color.A > 0).ToArray() ?? [];
|
||||
var accent = monetColors.Length > 0 ? monetColors[0] : context.AccentColor;
|
||||
var secondarySeed = monetColors.Length > 1
|
||||
? monetColors[1]
|
||||
: ColorMath.Blend(accent, Color.Parse("#FFFFFFFF"), 0.14);
|
||||
var accent = context.UseNeutralSurfaces
|
||||
? context.AccentColor
|
||||
: monetPalette?.Primary ?? GetColorOrDefault(monetColors, 0, context.AccentColor);
|
||||
var secondarySeed = monetPalette?.Secondary
|
||||
?? GetColorOrDefault(monetColors, 1, ColorMath.Blend(accent, Color.Parse("#FFFFFFFF"), 0.14));
|
||||
var tertiarySeed = monetPalette?.Tertiary
|
||||
?? GetColorOrDefault(monetColors, 2, ColorMath.Blend(accent, Color.Parse("#FFFFFFFF"), 0.22));
|
||||
var neutralSeed = monetPalette?.Neutral
|
||||
?? GetColorOrDefault(monetColors, 3, context.IsNightMode ? Color.Parse("#FF171C23") : Color.Parse("#FFF2F4F7"));
|
||||
var neutralVariantSeed = monetPalette?.NeutralVariant
|
||||
?? GetColorOrDefault(monetColors, 4, context.IsNightMode ? Color.Parse("#FF20262E") : Color.Parse("#FFE8EDF2"));
|
||||
|
||||
var accentLight1 = ColorMath.Blend(accent, Color.Parse("#FFFFFFFF"), 0.22);
|
||||
var accentLight2 = ColorMath.Blend(accent, Color.Parse("#FFFFFFFF"), 0.38);
|
||||
@@ -83,18 +92,23 @@ public static class ThemeColorSystemService
|
||||
|
||||
var primary = context.IsNightMode ? accentLight1 : accentDark1;
|
||||
var secondary = context.IsNightMode
|
||||
? ColorMath.Blend(secondarySeed, Color.Parse("#FFFFFFFF"), 0.16)
|
||||
: ColorMath.Blend(secondarySeed, Color.Parse("#FF111827"), 0.14);
|
||||
? ColorMath.Blend(secondarySeed, Color.Parse("#FFFFFFFF"), 0.12)
|
||||
: ColorMath.Blend(secondarySeed, Color.Parse("#FF111827"), 0.10);
|
||||
|
||||
var surfaceBase = context.IsNightMode
|
||||
? ColorMath.Blend(Color.Parse("#FF0A1018"), accent, 0.18)
|
||||
: ColorMath.Blend(Color.Parse("#FFF7F9FD"), accent, 0.09);
|
||||
var surfaceRaised = context.IsNightMode
|
||||
? ColorMath.Blend(Color.Parse("#FF121A24"), secondarySeed, 0.24)
|
||||
: ColorMath.Blend(Color.Parse("#FFFCFEFF"), secondarySeed, 0.12);
|
||||
var surfaceOverlayBase = context.IsNightMode
|
||||
? ColorMath.Blend(Color.Parse("#FF18212D"), accent, 0.28)
|
||||
: ColorMath.Blend(Color.Parse("#FFF1F5FB"), accent, 0.16);
|
||||
var baseSurface = context.IsNightMode ? Color.Parse("#FF0B0F14") : Color.Parse("#FFF7F8FA");
|
||||
var raisedSurface = context.IsNightMode ? Color.Parse("#FF131922") : Color.Parse("#FFFFFFFF");
|
||||
var overlaySurface = context.IsNightMode ? Color.Parse("#FF171E28") : Color.Parse("#FFF1F4F8");
|
||||
var navSurfaceBase = context.IsLightNavBackground ? Color.Parse("#FFF8FAFC") : Color.Parse("#FF111827");
|
||||
|
||||
var surfaceBase = context.UseNeutralSurfaces
|
||||
? baseSurface
|
||||
: ColorMath.Blend(baseSurface, neutralSeed, context.IsNightMode ? 0.84 : 0.78);
|
||||
var surfaceRaised = context.UseNeutralSurfaces
|
||||
? raisedSurface
|
||||
: ColorMath.Blend(raisedSurface, neutralVariantSeed, context.IsNightMode ? 0.72 : 0.60);
|
||||
var surfaceOverlayBase = context.UseNeutralSurfaces
|
||||
? overlaySurface
|
||||
: ColorMath.Blend(overlaySurface, neutralVariantSeed, context.IsNightMode ? 0.76 : 0.64);
|
||||
var surfaceOverlay = Color.FromArgb(
|
||||
context.IsNightMode ? (byte)0xE8 : (byte)0xF2,
|
||||
surfaceOverlayBase.R,
|
||||
@@ -115,9 +129,9 @@ public static class ThemeColorSystemService
|
||||
? ColorMath.EnsureContrast(ColorMath.Blend(accent, Color.Parse("#FF0B1220"), 0.20), surfaceRaised, WcagNormalTextContrast)
|
||||
: ColorMath.EnsureContrast(ColorMath.Blend(accent, Color.Parse("#FFFFFFFF"), 0.16), surfaceRaised, WcagNormalTextContrast);
|
||||
|
||||
var navSurface = context.IsLightNavBackground
|
||||
? ColorMath.Blend(surfaceRaised, accentLight2, 0.08)
|
||||
: ColorMath.Blend(Color.Parse("#FF111827"), accentDark2, 0.24);
|
||||
var navSurface = context.UseNeutralSurfaces
|
||||
? navSurfaceBase
|
||||
: ColorMath.Blend(navSurfaceBase, neutralSeed, context.IsNightMode ? 0.66 : 0.70);
|
||||
var navText = ColorMath.EnsureContrast(
|
||||
context.IsLightNavBackground ? Color.Parse("#FF0B1220") : Color.Parse("#FFF8FAFC"),
|
||||
navSurface,
|
||||
@@ -129,18 +143,23 @@ public static class ThemeColorSystemService
|
||||
? Color.FromArgb(0x33, surfaceRaised.R, surfaceRaised.G, surfaceRaised.B)
|
||||
: Color.FromArgb(0x38, navSurface.R, navSurface.G, navSurface.B);
|
||||
var navItemHoverBackground = context.IsLightNavBackground
|
||||
? ColorMath.WithAlpha(ColorMath.Blend(accentLight2, surfaceRaised, 0.30), 0x7A)
|
||||
: ColorMath.WithAlpha(ColorMath.Blend(accentDark1, navSurface, 0.26), 0x88);
|
||||
var navItemSelectedBackground = ColorMath.WithAlpha(accent, context.IsNightMode ? (byte)0xCE : (byte)0xD9);
|
||||
var navSelectionIndicator = ColorMath.EnsureContrast(accentLight1, navSurface, WcagLargeTextContrast);
|
||||
? ColorMath.WithAlpha(ColorMath.Blend(accentLight2, navSurface, 0.12), 0x64)
|
||||
: ColorMath.WithAlpha(ColorMath.Blend(accentDark1, navSurface, 0.18), 0x74);
|
||||
var navItemSelectedBackground = context.UseNeutralSurfaces
|
||||
? ColorMath.WithAlpha(ColorMath.Blend(accent, navSurface, 0.24), context.IsNightMode ? (byte)0x8F : (byte)0x6A)
|
||||
: ColorMath.WithAlpha(ColorMath.Blend(accent, navSurface, context.IsNightMode ? 0.28 : 0.22), context.IsNightMode ? (byte)0x88 : (byte)0x72);
|
||||
var navSelectionIndicator = ColorMath.EnsureContrast(
|
||||
context.UseNeutralSurfaces ? accent : accentLight1,
|
||||
navSurface,
|
||||
WcagLargeTextContrast);
|
||||
|
||||
var toggleOn = context.IsNightMode ? accent : accentDark1;
|
||||
var toggleOff = context.IsNightMode
|
||||
? Color.FromArgb(0x88, accentDark2.R, accentDark2.G, accentDark2.B)
|
||||
: Color.FromArgb(0x88, accentLight2.R, accentLight2.G, accentLight2.B);
|
||||
? Color.FromArgb(0x88, neutralVariantSeed.R, neutralVariantSeed.G, neutralVariantSeed.B)
|
||||
: Color.FromArgb(0x88, neutralVariantSeed.R, neutralVariantSeed.G, neutralVariantSeed.B);
|
||||
var toggleBorder = context.IsNightMode
|
||||
? ColorMath.WithAlpha(ColorMath.Blend(accentLight2, Color.Parse("#FFF8FAFC"), 0.28), 0x8C)
|
||||
: ColorMath.WithAlpha(ColorMath.Blend(accentDark2, Color.Parse("#FF334155"), 0.26), 0x78);
|
||||
? ColorMath.WithAlpha(ColorMath.Blend(neutralVariantSeed, Color.Parse("#FFF8FAFC"), 0.20), 0x8C)
|
||||
: ColorMath.WithAlpha(ColorMath.Blend(tertiarySeed, Color.Parse("#FF334155"), 0.18), 0x78);
|
||||
var onAccent = ColorMath.EnsureContrast(Color.Parse("#FFFFFFFF"), accent, WcagNormalTextContrast);
|
||||
|
||||
return new AppThemePalette(
|
||||
@@ -171,4 +190,11 @@ public static class ThemeColorSystemService
|
||||
toggleOff,
|
||||
toggleBorder);
|
||||
}
|
||||
|
||||
private static Color GetColorOrDefault(IReadOnlyList<Color> colors, int index, Color fallback)
|
||||
{
|
||||
return index >= 0 && index < colors.Count
|
||||
? colors[index]
|
||||
: fallback;
|
||||
}
|
||||
}
|
||||
|
||||
101
LanMountainDesktop/Services/XiaomiWeatherCodeMapper.cs
Normal file
101
LanMountainDesktop/Services/XiaomiWeatherCodeMapper.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace LanMountainDesktop.Services;
|
||||
|
||||
internal enum WeatherConditionBucket
|
||||
{
|
||||
Unknown,
|
||||
Clear,
|
||||
PartlyCloudy,
|
||||
Cloudy,
|
||||
Haze,
|
||||
Fog,
|
||||
RainLight,
|
||||
RainHeavy,
|
||||
Storm,
|
||||
Sleet,
|
||||
Snow
|
||||
}
|
||||
|
||||
internal static class XiaomiWeatherCodeMapper
|
||||
{
|
||||
private readonly record struct WeatherCodeEntry(string Zh, string En, WeatherConditionBucket Bucket);
|
||||
|
||||
private static readonly IReadOnlyDictionary<int, WeatherCodeEntry> Entries = new Dictionary<int, WeatherCodeEntry>
|
||||
{
|
||||
[0] = new("\u6674", "Clear", WeatherConditionBucket.Clear),
|
||||
[1] = new("\u591a\u4e91", "Partly Cloudy", WeatherConditionBucket.PartlyCloudy),
|
||||
[2] = new("\u9634", "Cloudy", WeatherConditionBucket.Cloudy),
|
||||
[3] = new("\u9635\u96e8", "Shower", WeatherConditionBucket.RainLight),
|
||||
[4] = new("\u96f7\u9635\u96e8", "Thunder Shower", WeatherConditionBucket.Storm),
|
||||
[5] = new("\u96f7\u9635\u96e8\u4f34\u6709\u51b0\u96f9", "Thunder Shower with Hail", WeatherConditionBucket.Storm),
|
||||
[6] = new("\u96e8\u5939\u96ea", "Sleet", WeatherConditionBucket.Sleet),
|
||||
[7] = new("\u5c0f\u96e8", "Light Rain", WeatherConditionBucket.RainLight),
|
||||
[8] = new("\u4e2d\u96e8", "Moderate Rain", WeatherConditionBucket.RainHeavy),
|
||||
[9] = new("\u5927\u96e8", "Heavy Rain", WeatherConditionBucket.RainHeavy),
|
||||
[10] = new("\u66b4\u96e8", "Storm", WeatherConditionBucket.RainHeavy),
|
||||
[11] = new("\u5927\u66b4\u96e8", "Heavy Storm", WeatherConditionBucket.RainHeavy),
|
||||
[12] = new("\u7279\u5927\u66b4\u96e8", "Severe Storm", WeatherConditionBucket.RainHeavy),
|
||||
[13] = new("\u9635\u96ea", "Snow Flurry", WeatherConditionBucket.Snow),
|
||||
[14] = new("\u5c0f\u96ea", "Light Snow", WeatherConditionBucket.Snow),
|
||||
[15] = new("\u4e2d\u96ea", "Moderate Snow", WeatherConditionBucket.Snow),
|
||||
[16] = new("\u5927\u96ea", "Heavy Snow", WeatherConditionBucket.Snow),
|
||||
[17] = new("\u66b4\u96ea", "Snowstorm", WeatherConditionBucket.Snow),
|
||||
[18] = new("\u96fe", "Fog", WeatherConditionBucket.Fog),
|
||||
[19] = new("\u51bb\u96e8", "Freezing Rain", WeatherConditionBucket.RainLight),
|
||||
[20] = new("\u6c99\u5c18\u66b4", "Duststorm", WeatherConditionBucket.Haze),
|
||||
[21] = new("\u5c0f\u5230\u4e2d\u96e8", "Light to Moderate Rain", WeatherConditionBucket.RainLight),
|
||||
[22] = new("\u4e2d\u5230\u5927\u96e8", "Moderate to Heavy Rain", WeatherConditionBucket.RainHeavy),
|
||||
[23] = new("\u5927\u5230\u66b4\u96e8", "Heavy Rain to Storm", WeatherConditionBucket.RainHeavy),
|
||||
[24] = new("\u66b4\u96e8\u5230\u5927\u66b4\u96e8", "Storm to Heavy Storm", WeatherConditionBucket.RainHeavy),
|
||||
[25] = new("\u5927\u66b4\u96e8\u5230\u7279\u5927\u66b4\u96e8", "Heavy to Severe Storm", WeatherConditionBucket.RainHeavy),
|
||||
[26] = new("\u5c0f\u5230\u4e2d\u96ea", "Light to Moderate Snow", WeatherConditionBucket.Snow),
|
||||
[27] = new("\u4e2d\u5230\u5927\u96ea", "Moderate to Heavy Snow", WeatherConditionBucket.Snow),
|
||||
[28] = new("\u5927\u5230\u66b4\u96ea", "Heavy Snow to Snowstorm", WeatherConditionBucket.Snow),
|
||||
[29] = new("\u6d6e\u5c18", "Dust", WeatherConditionBucket.Haze),
|
||||
[30] = new("\u626c\u6c99", "Sand", WeatherConditionBucket.Haze),
|
||||
[31] = new("\u5f3a\u6c99\u5c18\u66b4", "Sandstorm", WeatherConditionBucket.Haze),
|
||||
[32] = new("\u6d53\u96fe", "Dense Fog", WeatherConditionBucket.Fog),
|
||||
[49] = new("\u5f3a\u6d53\u96fe", "Strong Fog", WeatherConditionBucket.Fog),
|
||||
[53] = new("\u973e", "Haze", WeatherConditionBucket.Haze),
|
||||
[54] = new("\u4e2d\u5ea6\u973e", "Moderate Haze", WeatherConditionBucket.Haze),
|
||||
[55] = new("\u91cd\u5ea6\u973e", "Heavy Haze", WeatherConditionBucket.Haze),
|
||||
[56] = new("\u4e25\u91cd\u973e", "Severe Haze", WeatherConditionBucket.Haze),
|
||||
[57] = new("\u5927\u96fe", "Heavy Fog", WeatherConditionBucket.Fog),
|
||||
[58] = new("\u7279\u5f3a\u6d53\u96fe", "Extra Heavy Fog", WeatherConditionBucket.Fog),
|
||||
[301] = new("\u96e8", "Rain", WeatherConditionBucket.RainLight),
|
||||
[302] = new("\u96ea", "Snow", WeatherConditionBucket.Snow)
|
||||
};
|
||||
|
||||
public static WeatherConditionBucket ResolveBucket(int? code)
|
||||
{
|
||||
if (!code.HasValue)
|
||||
{
|
||||
return WeatherConditionBucket.Unknown;
|
||||
}
|
||||
|
||||
return Entries.TryGetValue(code.Value, out var entry)
|
||||
? entry.Bucket
|
||||
: WeatherConditionBucket.Unknown;
|
||||
}
|
||||
|
||||
public static string? ResolveDisplayText(int? code, string locale)
|
||||
{
|
||||
if (!code.HasValue)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Entries.TryGetValue(code.Value, out var entry))
|
||||
{
|
||||
return locale.StartsWith("zh", StringComparison.OrdinalIgnoreCase)
|
||||
? entry.Zh
|
||||
: entry.En;
|
||||
}
|
||||
|
||||
return locale.StartsWith("zh", StringComparison.OrdinalIgnoreCase)
|
||||
? $"\u5929\u6c14\u7801 {code.Value}"
|
||||
: $"Weather {code.Value}";
|
||||
}
|
||||
}
|
||||
@@ -39,42 +39,6 @@ public sealed class XiaomiWeatherService : IWeatherDataService, IDisposable
|
||||
{
|
||||
private sealed record CacheEntry(WeatherSnapshot Snapshot, DateTimeOffset ExpireAt);
|
||||
|
||||
private static readonly IReadOnlyDictionary<int, string> ZhWeatherDescriptions = new Dictionary<int, string>
|
||||
{
|
||||
[0] = "\u6674",
|
||||
[1] = "\u591a\u4e91",
|
||||
[2] = "\u9634",
|
||||
[3] = "\u9635\u96e8",
|
||||
[4] = "\u96f7\u9635\u96e8",
|
||||
[7] = "\u5c0f\u96e8",
|
||||
[8] = "\u4e2d\u96e8",
|
||||
[9] = "\u5927\u96e8",
|
||||
[13] = "\u9635\u96ea",
|
||||
[14] = "\u5c0f\u96ea",
|
||||
[15] = "\u4e2d\u96ea",
|
||||
[16] = "\u5927\u96ea",
|
||||
[18] = "\u96fe",
|
||||
[32] = "\u973e"
|
||||
};
|
||||
|
||||
private static readonly IReadOnlyDictionary<int, string> EnWeatherDescriptions = new Dictionary<int, string>
|
||||
{
|
||||
[0] = "Clear",
|
||||
[1] = "Partly Cloudy",
|
||||
[2] = "Cloudy",
|
||||
[3] = "Shower",
|
||||
[4] = "Thunder Shower",
|
||||
[7] = "Light Rain",
|
||||
[8] = "Moderate Rain",
|
||||
[9] = "Heavy Rain",
|
||||
[13] = "Snow Flurry",
|
||||
[14] = "Light Snow",
|
||||
[15] = "Moderate Snow",
|
||||
[16] = "Heavy Snow",
|
||||
[18] = "Fog",
|
||||
[32] = "Haze"
|
||||
};
|
||||
|
||||
private readonly XiaomiWeatherApiOptions _options;
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly bool _ownsHttpClient;
|
||||
@@ -412,9 +376,7 @@ public sealed class XiaomiWeatherService : IWeatherDataService, IDisposable
|
||||
TryGetNode(payload, "hourly") ??
|
||||
TryGetNode(payload, "hourlyForecast");
|
||||
|
||||
var weatherCode = ReadInt(currentNode, "weather", "value") ??
|
||||
ReadInt(currentNode, "weatherCode") ??
|
||||
ReadInt(currentNode, "code");
|
||||
var weatherCode = ReadWeatherCode(currentNode);
|
||||
|
||||
var weatherText = ReadString(currentNode, "weather", "desc") ??
|
||||
ReadString(currentNode, "weather", "text") ??
|
||||
@@ -497,8 +459,12 @@ public sealed class XiaomiWeatherService : IWeatherDataService, IDisposable
|
||||
var sunItem = GetArrayItem(sunArray, i);
|
||||
var precipitationItem = GetArrayItem(precipitationArray, i);
|
||||
|
||||
var dayCode = ReadInt(weatherItem, "from") ?? ReadInt(weatherItem, "day");
|
||||
var nightCode = ReadInt(weatherItem, "to") ?? ReadInt(weatherItem, "night");
|
||||
var dayCode = ReadInt(weatherItem, "from") ??
|
||||
ReadInt(weatherItem, "day") ??
|
||||
ReadWeatherCode(weatherItem);
|
||||
var nightCode = ReadInt(weatherItem, "to") ??
|
||||
ReadInt(weatherItem, "night") ??
|
||||
ReadWeatherCode(weatherItem);
|
||||
var dayText = ResolveWeatherDescription(dayCode, locale);
|
||||
var nightText = ResolveWeatherDescription(nightCode, locale);
|
||||
|
||||
@@ -591,11 +557,7 @@ public sealed class XiaomiWeatherService : IWeatherDataService, IDisposable
|
||||
continue;
|
||||
}
|
||||
|
||||
var code = ReadInt(weatherItem, "value") ??
|
||||
ReadInt(weatherItem, "code") ??
|
||||
ReadInt(weatherItem, "weatherCode") ??
|
||||
ReadInt(weatherItem, "from") ??
|
||||
ReadInt(weatherItem);
|
||||
var code = ReadInt(weatherItem, "from") ?? ReadWeatherCode(weatherItem);
|
||||
var weatherText = ReadString(weatherItem, "text") ??
|
||||
ReadString(weatherItem, "desc") ??
|
||||
ResolveWeatherDescription(code, locale);
|
||||
@@ -640,11 +602,7 @@ public sealed class XiaomiWeatherService : IWeatherDataService, IDisposable
|
||||
continue;
|
||||
}
|
||||
|
||||
var code = ReadInt(item, "weatherCode") ??
|
||||
ReadInt(item, "code") ??
|
||||
ReadInt(item, "weather", "value") ??
|
||||
ReadInt(item, "weather") ??
|
||||
ReadInt(item, "from");
|
||||
var code = ReadInt(item, "from") ?? ReadWeatherCode(item);
|
||||
var weatherText = ReadString(item, "weatherText") ??
|
||||
ReadString(item, "weather", "desc") ??
|
||||
ReadString(item, "weather", "text") ??
|
||||
@@ -903,6 +861,16 @@ public sealed class XiaomiWeatherService : IWeatherDataService, IDisposable
|
||||
return null;
|
||||
}
|
||||
|
||||
private static int? ReadWeatherCode(JsonElement? node)
|
||||
{
|
||||
return ReadInt(node, "weather", "value") ??
|
||||
ReadInt(node, "weatherCode") ??
|
||||
ReadInt(node, "code") ??
|
||||
ReadInt(node, "weather") ??
|
||||
ReadInt(node, "value") ??
|
||||
ReadInt(node);
|
||||
}
|
||||
|
||||
private static double? ReadDouble(JsonElement? node, params string[] path)
|
||||
{
|
||||
if (!node.HasValue)
|
||||
@@ -995,19 +963,7 @@ public sealed class XiaomiWeatherService : IWeatherDataService, IDisposable
|
||||
|
||||
private static string? ResolveWeatherDescription(int? code, string locale)
|
||||
{
|
||||
if (!code.HasValue)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var isZh = locale.StartsWith("zh", StringComparison.OrdinalIgnoreCase);
|
||||
var source = isZh ? ZhWeatherDescriptions : EnWeatherDescriptions;
|
||||
if (source.TryGetValue(code.Value, out var text))
|
||||
{
|
||||
return text;
|
||||
}
|
||||
|
||||
return isZh ? $"\u5929\u6c14\u7801 {code.Value}" : $"Weather {code.Value}";
|
||||
return XiaomiWeatherCodeMapper.ResolveDisplayText(code, locale);
|
||||
}
|
||||
|
||||
private static string Truncate(string? text, int maxLength)
|
||||
|
||||
@@ -184,8 +184,8 @@
|
||||
</Style>
|
||||
|
||||
<Style Selector="Border.glass-island">
|
||||
<Setter Property="Background" Value="{DynamicResource AdaptiveGlassStrongBackgroundBrush}" />
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource AdaptiveGlassStrongBorderBrush}" />
|
||||
<Setter Property="Background" Value="{DynamicResource AdaptiveDockGlassBackgroundBrush}" />
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource AdaptiveDockGlassBorderBrush}" />
|
||||
<Setter Property="BorderThickness" Value="1.5" />
|
||||
<Setter Property="CornerRadius" Value="36" />
|
||||
<Setter Property="Opacity" Value="{DynamicResource AdaptiveGlassStrongOpacity}" />
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using Avalonia.Media;
|
||||
using LanMountainDesktop.Models;
|
||||
using LanMountainDesktop.Services;
|
||||
|
||||
namespace LanMountainDesktop.Theme;
|
||||
|
||||
@@ -8,4 +10,7 @@ public sealed record ThemeColorContext(
|
||||
bool IsLightBackground,
|
||||
bool IsLightNavBackground,
|
||||
bool IsNightMode,
|
||||
IReadOnlyList<Color>? MonetColors = null);
|
||||
MonetPalette? MonetPalette = null,
|
||||
IReadOnlyList<Color>? MonetColors = null,
|
||||
bool UseNeutralSurfaces = false,
|
||||
string SystemMaterialMode = ThemeAppearanceValues.MaterialNone);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -62,6 +62,15 @@ public sealed partial class SettingsWindowViewModel : ViewModelBase
|
||||
[ObservableProperty]
|
||||
private string _restartButtonText = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _restartDialogTitle = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _restartDialogPrimaryText = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _restartDialogCloseText = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string? _drawerTitle;
|
||||
|
||||
@@ -84,6 +93,12 @@ public sealed partial class SettingsWindowViewModel : ViewModelBase
|
||||
Title = L("settings.title");
|
||||
RestartTitle = L("settings.restart_dock.title");
|
||||
RestartButtonText = L("settings.restart_dock.button");
|
||||
RestartDialogTitle = L("settings.restart_dialog.title");
|
||||
RestartDialogPrimaryText = L("settings.restart_dialog.restart");
|
||||
RestartDialogCloseText = _localizationService.GetString(
|
||||
_languageCode,
|
||||
"settings.restart_dialog.later",
|
||||
L("settings.restart_dialog.cancel"));
|
||||
DrawerFallbackTitle = L("settings.window.drawer_default");
|
||||
|
||||
var nextDefaultRestartMessage = L("settings.restart_dock.description");
|
||||
@@ -113,6 +128,28 @@ public sealed class SelectionOption
|
||||
public string Label { get; }
|
||||
}
|
||||
|
||||
public sealed class ThemeSeedCandidateOption
|
||||
{
|
||||
public ThemeSeedCandidateOption(string value, string label, Color color, bool isSelected)
|
||||
{
|
||||
Value = value;
|
||||
Label = label;
|
||||
Color = color;
|
||||
IsSelected = isSelected;
|
||||
Brush = new SolidColorBrush(color);
|
||||
}
|
||||
|
||||
public string Value { get; }
|
||||
|
||||
public string Label { get; }
|
||||
|
||||
public Color Color { get; }
|
||||
|
||||
public bool IsSelected { get; }
|
||||
|
||||
public IBrush Brush { get; }
|
||||
}
|
||||
|
||||
public sealed class TimeZoneOption
|
||||
{
|
||||
public TimeZoneOption(string? id, string label)
|
||||
@@ -384,27 +421,37 @@ public sealed partial class GeneralSettingsPageViewModel : ViewModelBase
|
||||
|
||||
public sealed partial class AppearanceSettingsPageViewModel : ViewModelBase
|
||||
{
|
||||
private static readonly Color DefaultSeedColor = Color.Parse("#FF3B82F6");
|
||||
private static readonly SolidColorBrush NeutralLightBrushValue = new(Color.Parse("#FFFFFFFF"));
|
||||
private static readonly SolidColorBrush NeutralDarkBrushValue = new(Color.Parse("#FF000000"));
|
||||
private readonly ISettingsFacadeService _settingsFacade;
|
||||
private readonly IAppearanceThemeService _appearanceThemeService;
|
||||
private readonly LocalizationService _localizationService = new();
|
||||
private readonly string _languageCode;
|
||||
private bool _isInitializing;
|
||||
private string? _selectedWallpaperSeed;
|
||||
|
||||
public AppearanceSettingsPageViewModel(ISettingsFacadeService settingsFacade)
|
||||
public AppearanceSettingsPageViewModel(
|
||||
ISettingsFacadeService settingsFacade,
|
||||
IAppearanceThemeService appearanceThemeService)
|
||||
{
|
||||
_settingsFacade = settingsFacade;
|
||||
_appearanceThemeService = appearanceThemeService;
|
||||
_languageCode = _localizationService.NormalizeLanguageCode(_settingsFacade.Region.Get().LanguageCode);
|
||||
WallpaperPlacements = CreateWallpaperPlacements();
|
||||
ClockFormats = CreateClockFormats();
|
||||
RefreshLocalizedText();
|
||||
ThemeColorModes = CreateThemeColorModes();
|
||||
|
||||
_isInitializing = true;
|
||||
Load();
|
||||
_isInitializing = false;
|
||||
}
|
||||
|
||||
public IReadOnlyList<SelectionOption> WallpaperPlacements { get; }
|
||||
public event Action<string>? RestartRequested;
|
||||
|
||||
public IReadOnlyList<SelectionOption> ClockFormats { get; }
|
||||
public IReadOnlyList<SelectionOption> ThemeColorModes { get; }
|
||||
|
||||
[ObservableProperty]
|
||||
private IReadOnlyList<SelectionOption> _systemMaterialModes = [];
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isNightMode;
|
||||
@@ -413,32 +460,66 @@ public sealed partial class AppearanceSettingsPageViewModel : ViewModelBase
|
||||
private string _themeColor = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private Color _themeColorPickerValue;
|
||||
private Color _customSeedPickerValue = DefaultSeedColor;
|
||||
|
||||
partial void OnThemeColorPickerValueChanged(Color value)
|
||||
partial void OnCustomSeedPickerValueChanged(Color value)
|
||||
{
|
||||
if (_isInitializing)
|
||||
if (_isInitializing ||
|
||||
!string.Equals(SelectedThemeColorMode?.Value, ThemeAppearanceValues.ColorModeSeedMonet, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ThemeColor = value.ToString();
|
||||
UpdatePreview(BuildPendingState(usePickerSeed: true));
|
||||
}
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _useSystemChrome;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _wallpaperPath = string.Empty;
|
||||
private SelectionOption _selectedThemeColorMode = new(ThemeAppearanceValues.ColorModeSeedMonet, "User theme color Monet");
|
||||
|
||||
[ObservableProperty]
|
||||
private SelectionOption _selectedWallpaperPlacement = new("Fill", "Fill");
|
||||
private SelectionOption _selectedSystemMaterialMode = new(ThemeAppearanceValues.MaterialNone, "None");
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _showClock = true;
|
||||
private bool _isThemeColorEditable;
|
||||
|
||||
[ObservableProperty]
|
||||
private SelectionOption _selectedClockFormat = new("HourMinuteSecond", "Hour:Minute:Second");
|
||||
private bool _isWallpaperMode;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _showNeutralPreview;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _showMonetPreview;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isWallpaperSeedSelectable;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _themeColorSourceDescription = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _systemMaterialDescription = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private IBrush _primarySwatchBrush = new SolidColorBrush(DefaultSeedColor);
|
||||
|
||||
[ObservableProperty]
|
||||
private IBrush _secondarySwatchBrush = new SolidColorBrush(DefaultSeedColor);
|
||||
|
||||
[ObservableProperty]
|
||||
private IBrush _tertiarySwatchBrush = new SolidColorBrush(DefaultSeedColor);
|
||||
|
||||
[ObservableProperty]
|
||||
private IBrush _neutralSwatchBrush = new SolidColorBrush(Color.Parse("#FFF2F4F7"));
|
||||
|
||||
[ObservableProperty]
|
||||
private IBrush _seedSwatchBrush = new SolidColorBrush(DefaultSeedColor);
|
||||
|
||||
[ObservableProperty]
|
||||
private IReadOnlyList<ThemeSeedCandidateOption> _wallpaperSeedCandidates = [];
|
||||
|
||||
[ObservableProperty]
|
||||
private string _pageTitle = string.Empty;
|
||||
@@ -455,75 +536,111 @@ public sealed partial class AppearanceSettingsPageViewModel : ViewModelBase
|
||||
[ObservableProperty]
|
||||
private string _themeColorLabel = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _themeColorModeLabel = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _systemMaterialLabel = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _themeHeader = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _wallpaperHeader = string.Empty;
|
||||
private string _themeSourceNeutralText = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _wallpaperPathLabel = string.Empty;
|
||||
private string _themeSourceUserColorText = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _wallpaperPlacementLabel = string.Empty;
|
||||
private string _themeSourceWallpaperText = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _importWallpaperButtonText = string.Empty;
|
||||
private string _themeSourceDefaultDescription = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _clockHeader = string.Empty;
|
||||
private string _themeSourceUserColorDescription = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _clockDescription = string.Empty;
|
||||
private string _themeSourceWallpaperDescription = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _clockFormatLabel = string.Empty;
|
||||
private string _themeSourceWallpaperAppDescription = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _filePickerTitle = string.Empty;
|
||||
private string _themeSourceWallpaperSystemDescription = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _themeSourceWallpaperFallbackDescription = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _systemMaterialNoneText = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _systemMaterialMicaText = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _systemMaterialAcrylicText = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _systemMaterialSwitchableDescription = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _systemMaterialFixedDescription = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _appearanceRestartMessage = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _previewPrimaryLabel = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _previewSecondaryLabel = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _previewTertiaryLabel = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _previewNeutralLabel = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _previewSeedLabel = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _previewNeutralLightLabel = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _previewNeutralDarkLabel = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _seedApplyButtonText = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _wallpaperSeedFlyoutTitle = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _wallpaperSeedCurrentText = string.Empty;
|
||||
|
||||
public IBrush NeutralLightPreviewBrush => NeutralLightBrushValue;
|
||||
|
||||
public IBrush NeutralDarkPreviewBrush => NeutralDarkBrushValue;
|
||||
|
||||
public void Load()
|
||||
{
|
||||
var theme = _settingsFacade.Theme.Get();
|
||||
IsNightMode = theme.IsNightMode;
|
||||
ThemeColor = theme.ThemeColor ?? string.Empty;
|
||||
if (Color.TryParse(ThemeColor, out var color))
|
||||
{
|
||||
ThemeColorPickerValue = color;
|
||||
}
|
||||
else
|
||||
{
|
||||
ThemeColorPickerValue = Color.Parse("#FF3B82F6");
|
||||
}
|
||||
UseSystemChrome = theme.UseSystemChrome;
|
||||
var liveSnapshot = _appearanceThemeService.GetCurrent();
|
||||
RefreshMaterialModeOptions(liveSnapshot);
|
||||
|
||||
var wallpaper = _settingsFacade.Wallpaper.Get();
|
||||
WallpaperPath = wallpaper.WallpaperPath ?? string.Empty;
|
||||
var wallpaperPlacement = string.IsNullOrWhiteSpace(wallpaper.Placement)
|
||||
? "Fill"
|
||||
: wallpaper.Placement;
|
||||
SelectedWallpaperPlacement = WallpaperPlacements.FirstOrDefault(option =>
|
||||
string.Equals(option.Value, wallpaperPlacement, StringComparison.OrdinalIgnoreCase))
|
||||
?? WallpaperPlacements[0];
|
||||
|
||||
var statusBar = _settingsFacade.StatusBar.Get();
|
||||
ShowClock = statusBar.TopStatusComponentIds.Any(id =>
|
||||
string.Equals(id, BuiltInComponentIds.Clock, StringComparison.OrdinalIgnoreCase));
|
||||
var clockFormat = string.IsNullOrWhiteSpace(statusBar.ClockDisplayFormat)
|
||||
? "HourMinuteSecond"
|
||||
: statusBar.ClockDisplayFormat;
|
||||
SelectedClockFormat = ClockFormats.FirstOrDefault(option =>
|
||||
string.Equals(option.Value, clockFormat, StringComparison.OrdinalIgnoreCase))
|
||||
?? ClockFormats[1];
|
||||
}
|
||||
|
||||
public async Task ImportWallpaperAsync(string sourcePath)
|
||||
{
|
||||
var importedPath = await _settingsFacade.WallpaperMedia.ImportAssetAsync(sourcePath);
|
||||
if (!string.IsNullOrWhiteSpace(importedPath))
|
||||
_isInitializing = true;
|
||||
try
|
||||
{
|
||||
WallpaperPath = importedPath;
|
||||
ApplySavedState(theme);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isInitializing = false;
|
||||
}
|
||||
|
||||
UpdatePreview(theme);
|
||||
}
|
||||
|
||||
partial void OnIsNightModeChanged(bool value)
|
||||
@@ -533,20 +650,7 @@ public sealed partial class AppearanceSettingsPageViewModel : ViewModelBase
|
||||
return;
|
||||
}
|
||||
|
||||
SaveTheme();
|
||||
}
|
||||
|
||||
partial void OnThemeColorChanged(string value)
|
||||
{
|
||||
if (_isInitializing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(value) || Color.TryParse(value, out _))
|
||||
{
|
||||
SaveTheme();
|
||||
}
|
||||
PersistCurrentState(restartRequired: false);
|
||||
}
|
||||
|
||||
partial void OnUseSystemChromeChanged(bool value)
|
||||
@@ -556,126 +660,254 @@ public sealed partial class AppearanceSettingsPageViewModel : ViewModelBase
|
||||
return;
|
||||
}
|
||||
|
||||
SaveTheme();
|
||||
PersistCurrentState(restartRequired: false);
|
||||
}
|
||||
|
||||
partial void OnWallpaperPathChanged(string value)
|
||||
{
|
||||
if (_isInitializing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SaveWallpaper();
|
||||
}
|
||||
|
||||
partial void OnSelectedWallpaperPlacementChanged(SelectionOption value)
|
||||
partial void OnSelectedThemeColorModeChanged(SelectionOption value)
|
||||
{
|
||||
if (_isInitializing || value is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SaveWallpaper();
|
||||
PersistCurrentState(restartRequired: true);
|
||||
}
|
||||
|
||||
partial void OnShowClockChanged(bool value)
|
||||
{
|
||||
if (_isInitializing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SaveStatusBar();
|
||||
}
|
||||
|
||||
partial void OnSelectedClockFormatChanged(SelectionOption value)
|
||||
partial void OnSelectedSystemMaterialModeChanged(SelectionOption value)
|
||||
{
|
||||
if (_isInitializing || value is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SaveStatusBar();
|
||||
PersistCurrentState(restartRequired: true);
|
||||
}
|
||||
|
||||
private void SaveTheme()
|
||||
[RelayCommand]
|
||||
private void ApplyCustomSeed()
|
||||
{
|
||||
_settingsFacade.Theme.Save(new ThemeAppearanceSettingsState(
|
||||
IsNightMode,
|
||||
string.IsNullOrWhiteSpace(ThemeColor) ? null : ThemeColor,
|
||||
UseSystemChrome));
|
||||
}
|
||||
|
||||
private void SaveWallpaper()
|
||||
{
|
||||
var current = _settingsFacade.Wallpaper.Get();
|
||||
_settingsFacade.Wallpaper.Save(new WallpaperSettingsState(
|
||||
string.IsNullOrWhiteSpace(WallpaperPath) ? null : WallpaperPath,
|
||||
current.Type,
|
||||
current.Color,
|
||||
SelectedWallpaperPlacement.Value));
|
||||
}
|
||||
|
||||
private void SaveStatusBar()
|
||||
{
|
||||
var state = _settingsFacade.StatusBar.Get();
|
||||
var topComponents = state.TopStatusComponentIds
|
||||
.Where(id => !string.Equals(id, BuiltInComponentIds.Clock, StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
|
||||
if (ShowClock)
|
||||
if (!IsThemeColorEditable)
|
||||
{
|
||||
topComponents.Add(BuiltInComponentIds.Clock);
|
||||
return;
|
||||
}
|
||||
|
||||
_settingsFacade.StatusBar.Save(new StatusBarSettingsState(
|
||||
topComponents,
|
||||
state.PinnedTaskbarActions,
|
||||
state.EnableDynamicTaskbarActions,
|
||||
state.TaskbarLayoutMode,
|
||||
SelectedClockFormat.Value,
|
||||
state.SpacingMode,
|
||||
state.CustomSpacingPercent));
|
||||
ThemeColor = CustomSeedPickerValue.ToString();
|
||||
PersistCurrentState(restartRequired: false);
|
||||
}
|
||||
|
||||
private IReadOnlyList<SelectionOption> CreateWallpaperPlacements()
|
||||
public void CancelCustomSeedPreview()
|
||||
{
|
||||
return
|
||||
[
|
||||
new SelectionOption("Fill", L("settings.wallpaper.placement.fill", "Fill")),
|
||||
new SelectionOption("Fit", L("settings.wallpaper.placement.fit", "Fit")),
|
||||
new SelectionOption("Stretch", L("settings.wallpaper.placement.stretch", "Stretch")),
|
||||
new SelectionOption("Center", L("settings.wallpaper.placement.center", "Center")),
|
||||
new SelectionOption("Tile", L("settings.wallpaper.placement.tile", "Tile"))
|
||||
];
|
||||
if (_isInitializing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SyncCustomSeedPickerWithSavedThemeColor();
|
||||
UpdatePreview(BuildPendingState(usePickerSeed: false));
|
||||
}
|
||||
|
||||
private IReadOnlyList<SelectionOption> CreateClockFormats()
|
||||
public void SelectWallpaperSeed(string value)
|
||||
{
|
||||
return
|
||||
[
|
||||
new SelectionOption("HourMinute", L("settings.status_bar.clock_format.hm", "Hour:Minute")),
|
||||
new SelectionOption("HourMinuteSecond", L("settings.status_bar.clock_format.hms", "Hour:Minute:Second"))
|
||||
];
|
||||
if (!IsWallpaperMode || string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_selectedWallpaperSeed = value;
|
||||
PersistCurrentState(restartRequired: true);
|
||||
}
|
||||
|
||||
private void RefreshLocalizedText()
|
||||
{
|
||||
PageTitle = L("settings.appearance.title", "Appearance");
|
||||
PageDescription = L("settings.appearance.description", "Theme and status bar presentation.");
|
||||
PageDescription = L("settings.appearance.description", "Adjust theme source, material background, and window chrome.");
|
||||
ThemeHeader = L("settings.appearance.theme_header", "Theme");
|
||||
NightModeLabel = L("settings.color.enable_night_mode_toggle", "Enable night mode");
|
||||
UseSystemChromeLabel = L("settings.color.use_system_chrome_toggle", "Use system window chrome");
|
||||
ThemeColorLabel = L("settings.color.theme_color_label", "Theme Accent Color");
|
||||
WallpaperHeader = L("settings.wallpaper.title", "Wallpaper");
|
||||
WallpaperPathLabel = L("settings.wallpaper.current_label", "Current Wallpaper");
|
||||
WallpaperPlacementLabel = L("settings.wallpaper.placement_label", "Placement");
|
||||
ImportWallpaperButtonText = L("settings.wallpaper.pick_button", "Import Wallpaper");
|
||||
ClockHeader = L("settings.status_bar.clock_header", "Clock Component");
|
||||
ClockDescription = L("settings.status_bar.clock_description", "Display a clock on the top status bar.");
|
||||
ClockFormatLabel = L("settings.status_bar.clock_format_label", "Clock Format");
|
||||
FilePickerTitle = L("filepicker.title", "Select wallpaper");
|
||||
ThemeColorModeLabel = L("settings.appearance.theme_color_mode_label", "Theme color source");
|
||||
SystemMaterialLabel = L("settings.appearance.system_material_label", "System material");
|
||||
ThemeSourceNeutralText = L("settings.appearance.theme_color_mode.neutral", "Default neutral");
|
||||
ThemeSourceUserColorText = L("settings.appearance.theme_color_mode.user", "User theme color Monet");
|
||||
ThemeSourceWallpaperText = L("settings.appearance.theme_color_mode.wallpaper", "Wallpaper Monet");
|
||||
ThemeSourceDefaultDescription = L("settings.appearance.theme_color_mode_desc.neutral", "Use the standard light and dark neutral surfaces.");
|
||||
ThemeSourceUserColorDescription = L("settings.appearance.theme_color_mode_desc.user", "Use the selected theme color as the Monet seed.");
|
||||
ThemeSourceWallpaperDescription = L("settings.appearance.theme_color_mode_desc.wallpaper", "Use the current wallpaper palette. App wallpaper is preferred, then system wallpaper.");
|
||||
ThemeSourceWallpaperAppDescription = L("settings.appearance.theme_color_preview.app", "Currently previewing colors extracted from the app wallpaper.");
|
||||
ThemeSourceWallpaperSystemDescription = L("settings.appearance.theme_color_preview.system", "Currently previewing colors extracted from the system wallpaper.");
|
||||
ThemeSourceWallpaperFallbackDescription = L("settings.appearance.theme_color_preview.fallback", "No usable wallpaper was found. The app is using a fallback accent.");
|
||||
SystemMaterialNoneText = L("settings.appearance.system_material.none", "None");
|
||||
SystemMaterialMicaText = L("settings.appearance.system_material.mica", "Mica");
|
||||
SystemMaterialAcrylicText = L("settings.appearance.system_material.acrylic", "Acrylic");
|
||||
SystemMaterialSwitchableDescription = L("settings.appearance.system_material_desc.switchable", "Apply the selected material to windows, Dock, status bar, and component hosts.");
|
||||
SystemMaterialFixedDescription = L("settings.appearance.system_material_desc.fixed", "Your current system only exposes the available material modes listed here.");
|
||||
AppearanceRestartMessage = L(
|
||||
"settings.appearance.restart_message",
|
||||
"Theme source and system material changes require restarting the app.");
|
||||
PreviewPrimaryLabel = L("settings.appearance.preview.primary", "Primary");
|
||||
PreviewSecondaryLabel = L("settings.appearance.preview.secondary", "Secondary");
|
||||
PreviewTertiaryLabel = L("settings.appearance.preview.tertiary", "Tertiary");
|
||||
PreviewNeutralLabel = L("settings.appearance.preview.neutral", "Neutral");
|
||||
PreviewSeedLabel = L("settings.appearance.preview.seed", "Seed");
|
||||
PreviewNeutralLightLabel = L("settings.appearance.preview.neutral_light", "White");
|
||||
PreviewNeutralDarkLabel = L("settings.appearance.preview.neutral_dark", "Black");
|
||||
SeedApplyButtonText = L("settings.appearance.preview.apply_seed", "Apply");
|
||||
WallpaperSeedFlyoutTitle = L("settings.appearance.preview.wallpaper_candidates", "Wallpaper seed candidates");
|
||||
WallpaperSeedCurrentText = L("settings.appearance.preview.wallpaper_current", "Current");
|
||||
}
|
||||
|
||||
private void RefreshMaterialModeOptions(AppearanceThemeSnapshot snapshot)
|
||||
{
|
||||
SystemMaterialModes = snapshot.AvailableSystemMaterialModes
|
||||
.Select(value => new SelectionOption(value, ResolveMaterialModeLabel(value)))
|
||||
.ToList();
|
||||
SystemMaterialDescription = snapshot.CanChangeSystemMaterial
|
||||
? SystemMaterialSwitchableDescription
|
||||
: SystemMaterialFixedDescription;
|
||||
}
|
||||
|
||||
private void ApplySavedState(ThemeAppearanceSettingsState theme)
|
||||
{
|
||||
IsNightMode = theme.IsNightMode;
|
||||
ThemeColor = theme.ThemeColor ?? string.Empty;
|
||||
UseSystemChrome = theme.UseSystemChrome;
|
||||
_selectedWallpaperSeed = theme.SelectedWallpaperSeed;
|
||||
SyncCustomSeedPickerWithSavedThemeColor();
|
||||
|
||||
var savedThemeColorMode = ThemeAppearanceValues.NormalizeThemeColorMode(theme.ThemeColorMode, theme.ThemeColor);
|
||||
var savedSystemMaterialMode = ThemeAppearanceValues.NormalizeSystemMaterialMode(theme.SystemMaterialMode);
|
||||
SelectedThemeColorMode = ThemeColorModes.FirstOrDefault(option =>
|
||||
string.Equals(option.Value, savedThemeColorMode, StringComparison.OrdinalIgnoreCase))
|
||||
?? ThemeColorModes[0];
|
||||
SelectedSystemMaterialMode = SystemMaterialModes.FirstOrDefault(option =>
|
||||
string.Equals(option.Value, savedSystemMaterialMode, StringComparison.OrdinalIgnoreCase))
|
||||
?? SystemMaterialModes[0];
|
||||
}
|
||||
|
||||
private void PersistCurrentState(bool restartRequired)
|
||||
{
|
||||
var pendingState = BuildPendingState(usePickerSeed: false);
|
||||
_settingsFacade.Theme.Save(pendingState);
|
||||
var savedState = _settingsFacade.Theme.Get();
|
||||
|
||||
_isInitializing = true;
|
||||
try
|
||||
{
|
||||
ApplySavedState(savedState);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isInitializing = false;
|
||||
}
|
||||
|
||||
RefreshMaterialModeOptions(_appearanceThemeService.GetCurrent());
|
||||
UpdatePreview(savedState);
|
||||
|
||||
if (restartRequired)
|
||||
{
|
||||
RestartRequested?.Invoke(AppearanceRestartMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private ThemeAppearanceSettingsState BuildPendingState(bool usePickerSeed)
|
||||
{
|
||||
var themeColorMode = ThemeAppearanceValues.NormalizeThemeColorMode(SelectedThemeColorMode?.Value, ThemeColor);
|
||||
var themeColor = themeColorMode == ThemeAppearanceValues.ColorModeSeedMonet
|
||||
? (usePickerSeed ? CustomSeedPickerValue.ToString() : string.IsNullOrWhiteSpace(ThemeColor) ? null : ThemeColor)
|
||||
: string.IsNullOrWhiteSpace(ThemeColor) ? null : ThemeColor;
|
||||
|
||||
return new ThemeAppearanceSettingsState(
|
||||
IsNightMode,
|
||||
themeColor,
|
||||
UseSystemChrome,
|
||||
themeColorMode,
|
||||
ThemeAppearanceValues.NormalizeSystemMaterialMode(SelectedSystemMaterialMode?.Value),
|
||||
_selectedWallpaperSeed);
|
||||
}
|
||||
|
||||
private void UpdatePreview(ThemeAppearanceSettingsState pendingState)
|
||||
{
|
||||
var preview = _appearanceThemeService.BuildPreview(pendingState);
|
||||
var normalizedMode = preview.ThemeColorMode;
|
||||
|
||||
ShowNeutralPreview = normalizedMode == ThemeAppearanceValues.ColorModeDefaultNeutral;
|
||||
ShowMonetPreview = !ShowNeutralPreview;
|
||||
IsThemeColorEditable = normalizedMode == ThemeAppearanceValues.ColorModeSeedMonet;
|
||||
IsWallpaperMode = normalizedMode == ThemeAppearanceValues.ColorModeWallpaperMonet;
|
||||
|
||||
PrimarySwatchBrush = new SolidColorBrush(preview.MonetPalette.Primary);
|
||||
SecondarySwatchBrush = new SolidColorBrush(preview.MonetPalette.Secondary);
|
||||
TertiarySwatchBrush = new SolidColorBrush(preview.MonetPalette.Tertiary);
|
||||
NeutralSwatchBrush = new SolidColorBrush(preview.MonetPalette.Neutral);
|
||||
SeedSwatchBrush = new SolidColorBrush(preview.EffectiveSeedColor);
|
||||
|
||||
if (IsWallpaperMode)
|
||||
{
|
||||
WallpaperSeedCandidates = preview.WallpaperSeedCandidates
|
||||
.Select((color, index) => new ThemeSeedCandidateOption(
|
||||
color.ToString(),
|
||||
$"{PreviewSeedLabel} {index + 1}",
|
||||
color,
|
||||
string.Equals(color.ToString(), _selectedWallpaperSeed, StringComparison.OrdinalIgnoreCase)))
|
||||
.ToArray();
|
||||
if (WallpaperSeedCandidates.Count > 0 &&
|
||||
!string.Equals(_selectedWallpaperSeed, preview.EffectiveSeedColor.ToString(), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_selectedWallpaperSeed = preview.EffectiveSeedColor.ToString();
|
||||
WallpaperSeedCandidates = preview.WallpaperSeedCandidates
|
||||
.Select((color, index) => new ThemeSeedCandidateOption(
|
||||
color.ToString(),
|
||||
$"{PreviewSeedLabel} {index + 1}",
|
||||
color,
|
||||
string.Equals(color.ToString(), _selectedWallpaperSeed, StringComparison.OrdinalIgnoreCase)))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
IsWallpaperSeedSelectable = WallpaperSeedCandidates.Count > 1;
|
||||
ThemeColorSourceDescription = preview.ResolvedSeedSource switch
|
||||
{
|
||||
"app_wallpaper" or "app_video" or "app_solid" => ThemeSourceWallpaperAppDescription,
|
||||
"system_wallpaper" => ThemeSourceWallpaperSystemDescription,
|
||||
_ => ThemeSourceWallpaperFallbackDescription
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
WallpaperSeedCandidates = [];
|
||||
IsWallpaperSeedSelectable = false;
|
||||
ThemeColorSourceDescription = normalizedMode switch
|
||||
{
|
||||
ThemeAppearanceValues.ColorModeDefaultNeutral => ThemeSourceDefaultDescription,
|
||||
_ => ThemeSourceUserColorDescription
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private string ResolveMaterialModeLabel(string value)
|
||||
{
|
||||
return ThemeAppearanceValues.NormalizeSystemMaterialMode(value) switch
|
||||
{
|
||||
ThemeAppearanceValues.MaterialMica => SystemMaterialMicaText,
|
||||
ThemeAppearanceValues.MaterialAcrylic => SystemMaterialAcrylicText,
|
||||
_ => SystemMaterialNoneText
|
||||
};
|
||||
}
|
||||
|
||||
private void SyncCustomSeedPickerWithSavedThemeColor()
|
||||
{
|
||||
CustomSeedPickerValue = !string.IsNullOrWhiteSpace(ThemeColor) && Color.TryParse(ThemeColor, out var parsedColor)
|
||||
? parsedColor
|
||||
: DefaultSeedColor;
|
||||
}
|
||||
|
||||
private IReadOnlyList<SelectionOption> CreateThemeColorModes()
|
||||
{
|
||||
return
|
||||
[
|
||||
new SelectionOption(ThemeAppearanceValues.ColorModeDefaultNeutral, ThemeSourceNeutralText),
|
||||
new SelectionOption(ThemeAppearanceValues.ColorModeSeedMonet, ThemeSourceUserColorText),
|
||||
new SelectionOption(ThemeAppearanceValues.ColorModeWallpaperMonet, ThemeSourceWallpaperText)
|
||||
];
|
||||
}
|
||||
|
||||
private string L(string key, string fallback)
|
||||
|
||||
@@ -169,8 +169,11 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
|
||||
|
||||
private void SaveWallpaper()
|
||||
{
|
||||
var normalizedPath = SelectedWallpaperType?.Value == "SolidColor" || string.IsNullOrWhiteSpace(WallpaperPath)
|
||||
? null
|
||||
: WallpaperPath;
|
||||
_settingsFacade.Wallpaper.Save(new WallpaperSettingsState(
|
||||
string.IsNullOrWhiteSpace(WallpaperPath) ? null : WallpaperPath,
|
||||
normalizedPath,
|
||||
SelectedWallpaperType.Value,
|
||||
SelectedColor,
|
||||
SelectedWallpaperPlacement.Value));
|
||||
|
||||
@@ -41,19 +41,8 @@ public sealed partial class WeatherSettingsPageViewModel : ViewModelBase
|
||||
LocationModes = CreateLocationModes();
|
||||
|
||||
var weatherState = _settingsFacade.Weather.Get();
|
||||
SearchKeyword = weatherState.LocationQuery;
|
||||
SelectedLocationMode = LocationModes.FirstOrDefault(option =>
|
||||
string.Equals(option.Value, weatherState.LocationMode, StringComparison.OrdinalIgnoreCase))
|
||||
?? LocationModes[0];
|
||||
|
||||
_isInitializing = true;
|
||||
Latitude = weatherState.Latitude;
|
||||
Longitude = weatherState.Longitude;
|
||||
LocationKey = weatherState.LocationKey;
|
||||
LocationName = weatherState.LocationName;
|
||||
AutoRefreshLocation = weatherState.AutoRefreshLocation;
|
||||
ExcludedAlerts = weatherState.ExcludedAlerts;
|
||||
NoTlsRequests = weatherState.NoTlsRequests;
|
||||
ApplySavedState(weatherState, save: false);
|
||||
_isInitializing = false;
|
||||
|
||||
IsLocationSupported = _locationService.IsSupported;
|
||||
@@ -455,15 +444,19 @@ public sealed partial class WeatherSettingsPageViewModel : ViewModelBase
|
||||
var isNight = snapshot.Current.IsDaylight.HasValue
|
||||
? !snapshot.Current.IsDaylight.Value
|
||||
: _settingsFacade.Theme.Get().IsNightMode;
|
||||
var visualKind = HyperOS3WeatherTheme.ResolveVisualKind(snapshot.Current.WeatherCode, isNight);
|
||||
PreviewIcon = HyperOS3WeatherAssetLoader.LoadImage(HyperOS3WeatherTheme.ResolveHeroIconAsset(visualKind));
|
||||
var preview = XiaomiWeatherVisualResolver.Resolve(
|
||||
snapshot.Current.WeatherText,
|
||||
snapshot.Current.WeatherCode,
|
||||
isNight,
|
||||
_languageCode);
|
||||
PreviewIcon = HyperOS3WeatherAssetLoader.LoadImage(preview.PrimaryIconAsset);
|
||||
PreviewLocation = string.IsNullOrWhiteSpace(snapshot.LocationName)
|
||||
? state.LocationName
|
||||
: snapshot.LocationName!;
|
||||
PreviewTemperature = snapshot.Current.TemperatureC.HasValue
|
||||
? string.Format(CultureInfo.InvariantCulture, "{0:0.#}°C", snapshot.Current.TemperatureC.Value)
|
||||
: "--";
|
||||
PreviewCondition = snapshot.Current.WeatherText ?? L("settings.weather.preview_unknown", "Unknown");
|
||||
PreviewCondition = preview.DisplayText;
|
||||
|
||||
var updatedAt = (snapshot.ObservationTime ?? snapshot.FetchedAt).ToLocalTime();
|
||||
PreviewUpdated = string.Format(
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
SizeToContent="Manual"
|
||||
ShowInTaskbar="False"
|
||||
SystemDecorations="BorderOnly"
|
||||
Background="{DynamicResource EditorWindowBackgroundBrush}"
|
||||
Background="Transparent"
|
||||
Title="Component Editor">
|
||||
<Window.Resources>
|
||||
<!-- Material Design 3 Brushes -->
|
||||
|
||||
@@ -96,6 +96,7 @@ public sealed class DesktopComponentRuntimeDescriptor
|
||||
ArgumentNullException.ThrowIfNull(settingsFacade);
|
||||
|
||||
var settingsService = settingsFacade.Settings;
|
||||
var appearanceTheme = HostAppearanceThemeProvider.GetOrCreate();
|
||||
var componentAccessor = settingsService.GetComponentAccessor(Definition.Id, placementId);
|
||||
var componentSettingsStore = new ComponentSettingsService(settingsService);
|
||||
componentSettingsStore.SetScopedComponentContext(Definition.Id, placementId);
|
||||
@@ -116,6 +117,7 @@ public sealed class DesktopComponentRuntimeDescriptor
|
||||
placementId,
|
||||
settingsFacade,
|
||||
settingsService,
|
||||
appearanceTheme,
|
||||
componentAccessor,
|
||||
componentSettingsStore);
|
||||
|
||||
@@ -133,6 +135,7 @@ public sealed class DesktopComponentRuntimeDescriptor
|
||||
placementId,
|
||||
settingsFacade,
|
||||
settingsService,
|
||||
appearanceTheme,
|
||||
componentAccessor,
|
||||
componentSettingsStore));
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
|
||||
[
|
||||
DailyIcon0, DailyIcon1, DailyIcon2, DailyIcon3, DailyIcon4
|
||||
];
|
||||
_dailyIconKinds = Enumerable.Repeat(HyperOS3WeatherVisualKind.CloudyDay, _dailyIconBlocks.Length).ToArray();
|
||||
_dailyIconKinds = Enumerable.Repeat(HyperOS3WeatherVisualKind.Unknown, _dailyIconBlocks.Length).ToArray();
|
||||
ConfigureTextOverflowGuards();
|
||||
_refreshTimer.Tick += OnRefreshTimerTick;
|
||||
_animationTimer.Tick += OnAnimationTick;
|
||||
@@ -328,12 +328,17 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
|
||||
snapshot,
|
||||
_timeZoneService?.CurrentTimeZone,
|
||||
_timeZoneService?.GetCurrentTime() ?? DateTime.Now);
|
||||
var kind = HyperOS3WeatherTheme.ResolveVisualKind(snapshot.Current.WeatherCode, isNight);
|
||||
var currentVisual = XiaomiWeatherVisualResolver.Resolve(
|
||||
snapshot.Current.WeatherText,
|
||||
snapshot.Current.WeatherCode,
|
||||
isNight,
|
||||
_languageCode);
|
||||
var kind = currentVisual.VisualKind;
|
||||
ApplyVisualTheme(kind);
|
||||
SetLoadingSkeleton(false);
|
||||
WeatherIconImage.Source = HyperOS3WeatherAssetLoader.LoadImage(HyperOS3WeatherTheme.ResolveHeroIconAsset(kind));
|
||||
WeatherIconImage.Source = HyperOS3WeatherAssetLoader.LoadImage(currentVisual.PrimaryIconAsset);
|
||||
CityTextBlock.Text = ResolveLocation(snapshot.LocationName, fallbackLocationName);
|
||||
ConditionTextBlock.Text = ResolveWeatherText(snapshot.Current.WeatherText, kind);
|
||||
ConditionTextBlock.Text = currentVisual.DisplayText;
|
||||
TemperatureTextBlock.Text = FormatTemperature(snapshot.Current.TemperatureC);
|
||||
|
||||
var today = snapshot.DailyForecasts.FirstOrDefault();
|
||||
@@ -354,12 +359,17 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
|
||||
.OrderBy(entry => Math.Abs((entry.Time - target).TotalMinutes))
|
||||
.FirstOrDefault();
|
||||
var weatherCode = item?.Source.WeatherCode ?? snapshot.Current.WeatherCode;
|
||||
var hourKind = HyperOS3WeatherTheme.ResolveVisualKind(weatherCode, IsNightHour(target));
|
||||
var hourVisual = XiaomiWeatherVisualResolver.Resolve(
|
||||
item?.Source.WeatherText,
|
||||
weatherCode,
|
||||
IsNightHour(target),
|
||||
_languageCode);
|
||||
var hourKind = hourVisual.VisualKind;
|
||||
_hourlyTempBlocks[i].Text = i == sunsetSlotIndex
|
||||
? L("weather.hourly.sunset", "Sunset")
|
||||
: FormatTemperature(item?.Source.TemperatureC ?? snapshot.Current.TemperatureC);
|
||||
_hourlyTimeBlocks[i].Text = target.ToString("HH:mm", CultureInfo.InvariantCulture);
|
||||
_hourlyIconBlocks[i].Source = HyperOS3WeatherAssetLoader.LoadImage(HyperOS3WeatherTheme.ResolveMiniIconAsset(hourKind));
|
||||
_hourlyIconBlocks[i].Source = HyperOS3WeatherAssetLoader.LoadImage(hourVisual.CompactIconAsset);
|
||||
}
|
||||
|
||||
var todayDate = DateOnly.FromDateTime(now);
|
||||
@@ -368,21 +378,26 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
|
||||
var date = todayDate.AddDays(i + 1);
|
||||
var daily = snapshot.DailyForecasts.FirstOrDefault(entry => entry.Date == date) ?? snapshot.DailyForecasts.FirstOrDefault();
|
||||
var weatherCode = daily?.DayWeatherCode ?? daily?.NightWeatherCode ?? snapshot.Current.WeatherCode;
|
||||
var dayKind = HyperOS3WeatherTheme.ResolveVisualKind(weatherCode, false);
|
||||
var dayText = ResolveWeatherText(daily?.DayWeatherText ?? daily?.NightWeatherText, dayKind);
|
||||
var dayVisual = XiaomiWeatherVisualResolver.Resolve(
|
||||
daily?.DayWeatherText ?? daily?.NightWeatherText,
|
||||
weatherCode,
|
||||
false,
|
||||
_languageCode);
|
||||
var dayKind = dayVisual.VisualKind;
|
||||
var dayText = dayVisual.DisplayText;
|
||||
_dailyLabelBlocks[i].Text = $"{ResolveDayLabel(date, i + 1)}·{dayText}";
|
||||
_dailyHighBlocks[i].Text = FormatTemperatureValue(daily?.HighTemperatureC);
|
||||
_dailyLowBlocks[i].Text = FormatTemperatureValue(daily?.LowTemperatureC);
|
||||
_dailyIconKinds[i] = dayKind;
|
||||
_dailyIconBlocks[i].Source = HyperOS3WeatherAssetLoader.LoadImage(HyperOS3WeatherTheme.ResolveMiniIconAsset(dayKind));
|
||||
_dailyIconBlocks[i].Source = HyperOS3WeatherAssetLoader.LoadImage(dayVisual.CompactIconAsset);
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyFallback()
|
||||
{
|
||||
ApplyVisualTheme(HyperOS3WeatherVisualKind.CloudyDay);
|
||||
ApplyVisualTheme(HyperOS3WeatherVisualKind.Unknown);
|
||||
SetLoadingSkeleton(false);
|
||||
WeatherIconImage.Source = HyperOS3WeatherAssetLoader.LoadImage(HyperOS3WeatherTheme.ResolveHeroIconAsset(HyperOS3WeatherVisualKind.CloudyDay));
|
||||
WeatherIconImage.Source = HyperOS3WeatherAssetLoader.LoadImage(HyperOS3WeatherTheme.ResolveHeroIconAsset(HyperOS3WeatherVisualKind.Unknown));
|
||||
CityTextBlock.Text = L("weather.widget.location_unknown", "Unknown location");
|
||||
ConditionTextBlock.Text = L("weather.widget.loading", "Loading...");
|
||||
TemperatureTextBlock.Text = "--°";
|
||||
@@ -393,7 +408,7 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
|
||||
{
|
||||
_hourlyTempBlocks[i].Text = i == 3 ? L("weather.hourly.sunset", "Sunset") : "--°";
|
||||
_hourlyTimeBlocks[i].Text = timelineStart.AddHours(i).ToString("HH:mm", CultureInfo.InvariantCulture);
|
||||
_hourlyIconBlocks[i].Source = HyperOS3WeatherAssetLoader.LoadImage(HyperOS3WeatherTheme.ResolveMiniIconAsset(HyperOS3WeatherVisualKind.CloudyDay));
|
||||
_hourlyIconBlocks[i].Source = HyperOS3WeatherAssetLoader.LoadImage(HyperOS3WeatherTheme.ResolveMiniIconAsset(HyperOS3WeatherVisualKind.Unknown));
|
||||
}
|
||||
|
||||
for (var i = 0; i < _dailyLabelBlocks.Length; i++)
|
||||
@@ -401,8 +416,8 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
|
||||
_dailyLabelBlocks[i].Text = $"{ResolveDayLabel(DateOnly.FromDateTime(DateTime.Now).AddDays(i + 1), i + 1)}·{L("weather.widget.condition_cloudy", "Cloudy")}";
|
||||
_dailyHighBlocks[i].Text = "--";
|
||||
_dailyLowBlocks[i].Text = "--";
|
||||
_dailyIconKinds[i] = HyperOS3WeatherVisualKind.CloudyDay;
|
||||
_dailyIconBlocks[i].Source = HyperOS3WeatherAssetLoader.LoadImage(HyperOS3WeatherTheme.ResolveMiniIconAsset(HyperOS3WeatherVisualKind.CloudyDay));
|
||||
_dailyIconKinds[i] = HyperOS3WeatherVisualKind.Unknown;
|
||||
_dailyIconBlocks[i].Source = HyperOS3WeatherAssetLoader.LoadImage(HyperOS3WeatherTheme.ResolveMiniIconAsset(HyperOS3WeatherVisualKind.Unknown));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -417,7 +432,7 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
|
||||
BackgroundMotionLayer.Background = background ?? CreateGradientBrush(palette.GradientFrom, palette.GradientTo);
|
||||
BackgroundTintLayer.Background = CreateSolidBrush(palette.Tint);
|
||||
|
||||
var isNightVisual = kind is HyperOS3WeatherVisualKind.ClearNight or HyperOS3WeatherVisualKind.CloudyNight;
|
||||
var isNightVisual = kind is HyperOS3WeatherVisualKind.ClearNight or HyperOS3WeatherVisualKind.PartlyCloudyNight or HyperOS3WeatherVisualKind.CloudyNight;
|
||||
var backgroundSamples = WeatherTypographyAccessibility.BuildBackgroundSamples(
|
||||
palette.GradientFrom,
|
||||
palette.GradientTo,
|
||||
@@ -612,7 +627,7 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
|
||||
|
||||
var dailyKind = i < _dailyIconKinds.Length
|
||||
? _dailyIconKinds[i]
|
||||
: HyperOS3WeatherVisualKind.CloudyDay;
|
||||
: HyperOS3WeatherVisualKind.Unknown;
|
||||
var dailyIconVisualSize = Math.Clamp(
|
||||
dailyIconSize * ResolveDailyMiniIconScaleBoost(dailyKind),
|
||||
8,
|
||||
@@ -851,18 +866,10 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
|
||||
return score;
|
||||
}
|
||||
|
||||
private string ResolveWeatherText(string? weatherText, HyperOS3WeatherVisualKind kind)
|
||||
private string ResolveWeatherText(string? weatherText, int? weatherCode, HyperOS3WeatherVisualKind kind)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(weatherText)) return weatherText;
|
||||
return kind switch
|
||||
{
|
||||
HyperOS3WeatherVisualKind.ClearDay or HyperOS3WeatherVisualKind.ClearNight => L("weather.widget.condition_clear", "Clear"),
|
||||
HyperOS3WeatherVisualKind.CloudyDay or HyperOS3WeatherVisualKind.CloudyNight => L("weather.widget.condition_cloudy", "Cloudy"),
|
||||
HyperOS3WeatherVisualKind.RainLight or HyperOS3WeatherVisualKind.RainHeavy => L("weather.widget.condition_rain", "Rain"),
|
||||
HyperOS3WeatherVisualKind.Storm => L("weather.widget.condition_storm", "Thunderstorm"),
|
||||
HyperOS3WeatherVisualKind.Snow => L("weather.widget.condition_snow", "Snow"),
|
||||
_ => L("weather.widget.condition_fog", "Fog")
|
||||
};
|
||||
_ = kind;
|
||||
return XiaomiWeatherVisualResolver.ResolveDisplayText(weatherText, weatherCode, _languageCode);
|
||||
}
|
||||
|
||||
private DateTime ConvertToConfiguredTime(DateTimeOffset sourceTime)
|
||||
@@ -999,7 +1006,8 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
|
||||
kind switch
|
||||
{
|
||||
HyperOS3WeatherVisualKind.RainLight or HyperOS3WeatherVisualKind.RainHeavy or HyperOS3WeatherVisualKind.Storm or HyperOS3WeatherVisualKind.Snow => 1.16,
|
||||
HyperOS3WeatherVisualKind.ClearNight or HyperOS3WeatherVisualKind.CloudyNight => 1.08,
|
||||
HyperOS3WeatherVisualKind.ClearNight or HyperOS3WeatherVisualKind.PartlyCloudyNight or HyperOS3WeatherVisualKind.CloudyNight => 1.08,
|
||||
HyperOS3WeatherVisualKind.Haze or HyperOS3WeatherVisualKind.Fog => 1.04,
|
||||
_ => 1.0
|
||||
};
|
||||
|
||||
@@ -1008,9 +1016,13 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
|
||||
{
|
||||
HyperOS3WeatherVisualKind.CloudyDay => 1.30,
|
||||
HyperOS3WeatherVisualKind.CloudyNight => 1.28,
|
||||
HyperOS3WeatherVisualKind.PartlyCloudyDay => 1.28,
|
||||
HyperOS3WeatherVisualKind.PartlyCloudyNight => 1.26,
|
||||
HyperOS3WeatherVisualKind.ClearDay => 1.26,
|
||||
HyperOS3WeatherVisualKind.ClearNight => 1.24,
|
||||
HyperOS3WeatherVisualKind.Haze => 1.20,
|
||||
HyperOS3WeatherVisualKind.Fog => 1.18,
|
||||
HyperOS3WeatherVisualKind.Sleet => 1.14,
|
||||
HyperOS3WeatherVisualKind.RainLight => 1.14,
|
||||
HyperOS3WeatherVisualKind.RainHeavy => 1.12,
|
||||
HyperOS3WeatherVisualKind.Snow => 1.12,
|
||||
|
||||
@@ -22,10 +22,15 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
|
||||
{
|
||||
private enum WeatherVisualKind
|
||||
{
|
||||
Unknown,
|
||||
ClearDay,
|
||||
ClearNight,
|
||||
PartlyCloudyDay,
|
||||
PartlyCloudyNight,
|
||||
CloudyDay,
|
||||
CloudyNight,
|
||||
Haze,
|
||||
Sleet,
|
||||
RainLight,
|
||||
RainHeavy,
|
||||
Storm,
|
||||
@@ -482,7 +487,12 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
|
||||
private void ApplySnapshot(WeatherSnapshot snapshot, string fallbackLocationName)
|
||||
{
|
||||
var isNight = ResolveIsNight(snapshot);
|
||||
var visualKind = ResolveVisualKind(snapshot.Current.WeatherCode, isNight);
|
||||
var visual = XiaomiWeatherVisualResolver.Resolve(
|
||||
snapshot.Current.WeatherText,
|
||||
snapshot.Current.WeatherCode,
|
||||
isNight,
|
||||
_languageCode);
|
||||
var visualKind = ResolveVisualKind(visual.VisualKind);
|
||||
ApplyVisualTheme(visualKind);
|
||||
|
||||
var rawLocation = string.IsNullOrWhiteSpace(snapshot.LocationName)
|
||||
@@ -490,8 +500,8 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
|
||||
: snapshot.LocationName;
|
||||
CityTextBlock.Text = ResolvePreciseDisplayLocation(rawLocation, _languageCode, L("weather.widget.location_unknown", "Unknown location"));
|
||||
|
||||
ConditionTextBlock.Text = ResolveWeatherConditionText(snapshot.Current.WeatherText, visualKind);
|
||||
SetMainWeatherIcon(visualKind);
|
||||
ConditionTextBlock.Text = visual.DisplayText;
|
||||
SetMainWeatherIcon(visual.PrimaryIconAsset, visualKind);
|
||||
SetLoadingSkeleton(false);
|
||||
|
||||
TemperatureTextBlock.Text = FormatTemperature(snapshot.Current.TemperatureC);
|
||||
@@ -505,7 +515,7 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
|
||||
{
|
||||
var fallbackKind = ResolveFallbackVisualKind();
|
||||
ApplyVisualTheme(fallbackKind);
|
||||
SetMainWeatherIcon(fallbackKind);
|
||||
SetMainWeatherIcon(null, fallbackKind);
|
||||
SetLoadingSkeleton(false);
|
||||
CityTextBlock.Text = L("weather.widget.location_not_configured", "Weather location is not configured");
|
||||
ConditionTextBlock.Text = L("weather.widget.configure_hint", "Open Settings > Weather to configure");
|
||||
@@ -520,7 +530,7 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
|
||||
{
|
||||
var loadingKind = IsNightNow() ? WeatherVisualKind.CloudyNight : WeatherVisualKind.CloudyDay;
|
||||
ApplyVisualTheme(loadingKind);
|
||||
SetMainWeatherIcon(loadingKind);
|
||||
SetMainWeatherIcon(null, loadingKind);
|
||||
SetLoadingSkeleton(true);
|
||||
CityTextBlock.Text = ResolvePreciseDisplayLocation(
|
||||
locationName,
|
||||
@@ -535,8 +545,8 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
|
||||
|
||||
private void ApplyFailedState(string locationName)
|
||||
{
|
||||
ApplyVisualTheme(WeatherVisualKind.Fog);
|
||||
SetMainWeatherIcon(WeatherVisualKind.Fog);
|
||||
ApplyVisualTheme(WeatherVisualKind.Unknown);
|
||||
SetMainWeatherIcon(HyperOS3WeatherTheme.ResolveHeroIconAsset(HyperOS3WeatherVisualKind.Unknown), WeatherVisualKind.Unknown);
|
||||
SetLoadingSkeleton(false);
|
||||
CityTextBlock.Text = ResolvePreciseDisplayLocation(
|
||||
locationName,
|
||||
@@ -545,7 +555,7 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
|
||||
ConditionTextBlock.Text = L("weather.widget.fetch_failed", "Weather fetch failed");
|
||||
TemperatureTextBlock.Text = "--°";
|
||||
RangeTextBlock.Text = L("weather.widget.range_unknown", "-- / --");
|
||||
ApplyHourlyForecastItems(BuildPlaceholderHourlyForecastItems(WeatherVisualKind.Fog));
|
||||
ApplyHourlyForecastItems(BuildPlaceholderHourlyForecastItems(WeatherVisualKind.Unknown));
|
||||
ApplyAdaptiveTypography();
|
||||
_latestSnapshot = null;
|
||||
}
|
||||
@@ -560,7 +570,7 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
|
||||
BackgroundTintLayer.Background = CreateSolidBrush(palette.Tint);
|
||||
|
||||
var particleBrush = ResolveParticleBrush(ToThemeKind(kind), palette.ParticleColor);
|
||||
var isNightVisual = kind is WeatherVisualKind.ClearNight or WeatherVisualKind.CloudyNight;
|
||||
var isNightVisual = kind is WeatherVisualKind.ClearNight or WeatherVisualKind.PartlyCloudyNight or WeatherVisualKind.CloudyNight;
|
||||
var backgroundSamples = WeatherTypographyAccessibility.BuildBackgroundSamples(
|
||||
palette.GradientFrom,
|
||||
palette.GradientTo,
|
||||
@@ -690,17 +700,28 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
|
||||
|
||||
private static WeatherVisualKind ResolveVisualKind(int? weatherCode, bool isNight)
|
||||
{
|
||||
return HyperOS3WeatherTheme.ResolveVisualKind(weatherCode, isNight) switch
|
||||
return ResolveVisualKind(HyperOS3WeatherTheme.ResolveVisualKind(weatherCode, isNight));
|
||||
}
|
||||
|
||||
private static WeatherVisualKind ResolveVisualKind(HyperOS3WeatherVisualKind kind)
|
||||
{
|
||||
return kind switch
|
||||
{
|
||||
HyperOS3WeatherVisualKind.Unknown => WeatherVisualKind.Unknown,
|
||||
HyperOS3WeatherVisualKind.ClearDay => WeatherVisualKind.ClearDay,
|
||||
HyperOS3WeatherVisualKind.ClearNight => WeatherVisualKind.ClearNight,
|
||||
HyperOS3WeatherVisualKind.PartlyCloudyDay => WeatherVisualKind.PartlyCloudyDay,
|
||||
HyperOS3WeatherVisualKind.PartlyCloudyNight => WeatherVisualKind.PartlyCloudyNight,
|
||||
HyperOS3WeatherVisualKind.CloudyDay => WeatherVisualKind.CloudyDay,
|
||||
HyperOS3WeatherVisualKind.CloudyNight => WeatherVisualKind.CloudyNight,
|
||||
HyperOS3WeatherVisualKind.Haze => WeatherVisualKind.Haze,
|
||||
HyperOS3WeatherVisualKind.Sleet => WeatherVisualKind.Sleet,
|
||||
HyperOS3WeatherVisualKind.RainLight => WeatherVisualKind.RainLight,
|
||||
HyperOS3WeatherVisualKind.RainHeavy => WeatherVisualKind.RainHeavy,
|
||||
HyperOS3WeatherVisualKind.Storm => WeatherVisualKind.Storm,
|
||||
HyperOS3WeatherVisualKind.Snow => WeatherVisualKind.Snow,
|
||||
_ => WeatherVisualKind.Fog
|
||||
HyperOS3WeatherVisualKind.Fog => WeatherVisualKind.Fog,
|
||||
_ => WeatherVisualKind.Unknown
|
||||
};
|
||||
}
|
||||
|
||||
@@ -721,35 +742,28 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
|
||||
{
|
||||
return kind switch
|
||||
{
|
||||
WeatherVisualKind.Unknown => HyperOS3WeatherVisualKind.Unknown,
|
||||
WeatherVisualKind.ClearDay => HyperOS3WeatherVisualKind.ClearDay,
|
||||
WeatherVisualKind.ClearNight => HyperOS3WeatherVisualKind.ClearNight,
|
||||
WeatherVisualKind.PartlyCloudyDay => HyperOS3WeatherVisualKind.PartlyCloudyDay,
|
||||
WeatherVisualKind.PartlyCloudyNight => HyperOS3WeatherVisualKind.PartlyCloudyNight,
|
||||
WeatherVisualKind.CloudyDay => HyperOS3WeatherVisualKind.CloudyDay,
|
||||
WeatherVisualKind.CloudyNight => HyperOS3WeatherVisualKind.CloudyNight,
|
||||
WeatherVisualKind.Haze => HyperOS3WeatherVisualKind.Haze,
|
||||
WeatherVisualKind.Sleet => HyperOS3WeatherVisualKind.Sleet,
|
||||
WeatherVisualKind.RainLight => HyperOS3WeatherVisualKind.RainLight,
|
||||
WeatherVisualKind.RainHeavy => HyperOS3WeatherVisualKind.RainHeavy,
|
||||
WeatherVisualKind.Storm => HyperOS3WeatherVisualKind.Storm,
|
||||
WeatherVisualKind.Snow => HyperOS3WeatherVisualKind.Snow,
|
||||
_ => HyperOS3WeatherVisualKind.Fog
|
||||
WeatherVisualKind.Fog => HyperOS3WeatherVisualKind.Fog,
|
||||
_ => HyperOS3WeatherVisualKind.Unknown
|
||||
};
|
||||
}
|
||||
|
||||
private string ResolveWeatherConditionText(string? weatherText, WeatherVisualKind kind)
|
||||
private string ResolveWeatherConditionText(string? weatherText, int? weatherCode, WeatherVisualKind kind)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(weatherText))
|
||||
{
|
||||
return weatherText;
|
||||
}
|
||||
|
||||
return kind switch
|
||||
{
|
||||
WeatherVisualKind.ClearDay or WeatherVisualKind.ClearNight => L("weather.widget.condition_clear", "Clear"),
|
||||
WeatherVisualKind.CloudyDay or WeatherVisualKind.CloudyNight => L("weather.widget.condition_cloudy", "Cloudy"),
|
||||
WeatherVisualKind.RainLight or WeatherVisualKind.RainHeavy => L("weather.widget.condition_rain", "Rain"),
|
||||
WeatherVisualKind.Storm => L("weather.widget.condition_storm", "Thunderstorm"),
|
||||
WeatherVisualKind.Snow => L("weather.widget.condition_snow", "Snow"),
|
||||
WeatherVisualKind.Fog => L("weather.widget.condition_fog", "Fog"),
|
||||
_ => L("weather.widget.condition_unknown", "Unknown")
|
||||
};
|
||||
_ = kind;
|
||||
return XiaomiWeatherVisualResolver.ResolveDisplayText(weatherText, weatherCode, _languageCode);
|
||||
}
|
||||
|
||||
private static (double? Low, double? High) ResolveTemperatureRange(WeatherSnapshot snapshot)
|
||||
@@ -1321,15 +1335,16 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
|
||||
return kind switch
|
||||
{
|
||||
WeatherVisualKind.RainLight or WeatherVisualKind.RainHeavy or WeatherVisualKind.Storm or WeatherVisualKind.Snow => 1.16,
|
||||
WeatherVisualKind.ClearNight or WeatherVisualKind.CloudyNight => 1.08,
|
||||
WeatherVisualKind.ClearNight or WeatherVisualKind.PartlyCloudyNight or WeatherVisualKind.CloudyNight => 1.08,
|
||||
WeatherVisualKind.Haze or WeatherVisualKind.Fog => 1.04,
|
||||
_ => 1.0
|
||||
};
|
||||
}
|
||||
|
||||
private void SetMainWeatherIcon(WeatherVisualKind kind)
|
||||
private void SetMainWeatherIcon(string? assetUri, WeatherVisualKind fallbackKind)
|
||||
{
|
||||
WeatherIconImage.Source = HyperOS3WeatherAssetLoader.LoadImage(
|
||||
HyperOS3WeatherTheme.ResolveHeroIconAsset(ToThemeKind(kind)));
|
||||
assetUri ?? HyperOS3WeatherTheme.ResolveHeroIconAsset(ToThemeKind(fallbackKind)));
|
||||
}
|
||||
|
||||
private void SetLoadingSkeleton(bool isLoading)
|
||||
@@ -1549,7 +1564,7 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
|
||||
|
||||
var sway = _activeVisualKind == WeatherVisualKind.Snow
|
||||
? Math.Sin(_animationPhase + (i * 0.45)) * 0.55
|
||||
: _activeVisualKind == WeatherVisualKind.Fog
|
||||
: _activeVisualKind is WeatherVisualKind.Fog or WeatherVisualKind.Haze
|
||||
? Math.Sin((_animationPhase * 0.7) + (i * 0.31)) * 0.18
|
||||
: 0;
|
||||
|
||||
@@ -1579,16 +1594,16 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
|
||||
var thickness = _activeVisualKind switch
|
||||
{
|
||||
WeatherVisualKind.Snow => NextRange(2.2, 4.3),
|
||||
WeatherVisualKind.Fog => NextRange(10.0, 22.0),
|
||||
WeatherVisualKind.Fog or WeatherVisualKind.Haze => NextRange(10.0, 22.0),
|
||||
_ => NextRange(1.0, 2.2)
|
||||
};
|
||||
var opacity = _activeVisualKind switch
|
||||
{
|
||||
WeatherVisualKind.Storm => NextRange(0.26, 0.52),
|
||||
WeatherVisualKind.RainHeavy => NextRange(0.24, 0.46),
|
||||
WeatherVisualKind.RainLight => NextRange(0.18, 0.34),
|
||||
WeatherVisualKind.RainLight or WeatherVisualKind.Sleet => NextRange(0.18, 0.34),
|
||||
WeatherVisualKind.Snow => NextRange(0.40, 0.72),
|
||||
WeatherVisualKind.Fog => NextRange(0.08, 0.20),
|
||||
WeatherVisualKind.Fog or WeatherVisualKind.Haze => NextRange(0.08, 0.20),
|
||||
_ => NextRange(0.10, 0.24)
|
||||
};
|
||||
|
||||
@@ -1600,7 +1615,7 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
|
||||
{
|
||||
WeatherVisualKind.Storm => -24,
|
||||
WeatherVisualKind.RainHeavy => -20,
|
||||
WeatherVisualKind.RainLight => -14,
|
||||
WeatherVisualKind.RainLight or WeatherVisualKind.Sleet => -14,
|
||||
WeatherVisualKind.Snow => -6,
|
||||
_ => 0
|
||||
});
|
||||
|
||||
@@ -3,15 +3,21 @@ using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using LanMountainDesktop.Models;
|
||||
using LanMountainDesktop.Services;
|
||||
|
||||
namespace LanMountainDesktop.Views.Components;
|
||||
|
||||
public enum HyperOS3WeatherVisualKind
|
||||
{
|
||||
Unknown,
|
||||
ClearDay,
|
||||
ClearNight,
|
||||
PartlyCloudyDay,
|
||||
PartlyCloudyNight,
|
||||
CloudyDay,
|
||||
CloudyNight,
|
||||
Haze,
|
||||
Sleet,
|
||||
RainLight,
|
||||
RainHeavy,
|
||||
Storm,
|
||||
@@ -91,38 +97,53 @@ public static class HyperOS3WeatherTheme
|
||||
private static readonly IReadOnlyDictionary<HyperOS3WeatherVisualKind, string> BackgroundAssets =
|
||||
new Dictionary<HyperOS3WeatherVisualKind, string>
|
||||
{
|
||||
[HyperOS3WeatherVisualKind.Unknown] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/hyper_sky_back.png",
|
||||
[HyperOS3WeatherVisualKind.ClearDay] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/hyper_cross_sky_day.png",
|
||||
[HyperOS3WeatherVisualKind.ClearNight] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/hyper_cross_sky_night.png",
|
||||
[HyperOS3WeatherVisualKind.PartlyCloudyDay] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/hyper_cross_sky_day.png",
|
||||
[HyperOS3WeatherVisualKind.PartlyCloudyNight] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/hyper_cross_sky_night.png",
|
||||
[HyperOS3WeatherVisualKind.CloudyDay] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/hyper_sky_front.png",
|
||||
[HyperOS3WeatherVisualKind.CloudyNight] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/hyper_sky_back.png",
|
||||
[HyperOS3WeatherVisualKind.Haze] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/hyper_haze.png",
|
||||
[HyperOS3WeatherVisualKind.Sleet] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/hyper_sky_front.png",
|
||||
[HyperOS3WeatherVisualKind.RainLight] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/hyper_sky_front.png",
|
||||
[HyperOS3WeatherVisualKind.RainHeavy] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/hyper_sky_back.png",
|
||||
[HyperOS3WeatherVisualKind.Storm] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/hyper_cross_sky_night.png",
|
||||
[HyperOS3WeatherVisualKind.Snow] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/hyper_sky_top.png",
|
||||
[HyperOS3WeatherVisualKind.Fog] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/hyper_sky_back.png"
|
||||
[HyperOS3WeatherVisualKind.Fog] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/hyper_fog.png"
|
||||
};
|
||||
|
||||
private static readonly IReadOnlyDictionary<HyperOS3WeatherVisualKind, string> HeroIconAssets =
|
||||
new Dictionary<HyperOS3WeatherVisualKind, string>
|
||||
{
|
||||
[HyperOS3WeatherVisualKind.Unknown] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_cloudy.webp",
|
||||
[HyperOS3WeatherVisualKind.ClearDay] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_hero_sun_soft.png",
|
||||
[HyperOS3WeatherVisualKind.ClearNight] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_hero_moon_soft.png",
|
||||
[HyperOS3WeatherVisualKind.CloudyDay] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_hero_sun_soft.png",
|
||||
[HyperOS3WeatherVisualKind.CloudyNight] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_hero_moon_soft.png",
|
||||
[HyperOS3WeatherVisualKind.RainLight] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_rain_light_soft.png",
|
||||
[HyperOS3WeatherVisualKind.RainHeavy] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_rain_heavy_soft.png",
|
||||
[HyperOS3WeatherVisualKind.Storm] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_storm_soft.png",
|
||||
[HyperOS3WeatherVisualKind.Snow] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_snow_soft.png",
|
||||
[HyperOS3WeatherVisualKind.Fog] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_hero_sun_soft.png"
|
||||
[HyperOS3WeatherVisualKind.PartlyCloudyDay] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_partly_cloudy_day.webp",
|
||||
[HyperOS3WeatherVisualKind.PartlyCloudyNight] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_partly_cloudy_night.webp",
|
||||
[HyperOS3WeatherVisualKind.CloudyDay] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_cloudy.webp",
|
||||
[HyperOS3WeatherVisualKind.CloudyNight] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_cloudy.webp",
|
||||
[HyperOS3WeatherVisualKind.Haze] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_haze.webp",
|
||||
[HyperOS3WeatherVisualKind.Sleet] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_sleet.webp",
|
||||
[HyperOS3WeatherVisualKind.RainLight] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_rain_light.webp",
|
||||
[HyperOS3WeatherVisualKind.RainHeavy] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_rain_heavy.webp",
|
||||
[HyperOS3WeatherVisualKind.Storm] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_thunder.webp",
|
||||
[HyperOS3WeatherVisualKind.Snow] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_snow.webp",
|
||||
[HyperOS3WeatherVisualKind.Fog] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_fog_soft.png"
|
||||
};
|
||||
|
||||
private static readonly IReadOnlyDictionary<HyperOS3WeatherVisualKind, string> MiniIconAssets =
|
||||
new Dictionary<HyperOS3WeatherVisualKind, string>
|
||||
{
|
||||
[HyperOS3WeatherVisualKind.Unknown] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_cloudy_soft.png",
|
||||
[HyperOS3WeatherVisualKind.ClearDay] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_partly_cloudy_day_soft.png",
|
||||
[HyperOS3WeatherVisualKind.ClearNight] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_partly_cloudy_night_soft.png",
|
||||
[HyperOS3WeatherVisualKind.CloudyDay] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_partly_cloudy_day_soft.png",
|
||||
[HyperOS3WeatherVisualKind.CloudyNight] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_partly_cloudy_night_soft.png",
|
||||
[HyperOS3WeatherVisualKind.PartlyCloudyDay] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_partly_cloudy_day_soft.png",
|
||||
[HyperOS3WeatherVisualKind.PartlyCloudyNight] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_partly_cloudy_night_soft.png",
|
||||
[HyperOS3WeatherVisualKind.CloudyDay] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_cloudy_soft.png",
|
||||
[HyperOS3WeatherVisualKind.CloudyNight] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_cloudy_soft.png",
|
||||
[HyperOS3WeatherVisualKind.Haze] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_haze.webp",
|
||||
[HyperOS3WeatherVisualKind.Sleet] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_sleet.webp",
|
||||
[HyperOS3WeatherVisualKind.RainLight] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_rain_light_soft.png",
|
||||
[HyperOS3WeatherVisualKind.RainHeavy] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_rain_heavy_soft.png",
|
||||
[HyperOS3WeatherVisualKind.Storm] = "avares://LanMountainDesktop/Assets/Weather/HyperOS3/Icons/icon_mini_storm_soft.png",
|
||||
@@ -133,6 +154,14 @@ public static class HyperOS3WeatherTheme
|
||||
private static readonly IReadOnlyDictionary<HyperOS3WeatherVisualKind, HyperOS3WeatherPalette> Palettes =
|
||||
new Dictionary<HyperOS3WeatherVisualKind, HyperOS3WeatherPalette>
|
||||
{
|
||||
[HyperOS3WeatherVisualKind.Unknown] = new(
|
||||
GradientFrom: "#6B7785",
|
||||
GradientTo: "#98A4B3",
|
||||
Tint: "#55606E",
|
||||
PrimaryText: "#F8FBFF",
|
||||
SecondaryText: "#E1E8F0",
|
||||
TertiaryText: "#C2CCD8",
|
||||
ParticleColor: "#24FFFFFF"),
|
||||
[HyperOS3WeatherVisualKind.ClearDay] = new(
|
||||
GradientFrom: "#5F7FA3",
|
||||
GradientTo: "#9BB4CF",
|
||||
@@ -149,6 +178,22 @@ public static class HyperOS3WeatherTheme
|
||||
SecondaryText: "#D9E4F0",
|
||||
TertiaryText: "#B4C3D6",
|
||||
ParticleColor: "#00FFFFFF"),
|
||||
[HyperOS3WeatherVisualKind.PartlyCloudyDay] = new(
|
||||
GradientFrom: "#607D9F",
|
||||
GradientTo: "#9BB2C8",
|
||||
Tint: "#55728F",
|
||||
PrimaryText: "#F8FCFF",
|
||||
SecondaryText: "#E4EDF7",
|
||||
TertiaryText: "#C4D4E4",
|
||||
ParticleColor: "#12FFFFFF"),
|
||||
[HyperOS3WeatherVisualKind.PartlyCloudyNight] = new(
|
||||
GradientFrom: "#5A6E87",
|
||||
GradientTo: "#8FA4BC",
|
||||
Tint: "#4D6178",
|
||||
PrimaryText: "#F8FBFF",
|
||||
SecondaryText: "#D9E5F0",
|
||||
TertiaryText: "#B6C5D7",
|
||||
ParticleColor: "#1FE8F2FF"),
|
||||
[HyperOS3WeatherVisualKind.CloudyDay] = new(
|
||||
GradientFrom: "#5D799A",
|
||||
GradientTo: "#95ADC6",
|
||||
@@ -165,6 +210,22 @@ public static class HyperOS3WeatherTheme
|
||||
SecondaryText: "#D4E0ED",
|
||||
TertiaryText: "#B0BFD2",
|
||||
ParticleColor: "#30F0F5FF"),
|
||||
[HyperOS3WeatherVisualKind.Haze] = new(
|
||||
GradientFrom: "#6A7E95",
|
||||
GradientTo: "#A5B2BE",
|
||||
Tint: "#657789",
|
||||
PrimaryText: "#F7FBFF",
|
||||
SecondaryText: "#E3E8EE",
|
||||
TertiaryText: "#C1CBD6",
|
||||
ParticleColor: "#6FD6DEE8"),
|
||||
[HyperOS3WeatherVisualKind.Sleet] = new(
|
||||
GradientFrom: "#61788F",
|
||||
GradientTo: "#9AB0C4",
|
||||
Tint: "#587087",
|
||||
PrimaryText: "#F7FBFF",
|
||||
SecondaryText: "#DCE6F0",
|
||||
TertiaryText: "#B8C7D7",
|
||||
ParticleColor: "#98DCEBFF"),
|
||||
[HyperOS3WeatherVisualKind.RainLight] = new(
|
||||
GradientFrom: "#4F6786",
|
||||
GradientTo: "#7A92AF",
|
||||
@@ -210,6 +271,14 @@ public static class HyperOS3WeatherTheme
|
||||
private static readonly IReadOnlyDictionary<HyperOS3WeatherVisualKind, HyperOS3WeatherMotion> Motions =
|
||||
new Dictionary<HyperOS3WeatherVisualKind, HyperOS3WeatherMotion>
|
||||
{
|
||||
[HyperOS3WeatherVisualKind.Unknown] = new(
|
||||
DriftX: 8.0, DriftY: 5.0, ZoomBase: 1.050, ZoomAmplitude: 0.010,
|
||||
MotionOpacityBase: 0.24, MotionOpacityPulse: 0.05,
|
||||
LightOpacityBase: 0.60, LightOpacityPulse: 0.05,
|
||||
ShadeOpacityBase: 0.80, ShadeOpacityPulse: 0.03,
|
||||
PhaseStep: 0.018, ParticleCount: 0,
|
||||
ParticleSpeedMin: 0, ParticleSpeedMax: 0,
|
||||
ParticleLengthMin: 0, ParticleLengthMax: 0, ParticleDriftPerTick: 0),
|
||||
[HyperOS3WeatherVisualKind.ClearDay] = new(
|
||||
DriftX: 8.0, DriftY: 4.0, ZoomBase: 1.055, ZoomAmplitude: 0.012,
|
||||
MotionOpacityBase: 0.22, MotionOpacityPulse: 0.05,
|
||||
@@ -226,6 +295,22 @@ public static class HyperOS3WeatherTheme
|
||||
PhaseStep: 0.018, ParticleCount: 0,
|
||||
ParticleSpeedMin: 0, ParticleSpeedMax: 0,
|
||||
ParticleLengthMin: 0, ParticleLengthMax: 0, ParticleDriftPerTick: 0),
|
||||
[HyperOS3WeatherVisualKind.PartlyCloudyDay] = new(
|
||||
DriftX: 10.0, DriftY: 6.0, ZoomBase: 1.058, ZoomAmplitude: 0.013,
|
||||
MotionOpacityBase: 0.26, MotionOpacityPulse: 0.05,
|
||||
LightOpacityBase: 0.65, LightOpacityPulse: 0.06,
|
||||
ShadeOpacityBase: 0.76, ShadeOpacityPulse: 0.03,
|
||||
PhaseStep: 0.017, ParticleCount: 0,
|
||||
ParticleSpeedMin: 0, ParticleSpeedMax: 0,
|
||||
ParticleLengthMin: 0, ParticleLengthMax: 0, ParticleDriftPerTick: 0),
|
||||
[HyperOS3WeatherVisualKind.PartlyCloudyNight] = new(
|
||||
DriftX: 12.0, DriftY: 7.0, ZoomBase: 1.061, ZoomAmplitude: 0.013,
|
||||
MotionOpacityBase: 0.30, MotionOpacityPulse: 0.06,
|
||||
LightOpacityBase: 0.55, LightOpacityPulse: 0.05,
|
||||
ShadeOpacityBase: 0.82, ShadeOpacityPulse: 0.03,
|
||||
PhaseStep: 0.019, ParticleCount: 0,
|
||||
ParticleSpeedMin: 0, ParticleSpeedMax: 0,
|
||||
ParticleLengthMin: 0, ParticleLengthMax: 0, ParticleDriftPerTick: 0),
|
||||
[HyperOS3WeatherVisualKind.CloudyDay] = new(
|
||||
DriftX: 12.0, DriftY: 7.0, ZoomBase: 1.060, ZoomAmplitude: 0.013,
|
||||
MotionOpacityBase: 0.32, MotionOpacityPulse: 0.06,
|
||||
@@ -242,6 +327,22 @@ public static class HyperOS3WeatherTheme
|
||||
PhaseStep: 0.021, ParticleCount: 0,
|
||||
ParticleSpeedMin: 0.35, ParticleSpeedMax: 0.80,
|
||||
ParticleLengthMin: 16, ParticleLengthMax: 30, ParticleDriftPerTick: 0.12),
|
||||
[HyperOS3WeatherVisualKind.Haze] = new(
|
||||
DriftX: 9.0, DriftY: 5.0, ZoomBase: 1.052, ZoomAmplitude: 0.010,
|
||||
MotionOpacityBase: 0.30, MotionOpacityPulse: 0.04,
|
||||
LightOpacityBase: 0.54, LightOpacityPulse: 0.04,
|
||||
ShadeOpacityBase: 0.85, ShadeOpacityPulse: 0.03,
|
||||
PhaseStep: 0.018, ParticleCount: 0,
|
||||
ParticleSpeedMin: 0.20, ParticleSpeedMax: 0.45,
|
||||
ParticleLengthMin: 12, ParticleLengthMax: 28, ParticleDriftPerTick: 0.10),
|
||||
[HyperOS3WeatherVisualKind.Sleet] = new(
|
||||
DriftX: 7.0, DriftY: 9.0, ZoomBase: 1.048, ZoomAmplitude: 0.011,
|
||||
MotionOpacityBase: 0.31, MotionOpacityPulse: 0.06,
|
||||
LightOpacityBase: 0.52, LightOpacityPulse: 0.05,
|
||||
ShadeOpacityBase: 0.82, ShadeOpacityPulse: 0.04,
|
||||
PhaseStep: 0.026, ParticleCount: 20,
|
||||
ParticleSpeedMin: 1.20, ParticleSpeedMax: 2.40,
|
||||
ParticleLengthMin: 8, ParticleLengthMax: 18, ParticleDriftPerTick: 0.34),
|
||||
[HyperOS3WeatherVisualKind.RainLight] = new(
|
||||
DriftX: 6.0, DriftY: 10.0, ZoomBase: 1.050, ZoomAmplitude: 0.010,
|
||||
MotionOpacityBase: 0.30, MotionOpacityPulse: 0.08,
|
||||
@@ -296,16 +397,20 @@ public static class HyperOS3WeatherTheme
|
||||
|
||||
public static HyperOS3WeatherVisualKind ResolveVisualKind(int? weatherCode, bool isNight)
|
||||
{
|
||||
return weatherCode switch
|
||||
return XiaomiWeatherCodeMapper.ResolveBucket(weatherCode) switch
|
||||
{
|
||||
0 => isNight ? HyperOS3WeatherVisualKind.ClearNight : HyperOS3WeatherVisualKind.ClearDay,
|
||||
1 or 2 => isNight ? HyperOS3WeatherVisualKind.CloudyNight : HyperOS3WeatherVisualKind.CloudyDay,
|
||||
3 or 7 => HyperOS3WeatherVisualKind.RainLight,
|
||||
8 or 9 => HyperOS3WeatherVisualKind.RainHeavy,
|
||||
4 => HyperOS3WeatherVisualKind.Storm,
|
||||
13 or 14 or 15 or 16 => HyperOS3WeatherVisualKind.Snow,
|
||||
18 or 32 => HyperOS3WeatherVisualKind.Fog,
|
||||
_ => isNight ? HyperOS3WeatherVisualKind.CloudyNight : HyperOS3WeatherVisualKind.CloudyDay
|
||||
WeatherConditionBucket.Unknown => HyperOS3WeatherVisualKind.Unknown,
|
||||
WeatherConditionBucket.Clear => isNight ? HyperOS3WeatherVisualKind.ClearNight : HyperOS3WeatherVisualKind.ClearDay,
|
||||
WeatherConditionBucket.PartlyCloudy => isNight ? HyperOS3WeatherVisualKind.PartlyCloudyNight : HyperOS3WeatherVisualKind.PartlyCloudyDay,
|
||||
WeatherConditionBucket.Cloudy => isNight ? HyperOS3WeatherVisualKind.CloudyNight : HyperOS3WeatherVisualKind.CloudyDay,
|
||||
WeatherConditionBucket.Haze => HyperOS3WeatherVisualKind.Haze,
|
||||
WeatherConditionBucket.Sleet => HyperOS3WeatherVisualKind.Sleet,
|
||||
WeatherConditionBucket.RainLight => HyperOS3WeatherVisualKind.RainLight,
|
||||
WeatherConditionBucket.RainHeavy => HyperOS3WeatherVisualKind.RainHeavy,
|
||||
WeatherConditionBucket.Storm => HyperOS3WeatherVisualKind.Storm,
|
||||
WeatherConditionBucket.Snow => HyperOS3WeatherVisualKind.Snow,
|
||||
WeatherConditionBucket.Fog => HyperOS3WeatherVisualKind.Fog,
|
||||
_ => HyperOS3WeatherVisualKind.Unknown
|
||||
};
|
||||
}
|
||||
|
||||
@@ -360,12 +465,14 @@ public static class HyperOS3WeatherTheme
|
||||
{
|
||||
return kind switch
|
||||
{
|
||||
HyperOS3WeatherVisualKind.RainLight or HyperOS3WeatherVisualKind.RainHeavy or HyperOS3WeatherVisualKind.Storm
|
||||
HyperOS3WeatherVisualKind.Sleet or HyperOS3WeatherVisualKind.RainLight or HyperOS3WeatherVisualKind.RainHeavy or HyperOS3WeatherVisualKind.Storm
|
||||
=> "avares://LanMountainDesktop/Assets/Weather/HyperOS3/hyper_rain_drop.png",
|
||||
HyperOS3WeatherVisualKind.Haze
|
||||
=> "avares://LanMountainDesktop/Assets/Weather/HyperOS3/hyper_haze.png",
|
||||
HyperOS3WeatherVisualKind.Fog
|
||||
=> "avares://LanMountainDesktop/Assets/Weather/HyperOS3/hyper_fog.png",
|
||||
HyperOS3WeatherVisualKind.Snow
|
||||
=> "avares://LanMountainDesktop/Assets/Weather/HyperOS3/hyper_snow_flake.png",
|
||||
HyperOS3WeatherVisualKind.Fog
|
||||
=> "avares://LanMountainDesktop/Assets/Weather/HyperOS3/hyper_haze.png",
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
@@ -20,10 +20,15 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
|
||||
{
|
||||
private enum WeatherVisualKind
|
||||
{
|
||||
Unknown,
|
||||
ClearDay,
|
||||
ClearNight,
|
||||
PartlyCloudyDay,
|
||||
PartlyCloudyNight,
|
||||
CloudyDay,
|
||||
CloudyNight,
|
||||
Haze,
|
||||
Sleet,
|
||||
RainLight,
|
||||
RainHeavy,
|
||||
Storm,
|
||||
@@ -480,7 +485,12 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
|
||||
private void ApplySnapshot(WeatherSnapshot snapshot, string fallbackLocationName)
|
||||
{
|
||||
var isNight = ResolveIsNight(snapshot);
|
||||
var visualKind = ResolveVisualKind(snapshot.Current.WeatherCode, isNight);
|
||||
var visual = XiaomiWeatherVisualResolver.Resolve(
|
||||
snapshot.Current.WeatherText,
|
||||
snapshot.Current.WeatherCode,
|
||||
isNight,
|
||||
_languageCode);
|
||||
var visualKind = ResolveVisualKind(visual.VisualKind);
|
||||
ApplyVisualTheme(visualKind);
|
||||
|
||||
var rawLocation = string.IsNullOrWhiteSpace(snapshot.LocationName)
|
||||
@@ -488,8 +498,8 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
|
||||
: snapshot.LocationName;
|
||||
CityTextBlock.Text = ResolvePreciseDisplayLocation(rawLocation, _languageCode, L("weather.widget.location_unknown", "Unknown location"));
|
||||
|
||||
ConditionTextBlock.Text = ResolveWeatherConditionText(snapshot.Current.WeatherText, visualKind);
|
||||
SetMainWeatherIcon(visualKind);
|
||||
ConditionTextBlock.Text = visual.DisplayText;
|
||||
SetMainWeatherIcon(visual.PrimaryIconAsset, visualKind);
|
||||
SetLoadingSkeleton(false);
|
||||
|
||||
TemperatureTextBlock.Text = FormatTemperature(snapshot.Current.TemperatureC);
|
||||
@@ -503,7 +513,7 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
|
||||
{
|
||||
var fallbackKind = ResolveFallbackVisualKind();
|
||||
ApplyVisualTheme(fallbackKind);
|
||||
SetMainWeatherIcon(fallbackKind);
|
||||
SetMainWeatherIcon(null, fallbackKind);
|
||||
SetLoadingSkeleton(false);
|
||||
CityTextBlock.Text = L("weather.widget.location_not_configured", "Weather location is not configured");
|
||||
ConditionTextBlock.Text = L("weather.widget.condition_unknown", "Unknown");
|
||||
@@ -518,7 +528,7 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
|
||||
{
|
||||
var loadingKind = IsNightNow() ? WeatherVisualKind.CloudyNight : WeatherVisualKind.CloudyDay;
|
||||
ApplyVisualTheme(loadingKind);
|
||||
SetMainWeatherIcon(loadingKind);
|
||||
SetMainWeatherIcon(null, loadingKind);
|
||||
SetLoadingSkeleton(true);
|
||||
CityTextBlock.Text = ResolvePreciseDisplayLocation(
|
||||
locationName,
|
||||
@@ -533,8 +543,8 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
|
||||
|
||||
private void ApplyFailedState(string locationName)
|
||||
{
|
||||
ApplyVisualTheme(WeatherVisualKind.Fog);
|
||||
SetMainWeatherIcon(WeatherVisualKind.Fog);
|
||||
ApplyVisualTheme(WeatherVisualKind.Unknown);
|
||||
SetMainWeatherIcon(HyperOS3WeatherTheme.ResolveHeroIconAsset(HyperOS3WeatherVisualKind.Unknown), WeatherVisualKind.Unknown);
|
||||
SetLoadingSkeleton(false);
|
||||
CityTextBlock.Text = ResolvePreciseDisplayLocation(
|
||||
locationName,
|
||||
@@ -543,7 +553,7 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
|
||||
ConditionTextBlock.Text = L("weather.widget.fetch_failed", "Weather fetch failed");
|
||||
TemperatureTextBlock.Text = "--°";
|
||||
RangeTextBlock.Text = L("weather.widget.range_unknown", "-- / --");
|
||||
ApplyHourlyForecastItems(BuildPlaceholderHourlyForecastItems(WeatherVisualKind.Fog));
|
||||
ApplyHourlyForecastItems(BuildPlaceholderHourlyForecastItems(WeatherVisualKind.Unknown));
|
||||
ApplyAdaptiveTypography();
|
||||
_latestSnapshot = null;
|
||||
}
|
||||
@@ -558,7 +568,7 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
|
||||
BackgroundTintLayer.Background = CreateSolidBrush(palette.Tint);
|
||||
|
||||
var particleBrush = ResolveParticleBrush(ToThemeKind(kind), palette.ParticleColor);
|
||||
var isNightVisual = kind is WeatherVisualKind.ClearNight or WeatherVisualKind.CloudyNight;
|
||||
var isNightVisual = kind is WeatherVisualKind.ClearNight or WeatherVisualKind.PartlyCloudyNight or WeatherVisualKind.CloudyNight;
|
||||
var backgroundSamples = WeatherTypographyAccessibility.BuildBackgroundSamples(
|
||||
palette.GradientFrom,
|
||||
palette.GradientTo,
|
||||
@@ -676,17 +686,28 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
|
||||
|
||||
private static WeatherVisualKind ResolveVisualKind(int? weatherCode, bool isNight)
|
||||
{
|
||||
return HyperOS3WeatherTheme.ResolveVisualKind(weatherCode, isNight) switch
|
||||
return ResolveVisualKind(HyperOS3WeatherTheme.ResolveVisualKind(weatherCode, isNight));
|
||||
}
|
||||
|
||||
private static WeatherVisualKind ResolveVisualKind(HyperOS3WeatherVisualKind kind)
|
||||
{
|
||||
return kind switch
|
||||
{
|
||||
HyperOS3WeatherVisualKind.Unknown => WeatherVisualKind.Unknown,
|
||||
HyperOS3WeatherVisualKind.ClearDay => WeatherVisualKind.ClearDay,
|
||||
HyperOS3WeatherVisualKind.ClearNight => WeatherVisualKind.ClearNight,
|
||||
HyperOS3WeatherVisualKind.PartlyCloudyDay => WeatherVisualKind.PartlyCloudyDay,
|
||||
HyperOS3WeatherVisualKind.PartlyCloudyNight => WeatherVisualKind.PartlyCloudyNight,
|
||||
HyperOS3WeatherVisualKind.CloudyDay => WeatherVisualKind.CloudyDay,
|
||||
HyperOS3WeatherVisualKind.CloudyNight => WeatherVisualKind.CloudyNight,
|
||||
HyperOS3WeatherVisualKind.Haze => WeatherVisualKind.Haze,
|
||||
HyperOS3WeatherVisualKind.Sleet => WeatherVisualKind.Sleet,
|
||||
HyperOS3WeatherVisualKind.RainLight => WeatherVisualKind.RainLight,
|
||||
HyperOS3WeatherVisualKind.RainHeavy => WeatherVisualKind.RainHeavy,
|
||||
HyperOS3WeatherVisualKind.Storm => WeatherVisualKind.Storm,
|
||||
HyperOS3WeatherVisualKind.Snow => WeatherVisualKind.Snow,
|
||||
_ => WeatherVisualKind.Fog
|
||||
HyperOS3WeatherVisualKind.Fog => WeatherVisualKind.Fog,
|
||||
_ => WeatherVisualKind.Unknown
|
||||
};
|
||||
}
|
||||
|
||||
@@ -707,35 +728,28 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
|
||||
{
|
||||
return kind switch
|
||||
{
|
||||
WeatherVisualKind.Unknown => HyperOS3WeatherVisualKind.Unknown,
|
||||
WeatherVisualKind.ClearDay => HyperOS3WeatherVisualKind.ClearDay,
|
||||
WeatherVisualKind.ClearNight => HyperOS3WeatherVisualKind.ClearNight,
|
||||
WeatherVisualKind.PartlyCloudyDay => HyperOS3WeatherVisualKind.PartlyCloudyDay,
|
||||
WeatherVisualKind.PartlyCloudyNight => HyperOS3WeatherVisualKind.PartlyCloudyNight,
|
||||
WeatherVisualKind.CloudyDay => HyperOS3WeatherVisualKind.CloudyDay,
|
||||
WeatherVisualKind.CloudyNight => HyperOS3WeatherVisualKind.CloudyNight,
|
||||
WeatherVisualKind.Haze => HyperOS3WeatherVisualKind.Haze,
|
||||
WeatherVisualKind.Sleet => HyperOS3WeatherVisualKind.Sleet,
|
||||
WeatherVisualKind.RainLight => HyperOS3WeatherVisualKind.RainLight,
|
||||
WeatherVisualKind.RainHeavy => HyperOS3WeatherVisualKind.RainHeavy,
|
||||
WeatherVisualKind.Storm => HyperOS3WeatherVisualKind.Storm,
|
||||
WeatherVisualKind.Snow => HyperOS3WeatherVisualKind.Snow,
|
||||
_ => HyperOS3WeatherVisualKind.Fog
|
||||
WeatherVisualKind.Fog => HyperOS3WeatherVisualKind.Fog,
|
||||
_ => HyperOS3WeatherVisualKind.Unknown
|
||||
};
|
||||
}
|
||||
|
||||
private string ResolveWeatherConditionText(string? weatherText, WeatherVisualKind kind)
|
||||
private string ResolveWeatherConditionText(string? weatherText, int? weatherCode, WeatherVisualKind kind)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(weatherText))
|
||||
{
|
||||
return weatherText;
|
||||
}
|
||||
|
||||
return kind switch
|
||||
{
|
||||
WeatherVisualKind.ClearDay or WeatherVisualKind.ClearNight => L("weather.widget.condition_clear", "Clear"),
|
||||
WeatherVisualKind.CloudyDay or WeatherVisualKind.CloudyNight => L("weather.widget.condition_cloudy", "Cloudy"),
|
||||
WeatherVisualKind.RainLight or WeatherVisualKind.RainHeavy => L("weather.widget.condition_rain", "Rain"),
|
||||
WeatherVisualKind.Storm => L("weather.widget.condition_storm", "Thunderstorm"),
|
||||
WeatherVisualKind.Snow => L("weather.widget.condition_snow", "Snow"),
|
||||
WeatherVisualKind.Fog => L("weather.widget.condition_fog", "Fog"),
|
||||
_ => L("weather.widget.condition_unknown", "Unknown")
|
||||
};
|
||||
_ = kind;
|
||||
return XiaomiWeatherVisualResolver.ResolveDisplayText(weatherText, weatherCode, _languageCode);
|
||||
}
|
||||
|
||||
private static (double? Low, double? High) ResolveTemperatureRange(WeatherSnapshot snapshot)
|
||||
@@ -1171,15 +1185,16 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
|
||||
return kind switch
|
||||
{
|
||||
WeatherVisualKind.RainLight or WeatherVisualKind.RainHeavy or WeatherVisualKind.Storm or WeatherVisualKind.Snow => 1.16,
|
||||
WeatherVisualKind.ClearNight or WeatherVisualKind.CloudyNight => 1.08,
|
||||
WeatherVisualKind.ClearNight or WeatherVisualKind.PartlyCloudyNight or WeatherVisualKind.CloudyNight => 1.08,
|
||||
WeatherVisualKind.Haze or WeatherVisualKind.Fog => 1.04,
|
||||
_ => 1.0
|
||||
};
|
||||
}
|
||||
|
||||
private void SetMainWeatherIcon(WeatherVisualKind kind)
|
||||
private void SetMainWeatherIcon(string? assetUri, WeatherVisualKind fallbackKind)
|
||||
{
|
||||
WeatherIconImage.Source = HyperOS3WeatherAssetLoader.LoadImage(
|
||||
HyperOS3WeatherTheme.ResolveHeroIconAsset(ToThemeKind(kind)));
|
||||
assetUri ?? HyperOS3WeatherTheme.ResolveHeroIconAsset(ToThemeKind(fallbackKind)));
|
||||
}
|
||||
|
||||
private void SetLoadingSkeleton(bool isLoading)
|
||||
@@ -1399,7 +1414,7 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
|
||||
|
||||
var sway = _activeVisualKind == WeatherVisualKind.Snow
|
||||
? Math.Sin(_animationPhase + (i * 0.45)) * 0.55
|
||||
: _activeVisualKind == WeatherVisualKind.Fog
|
||||
: _activeVisualKind is WeatherVisualKind.Fog or WeatherVisualKind.Haze
|
||||
? Math.Sin((_animationPhase * 0.7) + (i * 0.31)) * 0.18
|
||||
: 0;
|
||||
|
||||
@@ -1429,16 +1444,16 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
|
||||
var thickness = _activeVisualKind switch
|
||||
{
|
||||
WeatherVisualKind.Snow => NextRange(2.2, 4.3),
|
||||
WeatherVisualKind.Fog => NextRange(10.0, 22.0),
|
||||
WeatherVisualKind.Fog or WeatherVisualKind.Haze => NextRange(10.0, 22.0),
|
||||
_ => NextRange(1.0, 2.2)
|
||||
};
|
||||
var opacity = _activeVisualKind switch
|
||||
{
|
||||
WeatherVisualKind.Storm => NextRange(0.26, 0.52),
|
||||
WeatherVisualKind.RainHeavy => NextRange(0.24, 0.46),
|
||||
WeatherVisualKind.RainLight => NextRange(0.18, 0.34),
|
||||
WeatherVisualKind.RainLight or WeatherVisualKind.Sleet => NextRange(0.18, 0.34),
|
||||
WeatherVisualKind.Snow => NextRange(0.40, 0.72),
|
||||
WeatherVisualKind.Fog => NextRange(0.08, 0.20),
|
||||
WeatherVisualKind.Fog or WeatherVisualKind.Haze => NextRange(0.08, 0.20),
|
||||
_ => NextRange(0.10, 0.24)
|
||||
};
|
||||
|
||||
@@ -1450,7 +1465,7 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
|
||||
{
|
||||
WeatherVisualKind.Storm => -24,
|
||||
WeatherVisualKind.RainHeavy => -20,
|
||||
WeatherVisualKind.RainLight => -14,
|
||||
WeatherVisualKind.RainLight or WeatherVisualKind.Sleet => -14,
|
||||
WeatherVisualKind.Snow => -6,
|
||||
_ => 0
|
||||
});
|
||||
|
||||
@@ -22,10 +22,15 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
|
||||
{
|
||||
private enum WeatherVisualKind
|
||||
{
|
||||
Unknown,
|
||||
ClearDay,
|
||||
ClearNight,
|
||||
PartlyCloudyDay,
|
||||
PartlyCloudyNight,
|
||||
CloudyDay,
|
||||
CloudyNight,
|
||||
Haze,
|
||||
Sleet,
|
||||
RainLight,
|
||||
RainHeavy,
|
||||
Storm,
|
||||
@@ -427,7 +432,12 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
|
||||
private void ApplySnapshot(WeatherSnapshot snapshot, string fallbackLocationName)
|
||||
{
|
||||
var isNight = ResolveIsNight(snapshot);
|
||||
var visualKind = ResolveVisualKind(snapshot.Current.WeatherCode, isNight);
|
||||
var visual = XiaomiWeatherVisualResolver.Resolve(
|
||||
snapshot.Current.WeatherText,
|
||||
snapshot.Current.WeatherCode,
|
||||
isNight,
|
||||
_languageCode);
|
||||
var visualKind = ResolveVisualKind(visual.VisualKind);
|
||||
ApplyVisualTheme(visualKind);
|
||||
|
||||
var rawLocation = string.IsNullOrWhiteSpace(snapshot.LocationName)
|
||||
@@ -435,8 +445,8 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
|
||||
: snapshot.LocationName;
|
||||
CityTextBlock.Text = ResolvePreciseDisplayLocation(rawLocation, _languageCode, L("weather.widget.location_unknown", "Unknown location"));
|
||||
|
||||
ConditionTextBlock.Text = ResolveWeatherConditionText(snapshot.Current.WeatherText, visualKind);
|
||||
SetWeatherIcon(visualKind);
|
||||
ConditionTextBlock.Text = visual.DisplayText;
|
||||
SetWeatherIcon(visual.PrimaryIconAsset, visualKind);
|
||||
SetLoadingSkeleton(false);
|
||||
|
||||
TemperatureTextBlock.Text = FormatTemperature(snapshot.Current.TemperatureC);
|
||||
@@ -449,7 +459,7 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
|
||||
{
|
||||
var fallbackKind = ResolveFallbackVisualKind();
|
||||
ApplyVisualTheme(fallbackKind);
|
||||
SetWeatherIcon(fallbackKind);
|
||||
SetWeatherIcon(null, fallbackKind);
|
||||
SetLoadingSkeleton(false);
|
||||
CityTextBlock.Text = L("weather.widget.location_not_configured", "Weather location is not configured");
|
||||
ConditionTextBlock.Text = L("weather.widget.configure_hint", "Open Settings > Weather to configure");
|
||||
@@ -463,7 +473,7 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
|
||||
{
|
||||
var loadingKind = IsNightNow() ? WeatherVisualKind.CloudyNight : WeatherVisualKind.CloudyDay;
|
||||
ApplyVisualTheme(loadingKind);
|
||||
SetWeatherIcon(loadingKind);
|
||||
SetWeatherIcon(null, loadingKind);
|
||||
SetLoadingSkeleton(true);
|
||||
CityTextBlock.Text = ResolvePreciseDisplayLocation(
|
||||
locationName,
|
||||
@@ -477,8 +487,8 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
|
||||
|
||||
private void ApplyFailedState(string locationName)
|
||||
{
|
||||
ApplyVisualTheme(WeatherVisualKind.Fog);
|
||||
SetWeatherIcon(WeatherVisualKind.Fog);
|
||||
ApplyVisualTheme(WeatherVisualKind.Unknown);
|
||||
SetWeatherIcon(HyperOS3WeatherTheme.ResolveHeroIconAsset(HyperOS3WeatherVisualKind.Unknown), WeatherVisualKind.Unknown);
|
||||
SetLoadingSkeleton(false);
|
||||
CityTextBlock.Text = ResolvePreciseDisplayLocation(
|
||||
locationName,
|
||||
@@ -500,7 +510,7 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
|
||||
BackgroundMotionLayer.Background = ResolveWeatherBackgroundBrush(kind, palette);
|
||||
BackgroundTintLayer.Background = CreateSolidBrush(palette.Tint);
|
||||
|
||||
var isNightVisual = kind is WeatherVisualKind.ClearNight or WeatherVisualKind.CloudyNight;
|
||||
var isNightVisual = kind is WeatherVisualKind.ClearNight or WeatherVisualKind.PartlyCloudyNight or WeatherVisualKind.CloudyNight;
|
||||
var backgroundSamples = WeatherTypographyAccessibility.BuildBackgroundSamples(
|
||||
palette.GradientFrom,
|
||||
palette.GradientTo,
|
||||
@@ -610,17 +620,28 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
|
||||
|
||||
private static WeatherVisualKind ResolveVisualKind(int? weatherCode, bool isNight)
|
||||
{
|
||||
return HyperOS3WeatherTheme.ResolveVisualKind(weatherCode, isNight) switch
|
||||
return ResolveVisualKind(HyperOS3WeatherTheme.ResolveVisualKind(weatherCode, isNight));
|
||||
}
|
||||
|
||||
private static WeatherVisualKind ResolveVisualKind(HyperOS3WeatherVisualKind kind)
|
||||
{
|
||||
return kind switch
|
||||
{
|
||||
HyperOS3WeatherVisualKind.Unknown => WeatherVisualKind.Unknown,
|
||||
HyperOS3WeatherVisualKind.ClearDay => WeatherVisualKind.ClearDay,
|
||||
HyperOS3WeatherVisualKind.ClearNight => WeatherVisualKind.ClearNight,
|
||||
HyperOS3WeatherVisualKind.PartlyCloudyDay => WeatherVisualKind.PartlyCloudyDay,
|
||||
HyperOS3WeatherVisualKind.PartlyCloudyNight => WeatherVisualKind.PartlyCloudyNight,
|
||||
HyperOS3WeatherVisualKind.CloudyDay => WeatherVisualKind.CloudyDay,
|
||||
HyperOS3WeatherVisualKind.CloudyNight => WeatherVisualKind.CloudyNight,
|
||||
HyperOS3WeatherVisualKind.Haze => WeatherVisualKind.Haze,
|
||||
HyperOS3WeatherVisualKind.Sleet => WeatherVisualKind.Sleet,
|
||||
HyperOS3WeatherVisualKind.RainLight => WeatherVisualKind.RainLight,
|
||||
HyperOS3WeatherVisualKind.RainHeavy => WeatherVisualKind.RainHeavy,
|
||||
HyperOS3WeatherVisualKind.Storm => WeatherVisualKind.Storm,
|
||||
HyperOS3WeatherVisualKind.Snow => WeatherVisualKind.Snow,
|
||||
_ => WeatherVisualKind.Fog
|
||||
HyperOS3WeatherVisualKind.Fog => WeatherVisualKind.Fog,
|
||||
_ => WeatherVisualKind.Unknown
|
||||
};
|
||||
}
|
||||
|
||||
@@ -641,35 +662,28 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
|
||||
{
|
||||
return kind switch
|
||||
{
|
||||
WeatherVisualKind.Unknown => HyperOS3WeatherVisualKind.Unknown,
|
||||
WeatherVisualKind.ClearDay => HyperOS3WeatherVisualKind.ClearDay,
|
||||
WeatherVisualKind.ClearNight => HyperOS3WeatherVisualKind.ClearNight,
|
||||
WeatherVisualKind.PartlyCloudyDay => HyperOS3WeatherVisualKind.PartlyCloudyDay,
|
||||
WeatherVisualKind.PartlyCloudyNight => HyperOS3WeatherVisualKind.PartlyCloudyNight,
|
||||
WeatherVisualKind.CloudyDay => HyperOS3WeatherVisualKind.CloudyDay,
|
||||
WeatherVisualKind.CloudyNight => HyperOS3WeatherVisualKind.CloudyNight,
|
||||
WeatherVisualKind.Haze => HyperOS3WeatherVisualKind.Haze,
|
||||
WeatherVisualKind.Sleet => HyperOS3WeatherVisualKind.Sleet,
|
||||
WeatherVisualKind.RainLight => HyperOS3WeatherVisualKind.RainLight,
|
||||
WeatherVisualKind.RainHeavy => HyperOS3WeatherVisualKind.RainHeavy,
|
||||
WeatherVisualKind.Storm => HyperOS3WeatherVisualKind.Storm,
|
||||
WeatherVisualKind.Snow => HyperOS3WeatherVisualKind.Snow,
|
||||
_ => HyperOS3WeatherVisualKind.Fog
|
||||
WeatherVisualKind.Fog => HyperOS3WeatherVisualKind.Fog,
|
||||
_ => HyperOS3WeatherVisualKind.Unknown
|
||||
};
|
||||
}
|
||||
|
||||
private string ResolveWeatherConditionText(string? weatherText, WeatherVisualKind kind)
|
||||
private string ResolveWeatherConditionText(string? weatherText, int? weatherCode, WeatherVisualKind kind)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(weatherText))
|
||||
{
|
||||
return weatherText;
|
||||
}
|
||||
|
||||
return kind switch
|
||||
{
|
||||
WeatherVisualKind.ClearDay or WeatherVisualKind.ClearNight => L("weather.widget.condition_clear", "Clear"),
|
||||
WeatherVisualKind.CloudyDay or WeatherVisualKind.CloudyNight => L("weather.widget.condition_cloudy", "Cloudy"),
|
||||
WeatherVisualKind.RainLight or WeatherVisualKind.RainHeavy => L("weather.widget.condition_rain", "Rain"),
|
||||
WeatherVisualKind.Storm => L("weather.widget.condition_storm", "Thunderstorm"),
|
||||
WeatherVisualKind.Snow => L("weather.widget.condition_snow", "Snow"),
|
||||
WeatherVisualKind.Fog => L("weather.widget.condition_fog", "Fog"),
|
||||
_ => L("weather.widget.condition_unknown", "Unknown")
|
||||
};
|
||||
_ = kind;
|
||||
return XiaomiWeatherVisualResolver.ResolveDisplayText(weatherText, weatherCode, _languageCode);
|
||||
}
|
||||
|
||||
private static (double? Low, double? High) ResolveTemperatureRange(WeatherSnapshot snapshot)
|
||||
@@ -965,15 +979,16 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
|
||||
return kind switch
|
||||
{
|
||||
WeatherVisualKind.RainLight or WeatherVisualKind.RainHeavy or WeatherVisualKind.Storm or WeatherVisualKind.Snow => 1.16,
|
||||
WeatherVisualKind.ClearNight or WeatherVisualKind.CloudyNight => 1.08,
|
||||
WeatherVisualKind.ClearNight or WeatherVisualKind.PartlyCloudyNight or WeatherVisualKind.CloudyNight => 1.08,
|
||||
WeatherVisualKind.Haze or WeatherVisualKind.Fog => 1.04,
|
||||
_ => 1.0
|
||||
};
|
||||
}
|
||||
|
||||
private void SetWeatherIcon(WeatherVisualKind kind)
|
||||
private void SetWeatherIcon(string? assetUri, WeatherVisualKind fallbackKind)
|
||||
{
|
||||
WeatherIconImage.Source = HyperOS3WeatherAssetLoader.LoadImage(
|
||||
HyperOS3WeatherTheme.ResolveHeroIconAsset(ToThemeKind(kind)));
|
||||
assetUri ?? HyperOS3WeatherTheme.ResolveHeroIconAsset(ToThemeKind(fallbackKind)));
|
||||
}
|
||||
|
||||
private void SetLoadingSkeleton(bool isLoading)
|
||||
@@ -1190,7 +1205,7 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
|
||||
|
||||
var sway = _activeVisualKind == WeatherVisualKind.Snow
|
||||
? Math.Sin(_animationPhase + (i * 0.45)) * 0.55
|
||||
: _activeVisualKind == WeatherVisualKind.Fog
|
||||
: _activeVisualKind is WeatherVisualKind.Fog or WeatherVisualKind.Haze
|
||||
? Math.Sin((_animationPhase * 0.7) + (i * 0.31)) * 0.18
|
||||
: 0;
|
||||
|
||||
@@ -1220,16 +1235,16 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
|
||||
var thickness = _activeVisualKind switch
|
||||
{
|
||||
WeatherVisualKind.Snow => NextRange(2.2, 4.3),
|
||||
WeatherVisualKind.Fog => NextRange(10.0, 22.0),
|
||||
WeatherVisualKind.Fog or WeatherVisualKind.Haze => NextRange(10.0, 22.0),
|
||||
_ => NextRange(1.0, 2.2)
|
||||
};
|
||||
var opacity = _activeVisualKind switch
|
||||
{
|
||||
WeatherVisualKind.Storm => NextRange(0.26, 0.52),
|
||||
WeatherVisualKind.RainHeavy => NextRange(0.24, 0.46),
|
||||
WeatherVisualKind.RainLight => NextRange(0.18, 0.34),
|
||||
WeatherVisualKind.RainLight or WeatherVisualKind.Sleet => NextRange(0.18, 0.34),
|
||||
WeatherVisualKind.Snow => NextRange(0.40, 0.72),
|
||||
WeatherVisualKind.Fog => NextRange(0.08, 0.20),
|
||||
WeatherVisualKind.Fog or WeatherVisualKind.Haze => NextRange(0.08, 0.20),
|
||||
_ => NextRange(0.10, 0.24)
|
||||
};
|
||||
|
||||
@@ -1241,7 +1256,7 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
|
||||
{
|
||||
WeatherVisualKind.Storm => -24,
|
||||
WeatherVisualKind.RainHeavy => -20,
|
||||
WeatherVisualKind.RainLight => -14,
|
||||
WeatherVisualKind.RainLight or WeatherVisualKind.Sleet => -14,
|
||||
WeatherVisualKind.Snow => -6,
|
||||
_ => 0
|
||||
});
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -239,6 +239,7 @@ public partial class MainWindow
|
||||
private void ApplyTopStatusComponentVisibility()
|
||||
{
|
||||
var showClock = _topStatusComponentIds.Contains(BuiltInComponentIds.Clock);
|
||||
var hasVisibleTopStatusComponent = false;
|
||||
|
||||
if (ClockWidget is not null)
|
||||
{
|
||||
@@ -248,8 +249,33 @@ public partial class MainWindow
|
||||
ClockWidget.SetDisplayFormat(_clockDisplayFormat);
|
||||
var columnSpan = _clockDisplayFormat == ClockDisplayFormat.HourMinute ? 2 : 3;
|
||||
Grid.SetColumnSpan(ClockWidget, columnSpan);
|
||||
hasVisibleTopStatusComponent = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (TopStatusBarHost is not null)
|
||||
{
|
||||
TopStatusBarHost.IsVisible = hasVisibleTopStatusComponent;
|
||||
}
|
||||
|
||||
if (WallpaperPreviewClockWidget is not null)
|
||||
{
|
||||
WallpaperPreviewClockWidget.IsVisible = showClock;
|
||||
if (showClock)
|
||||
{
|
||||
WallpaperPreviewClockWidget.SetDisplayFormat(_clockDisplayFormat);
|
||||
}
|
||||
}
|
||||
|
||||
if (WallpaperPreviewTopStatusBarHost is not null)
|
||||
{
|
||||
WallpaperPreviewTopStatusBarHost.IsVisible = hasVisibleTopStatusComponent;
|
||||
}
|
||||
|
||||
if (GridPreviewTopStatusBarHost is not null)
|
||||
{
|
||||
GridPreviewTopStatusBarHost.IsVisible = hasVisibleTopStatusComponent;
|
||||
}
|
||||
}
|
||||
|
||||
private TaskbarContext GetCurrentTaskbarContext()
|
||||
|
||||
@@ -71,7 +71,19 @@ public partial class MainWindow
|
||||
private void OnSettingsChanged(object? sender, SettingsChangedEvent e)
|
||||
{
|
||||
_ = sender;
|
||||
_ = e;
|
||||
|
||||
if (e.Scope == SettingsScope.App && e.ChangedKeys is { Count: > 0 })
|
||||
{
|
||||
var changedKeys = e.ChangedKeys.ToArray();
|
||||
if (changedKeys.All(key =>
|
||||
string.Equals(key, nameof(AppSettingsSnapshot.ThemeColorMode), StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(key, nameof(AppSettingsSnapshot.SystemMaterialMode), StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(key, nameof(AppSettingsSnapshot.SelectedWallpaperSeed), StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
ScheduleReloadFromExternalSettings();
|
||||
}
|
||||
|
||||
@@ -198,16 +210,20 @@ public partial class MainWindow
|
||||
|
||||
private ThemeColorContext BuildAdaptiveThemeContext()
|
||||
{
|
||||
var palette = _themeSettingsService.BuildPalette(_isNightMode, _wallpaperPath, _selectedThemeColor.ToString());
|
||||
var accentColor = palette.MonetColors is { Count: > 0 }
|
||||
? palette.MonetColors[0]
|
||||
: _selectedThemeColor;
|
||||
var appearanceSnapshot = _appearanceThemeService.GetCurrent();
|
||||
|
||||
return new ThemeColorContext(
|
||||
accentColor,
|
||||
IsLightBackground: !_isNightMode,
|
||||
IsLightNavBackground: !_isNightMode,
|
||||
IsNightMode: _isNightMode,
|
||||
MonetColors: palette.MonetColors);
|
||||
appearanceSnapshot.AccentColor,
|
||||
IsLightBackground: !appearanceSnapshot.IsNightMode,
|
||||
IsLightNavBackground: !appearanceSnapshot.IsNightMode,
|
||||
IsNightMode: appearanceSnapshot.IsNightMode,
|
||||
MonetPalette: appearanceSnapshot.MonetPalette,
|
||||
MonetColors: appearanceSnapshot.MonetPalette.MonetColors,
|
||||
UseNeutralSurfaces: string.Equals(
|
||||
appearanceSnapshot.ThemeColorMode,
|
||||
ThemeAppearanceValues.ColorModeDefaultNeutral,
|
||||
StringComparison.OrdinalIgnoreCase),
|
||||
SystemMaterialMode: appearanceSnapshot.SystemMaterialMode);
|
||||
}
|
||||
|
||||
private void ApplyAdaptiveThemeResources()
|
||||
@@ -222,7 +238,8 @@ public partial class MainWindow
|
||||
GlassEffectService.ApplyGlassResources(applicationResources, context);
|
||||
}
|
||||
|
||||
_defaultDesktopBackground = GetThemeBrush("AdaptiveSurfaceBaseBrush");
|
||||
_defaultDesktopBackground = GetThemeBrush("AdaptiveWindowBackgroundBrush")
|
||||
?? GetThemeBrush("AdaptiveSurfaceBaseBrush");
|
||||
}
|
||||
|
||||
private void TryRestoreWallpaper(string? savedWallpaperPath, string? type = null, string? color = null)
|
||||
@@ -391,9 +408,9 @@ public partial class MainWindow
|
||||
return;
|
||||
}
|
||||
|
||||
var palette = _themeSettingsService.BuildPalette(enabled, _wallpaperPath, _selectedThemeColor.ToString());
|
||||
_recommendedColors = palette.RecommendedColors;
|
||||
_monetColors = palette.MonetColors;
|
||||
var snapshot = _appearanceThemeService.GetCurrent();
|
||||
_recommendedColors = snapshot.MonetPalette.RecommendedColors;
|
||||
_monetColors = snapshot.MonetPalette.MonetColors;
|
||||
}
|
||||
|
||||
private static double CalculateRelativeLuminance(Color color)
|
||||
@@ -521,13 +538,18 @@ public partial class MainWindow
|
||||
{
|
||||
var latestWeatherState = _weatherSettingsService.Get();
|
||||
var latestUpdateState = _updateSettingsService.Get();
|
||||
var latestThemeState = _themeSettingsService.Get();
|
||||
return new AppSettingsSnapshot
|
||||
{
|
||||
GridShortSideCells = _targetShortSideCells,
|
||||
GridSpacingPreset = _gridSpacingPreset,
|
||||
DesktopEdgeInsetPercent = _desktopEdgeInsetPercent,
|
||||
IsNightMode = _isNightMode,
|
||||
ThemeColor = _selectedThemeColor.ToString(),
|
||||
ThemeColor = latestThemeState.ThemeColor,
|
||||
ThemeColorMode = latestThemeState.ThemeColorMode,
|
||||
SystemMaterialMode = latestThemeState.SystemMaterialMode,
|
||||
SelectedWallpaperSeed = latestThemeState.SelectedWallpaperSeed,
|
||||
UseSystemChrome = latestThemeState.UseSystemChrome,
|
||||
WallpaperPath = _wallpaperPath,
|
||||
WallpaperType = _wallpaperType,
|
||||
WallpaperColor = _wallpaperSolidColor?.ToString(),
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
CanResize="False"
|
||||
UseLayoutRounding="True"
|
||||
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
||||
Background="{DynamicResource AdaptiveSurfaceBaseBrush}"
|
||||
Background="Transparent"
|
||||
Title="LanMountainDesktop">
|
||||
|
||||
<Design.DataContext>
|
||||
|
||||
@@ -78,6 +78,7 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
|
||||
TaskbarActionId.MinimizeToWindows
|
||||
];
|
||||
private readonly ISettingsFacadeService _settingsFacade = HostSettingsFacadeProvider.GetOrCreate();
|
||||
private readonly IAppearanceThemeService _appearanceThemeService = HostAppearanceThemeProvider.GetOrCreate();
|
||||
private readonly IGridSettingsService _gridSettingsService;
|
||||
private readonly IThemeAppearanceService _themeSettingsService;
|
||||
private readonly IWeatherSettingsService _weatherSettingsService;
|
||||
@@ -206,6 +207,7 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
|
||||
_componentEditorWindowService = new ComponentEditorWindowService(_settingsFacade);
|
||||
_fluentAvaloniaTheme = Application.Current?.Styles.OfType<FluentAvaloniaTheme>().FirstOrDefault();
|
||||
_settingsService.Changed += OnSettingsChanged;
|
||||
_appearanceThemeService.Changed += OnAppearanceThemeChanged;
|
||||
PropertyChanged += OnWindowPropertyChanged;
|
||||
InitializeDesktopSurfaceSwipeHandlers();
|
||||
InitializeDesktopComponentDragHandlers();
|
||||
@@ -340,6 +342,7 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
|
||||
_wallpaperBitmap?.Dispose();
|
||||
_wallpaperBitmap = null;
|
||||
_settingsService.Changed -= OnSettingsChanged;
|
||||
_appearanceThemeService.Changed -= OnAppearanceThemeChanged;
|
||||
PropertyChanged -= OnWindowPropertyChanged;
|
||||
DesktopHost.SizeChanged -= OnDesktopHostSizeChanged;
|
||||
if (Application.Current is App app && app.SettingsWindowService is { } settingsWindowService)
|
||||
@@ -349,6 +352,23 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
|
||||
base.OnClosed(e);
|
||||
}
|
||||
|
||||
private void OnAppearanceThemeChanged(object? sender, AppearanceThemeSnapshot snapshot)
|
||||
{
|
||||
_ = sender;
|
||||
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
if (!IsVisible)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ApplyAdaptiveThemeResources();
|
||||
_recommendedColors = snapshot.MonetPalette.RecommendedColors;
|
||||
_monetColors = snapshot.MonetPalette.MonetColors;
|
||||
}, DispatcherPriority.Background);
|
||||
}
|
||||
|
||||
private int CalculateDefaultShortSideCellCountFromDpi()
|
||||
{
|
||||
var dpi = 96d * RenderScaling;
|
||||
|
||||
@@ -31,13 +31,224 @@
|
||||
</ui:SettingsExpander.Footer>
|
||||
</ui:SettingsExpander>
|
||||
|
||||
<ui:SettingsExpander Header="{Binding ThemeColorLabel}">
|
||||
<ui:SettingsExpander Header="{Binding ThemeColorModeLabel}"
|
||||
Description="{Binding ThemeColorSourceDescription}">
|
||||
<ui:SettingsExpander.IconSource>
|
||||
<fi:SymbolIconSource Symbol="PaintBrush" />
|
||||
</ui:SettingsExpander.IconSource>
|
||||
<ui:SettingsExpander.Footer>
|
||||
<ComboBox Width="240"
|
||||
ItemsSource="{Binding ThemeColorModes}"
|
||||
SelectedItem="{Binding SelectedThemeColorMode}">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:SelectionOption">
|
||||
<TextBlock Text="{Binding Label}" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</ui:SettingsExpander.Footer>
|
||||
</ui:SettingsExpander>
|
||||
|
||||
<ui:SettingsExpander Header="{Binding SystemMaterialLabel}"
|
||||
Description="{Binding SystemMaterialDescription}">
|
||||
<ui:SettingsExpander.IconSource>
|
||||
<fi:SymbolIconSource Symbol="WindowDevTools" />
|
||||
</ui:SettingsExpander.IconSource>
|
||||
<ui:SettingsExpander.Footer>
|
||||
<ComboBox Width="220"
|
||||
ItemsSource="{Binding SystemMaterialModes}"
|
||||
SelectedItem="{Binding SelectedSystemMaterialMode}">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:SelectionOption">
|
||||
<TextBlock Text="{Binding Label}" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</ui:SettingsExpander.Footer>
|
||||
</ui:SettingsExpander>
|
||||
|
||||
<ui:SettingsExpander Header="{Binding ThemeColorLabel}"
|
||||
Description="{Binding ThemeColorSourceDescription}">
|
||||
<ui:SettingsExpander.IconSource>
|
||||
<fi:SymbolIconSource Symbol="Color" />
|
||||
</ui:SettingsExpander.IconSource>
|
||||
<ui:SettingsExpander.Footer>
|
||||
<ColorPicker Color="{Binding ThemeColorPickerValue}" />
|
||||
</ui:SettingsExpander.Footer>
|
||||
<ui:SettingsExpanderItem>
|
||||
<StackPanel Spacing="12">
|
||||
<StackPanel Orientation="Horizontal"
|
||||
Spacing="12"
|
||||
IsVisible="{Binding ShowNeutralPreview}">
|
||||
<StackPanel Width="96"
|
||||
Spacing="6">
|
||||
<Border Height="54"
|
||||
Background="{Binding NeutralLightPreviewBrush}"
|
||||
BorderBrush="{DynamicResource AdaptiveGlassPanelBorderBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="14" />
|
||||
<TextBlock Text="{Binding PreviewNeutralLightLabel}"
|
||||
HorizontalAlignment="Center"
|
||||
TextAlignment="Center" />
|
||||
</StackPanel>
|
||||
<StackPanel Width="96"
|
||||
Spacing="6">
|
||||
<Border Height="54"
|
||||
Background="{Binding NeutralDarkPreviewBrush}"
|
||||
BorderBrush="{DynamicResource AdaptiveGlassPanelBorderBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="14" />
|
||||
<TextBlock Text="{Binding PreviewNeutralDarkLabel}"
|
||||
HorizontalAlignment="Center"
|
||||
TextAlignment="Center" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Orientation="Horizontal"
|
||||
Spacing="12"
|
||||
IsVisible="{Binding ShowMonetPreview}">
|
||||
<StackPanel Width="92"
|
||||
Spacing="6">
|
||||
<Border Height="54"
|
||||
Background="{Binding PrimarySwatchBrush}"
|
||||
BorderBrush="{DynamicResource AdaptiveGlassPanelBorderBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="14" />
|
||||
<TextBlock Text="{Binding PreviewPrimaryLabel}"
|
||||
HorizontalAlignment="Center"
|
||||
TextAlignment="Center" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Width="92"
|
||||
Spacing="6">
|
||||
<Border Height="54"
|
||||
Background="{Binding SecondarySwatchBrush}"
|
||||
BorderBrush="{DynamicResource AdaptiveGlassPanelBorderBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="14" />
|
||||
<TextBlock Text="{Binding PreviewSecondaryLabel}"
|
||||
HorizontalAlignment="Center"
|
||||
TextAlignment="Center" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Width="92"
|
||||
Spacing="6">
|
||||
<Border Height="54"
|
||||
Background="{Binding TertiarySwatchBrush}"
|
||||
BorderBrush="{DynamicResource AdaptiveGlassPanelBorderBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="14" />
|
||||
<TextBlock Text="{Binding PreviewTertiaryLabel}"
|
||||
HorizontalAlignment="Center"
|
||||
TextAlignment="Center" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Width="92"
|
||||
Spacing="6">
|
||||
<Border Height="54"
|
||||
Background="{Binding NeutralSwatchBrush}"
|
||||
BorderBrush="{DynamicResource AdaptiveGlassPanelBorderBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="14" />
|
||||
<TextBlock Text="{Binding PreviewNeutralLabel}"
|
||||
HorizontalAlignment="Center"
|
||||
TextAlignment="Center" />
|
||||
</StackPanel>
|
||||
|
||||
<Button x:Name="CustomSeedButton"
|
||||
Width="92"
|
||||
Padding="0"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
HorizontalAlignment="Left"
|
||||
IsVisible="{Binding IsThemeColorEditable}">
|
||||
<Button.Flyout>
|
||||
<Flyout Placement="BottomEdgeAlignedLeft"
|
||||
Closed="OnCustomSeedFlyoutClosed">
|
||||
<StackPanel Width="300"
|
||||
Spacing="12">
|
||||
<ColorPicker Color="{Binding CustomSeedPickerValue}" />
|
||||
<Button Content="{Binding SeedApplyButtonText}"
|
||||
HorizontalAlignment="Right"
|
||||
Click="OnApplyCustomSeedClick" />
|
||||
</StackPanel>
|
||||
</Flyout>
|
||||
</Button.Flyout>
|
||||
<StackPanel Width="92"
|
||||
Spacing="6">
|
||||
<Border Height="54"
|
||||
Background="{Binding SeedSwatchBrush}"
|
||||
BorderBrush="{DynamicResource AdaptiveGlassPanelBorderBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="14" />
|
||||
<TextBlock Text="{Binding PreviewSeedLabel}"
|
||||
HorizontalAlignment="Center"
|
||||
TextAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<Button x:Name="WallpaperSeedButton"
|
||||
Width="92"
|
||||
Padding="0"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
HorizontalAlignment="Left"
|
||||
IsVisible="{Binding IsWallpaperMode}"
|
||||
IsEnabled="{Binding IsWallpaperSeedSelectable}">
|
||||
<Button.Flyout>
|
||||
<Flyout Placement="BottomEdgeAlignedLeft">
|
||||
<StackPanel Width="280"
|
||||
Spacing="12">
|
||||
<TextBlock Text="{Binding WallpaperSeedFlyoutTitle}"
|
||||
FontWeight="SemiBold" />
|
||||
<ItemsControl ItemsSource="{Binding WallpaperSeedCandidates}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<WrapPanel Orientation="Horizontal" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:ThemeSeedCandidateOption">
|
||||
<Button Padding="0"
|
||||
Margin="0,0,8,8"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Click="OnWallpaperSeedCandidateClick">
|
||||
<StackPanel Width="76"
|
||||
Spacing="6">
|
||||
<Border Height="44"
|
||||
Background="{Binding Brush}"
|
||||
BorderBrush="{DynamicResource AdaptiveGlassPanelBorderBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="12" />
|
||||
<TextBlock Text="{Binding Label}"
|
||||
HorizontalAlignment="Center"
|
||||
TextAlignment="Center"
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock Text="{Binding $parent[vm:AppearanceSettingsPageViewModel].WallpaperSeedCurrentText}"
|
||||
HorizontalAlignment="Center"
|
||||
FontSize="10"
|
||||
IsVisible="{Binding IsSelected}" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</Flyout>
|
||||
</Button.Flyout>
|
||||
<StackPanel Width="92"
|
||||
Spacing="6">
|
||||
<Border Height="54"
|
||||
Background="{Binding SeedSwatchBrush}"
|
||||
BorderBrush="{DynamicResource AdaptiveGlassPanelBorderBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="14" />
|
||||
<TextBlock Text="{Binding PreviewSeedLabel}"
|
||||
HorizontalAlignment="Center"
|
||||
TextAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</ui:SettingsExpanderItem>
|
||||
</ui:SettingsExpander>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
using System;
|
||||
using LanMountainDesktop.PluginSdk;
|
||||
using LanMountainDesktop.Services;
|
||||
using LanMountainDesktop.Services.Settings;
|
||||
using LanMountainDesktop.ViewModels;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
|
||||
namespace LanMountainDesktop.Views.SettingsPages;
|
||||
|
||||
@@ -15,16 +19,51 @@ namespace LanMountainDesktop.Views.SettingsPages;
|
||||
public partial class AppearanceSettingsPage : SettingsPageBase
|
||||
{
|
||||
public AppearanceSettingsPage()
|
||||
: this(new AppearanceSettingsPageViewModel(HostSettingsFacadeProvider.GetOrCreate()))
|
||||
: this(new AppearanceSettingsPageViewModel(
|
||||
HostSettingsFacadeProvider.GetOrCreate(),
|
||||
HostAppearanceThemeProvider.GetOrCreate()))
|
||||
{
|
||||
}
|
||||
|
||||
public AppearanceSettingsPage(AppearanceSettingsPageViewModel viewModel)
|
||||
{
|
||||
ViewModel = viewModel;
|
||||
ViewModel.RestartRequested += OnRestartRequested;
|
||||
DataContext = ViewModel;
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public AppearanceSettingsPageViewModel ViewModel { get; }
|
||||
|
||||
private void OnRestartRequested(string reason)
|
||||
{
|
||||
RequestRestart(reason);
|
||||
}
|
||||
|
||||
private void OnApplyCustomSeedClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
_ = sender;
|
||||
_ = e;
|
||||
ViewModel.ApplyCustomSeedCommand.Execute(null);
|
||||
CustomSeedButton?.Flyout?.Hide();
|
||||
}
|
||||
|
||||
private void OnCustomSeedFlyoutClosed(object? sender, EventArgs e)
|
||||
{
|
||||
_ = sender;
|
||||
_ = e;
|
||||
ViewModel.CancelCustomSeedPreview();
|
||||
}
|
||||
|
||||
private void OnWallpaperSeedCandidateClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
_ = e;
|
||||
|
||||
if (sender is Button { DataContext: ThemeSeedCandidateOption option })
|
||||
{
|
||||
ViewModel.SelectWallpaperSeed(option.Value);
|
||||
}
|
||||
|
||||
WallpaperSeedButton?.Flyout?.Hide();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -7,9 +7,9 @@ namespace LanMountainDesktop.Views.SettingsPages;
|
||||
[SettingsPageInfo(
|
||||
"status-bar",
|
||||
"Status Bar",
|
||||
SettingsPageCategory.Appearance,
|
||||
SettingsPageCategory.Components,
|
||||
IconKey = "Apps",
|
||||
SortOrder = 17,
|
||||
SortOrder = 15,
|
||||
TitleLocalizationKey = "settings.status_bar.title",
|
||||
DescriptionLocalizationKey = "settings.status_bar.description")]
|
||||
public partial class StatusBarSettingsPage : SettingsPageBase
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
WindowStartupLocation="Manual"
|
||||
SystemDecorations="BorderOnly"
|
||||
FontFamily="{DynamicResource AppFontFamily}"
|
||||
Background="{DynamicResource AdaptiveSurfaceBaseBrush}"
|
||||
Background="Transparent"
|
||||
Icon="/Assets/avalonia-logo.ico"
|
||||
Title="{Binding Title}">
|
||||
|
||||
@@ -38,13 +38,13 @@
|
||||
</Window.Styles>
|
||||
|
||||
<Grid Classes="settings-scope"
|
||||
Background="{DynamicResource AdaptiveSurfaceBaseBrush}"
|
||||
RowDefinitions="Auto,Auto,*">
|
||||
Background="{DynamicResource AdaptiveSettingsWindowBackgroundBrush}"
|
||||
RowDefinitions="Auto,*">
|
||||
<Border x:Name="WindowTitleBarHost"
|
||||
Height="48"
|
||||
Padding="12,0,12,0"
|
||||
Background="{DynamicResource AdaptiveSurfaceBaseBrush}"
|
||||
BorderBrush="{DynamicResource AdaptiveGlassPanelBorderBrush}"
|
||||
Background="{DynamicResource AdaptiveSettingsWindowBackgroundBrush}"
|
||||
BorderBrush="{DynamicResource AdaptiveSettingsWindowBorderBrush}"
|
||||
BorderThickness="0,0,0,1"
|
||||
PointerPressed="OnWindowTitleBarPointerPressed">
|
||||
<Grid ColumnDefinitions="Auto,Auto,*,Auto,Auto"
|
||||
@@ -113,22 +113,8 @@
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<ui:InfoBar x:Name="RestartInfoBar"
|
||||
Grid.Row="1"
|
||||
IsOpen="{Binding IsRestartRequested}"
|
||||
Margin="16,8,16,0"
|
||||
Severity="Informational"
|
||||
IsClosable="False"
|
||||
Title="{Binding RestartTitle}"
|
||||
Message="{Binding RestartMessage}">
|
||||
<ui:InfoBar.ActionButton>
|
||||
<Button Click="OnRestartNowClick"
|
||||
Content="{Binding RestartButtonText}" />
|
||||
</ui:InfoBar.ActionButton>
|
||||
</ui:InfoBar>
|
||||
|
||||
<ui:NavigationView x:Name="RootNavigationView"
|
||||
Grid.Row="2"
|
||||
Grid.Row="1"
|
||||
Margin="0,8,0,0"
|
||||
Background="Transparent"
|
||||
PaneDisplayMode="Auto"
|
||||
|
||||
@@ -33,6 +33,7 @@ public partial class SettingsWindow : Window, ISettingsPageHostContext
|
||||
private readonly Dictionary<string, Control> _cachedPages = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly bool _useSystemChrome;
|
||||
private bool _isResponsiveRefreshPending;
|
||||
private bool _isRestartPromptVisible;
|
||||
|
||||
public SettingsWindow()
|
||||
: this(
|
||||
@@ -129,6 +130,8 @@ public partial class SettingsWindow : Window, ISettingsPageHostContext
|
||||
? ViewModel.GetDefaultRestartMessage()
|
||||
: reason;
|
||||
ViewModel.IsRestartRequested = true;
|
||||
PendingRestartStateService.SetPending(PendingRestartStateService.SettingsWindowReason, true);
|
||||
ShowRestartPrompt();
|
||||
}
|
||||
|
||||
public void ApplyChromeMode(bool useSystemChrome)
|
||||
@@ -269,14 +272,11 @@ public partial class SettingsWindow : Window, ISettingsPageHostContext
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnRestartNowClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
private void OnRestartNowClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
_ = sender;
|
||||
_ = e;
|
||||
_hostApplicationLifecycle.TryRestart(new HostApplicationLifecycleRequest(
|
||||
Source: "SettingsWindow",
|
||||
Reason: "User accepted restart from settings window."));
|
||||
await Task.CompletedTask;
|
||||
ShowRestartPrompt();
|
||||
}
|
||||
|
||||
private void OnCloseDrawerClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
@@ -306,6 +306,58 @@ public partial class SettingsWindow : Window, ISettingsPageHostContext
|
||||
ViewModel.IsRestartRequested = ViewModel.IsRestartRequested || PendingRestartStateService.HasPendingRestart;
|
||||
}
|
||||
|
||||
private void ShowRestartPrompt()
|
||||
{
|
||||
void ShowPrompt()
|
||||
{
|
||||
UiExceptionGuard.FireAndForgetGuarded(
|
||||
ShowRestartPromptCoreAsync,
|
||||
"SettingsWindow.ShowRestartPrompt");
|
||||
}
|
||||
|
||||
if (Dispatcher.UIThread.CheckAccess())
|
||||
{
|
||||
ShowPrompt();
|
||||
return;
|
||||
}
|
||||
|
||||
Dispatcher.UIThread.Post(ShowPrompt, DispatcherPriority.Send);
|
||||
}
|
||||
|
||||
private async Task ShowRestartPromptCoreAsync()
|
||||
{
|
||||
if (_isRestartPromptVisible)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_isRestartPromptVisible = true;
|
||||
|
||||
try
|
||||
{
|
||||
var dialog = new ContentDialog
|
||||
{
|
||||
Title = ViewModel.RestartDialogTitle,
|
||||
Content = ViewModel.RestartMessage,
|
||||
PrimaryButtonText = ViewModel.RestartDialogPrimaryText,
|
||||
CloseButtonText = ViewModel.RestartDialogCloseText,
|
||||
DefaultButton = ContentDialogButton.Primary
|
||||
};
|
||||
|
||||
var result = await dialog.ShowAsync(this);
|
||||
if (result == ContentDialogResult.Primary)
|
||||
{
|
||||
_hostApplicationLifecycle.TryRestart(new HostApplicationLifecycleRequest(
|
||||
Source: "SettingsWindow",
|
||||
Reason: "User accepted restart from settings window."));
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isRestartPromptVisible = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnOpened(object? sender, EventArgs e)
|
||||
{
|
||||
_ = sender;
|
||||
@@ -642,6 +694,7 @@ public partial class SettingsWindow : Window, ISettingsPageHostContext
|
||||
"GridDots" => Symbol.GridDots,
|
||||
"PuzzlePiece" => Symbol.PuzzlePiece,
|
||||
"ShoppingBag" => Symbol.ShoppingBag,
|
||||
"Shield" => Symbol.ShieldDismiss,
|
||||
"Info" => Symbol.Info,
|
||||
"ArrowSync" => Symbol.ArrowSync,
|
||||
_ => Symbol.Settings
|
||||
|
||||
@@ -327,6 +327,7 @@ public sealed class PluginLoader
|
||||
RegisterHostService<ISettingsFacadeService>(services, hostServices);
|
||||
RegisterHostService<ISettingsService>(services, hostServices);
|
||||
RegisterHostService<ISettingsCatalog>(services, hostServices);
|
||||
RegisterHostService<IAppearanceThemeService>(services, hostServices);
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
@@ -835,10 +835,11 @@ public sealed class PluginRuntimeService : IDisposable
|
||||
{
|
||||
private readonly IPluginPackageManager _packageManager;
|
||||
private readonly IHostApplicationLifecycle _applicationLifecycle;
|
||||
private readonly IPluginExportRegistry _exportRegistry;
|
||||
private readonly ISettingsFacadeService _settingsFacade;
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly ISettingsCatalog _settingsCatalog;
|
||||
private readonly IPluginExportRegistry _exportRegistry;
|
||||
private readonly ISettingsFacadeService _settingsFacade;
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly ISettingsCatalog _settingsCatalog;
|
||||
private readonly IAppearanceThemeService _appearanceThemeService;
|
||||
|
||||
public PluginHostServiceProvider(
|
||||
IPluginPackageManager packageManager,
|
||||
@@ -854,6 +855,7 @@ public sealed class PluginRuntimeService : IDisposable
|
||||
_settingsFacade = settingsFacade;
|
||||
_settingsService = settingsService;
|
||||
_settingsCatalog = settingsCatalog;
|
||||
_appearanceThemeService = HostAppearanceThemeProvider.GetOrCreate();
|
||||
}
|
||||
|
||||
public object? GetService(Type serviceType)
|
||||
@@ -888,6 +890,11 @@ public sealed class PluginRuntimeService : IDisposable
|
||||
return _settingsCatalog;
|
||||
}
|
||||
|
||||
if (serviceType == typeof(IAppearanceThemeService))
|
||||
{
|
||||
return _appearanceThemeService;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user