mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-22 00:54:26 +08:00
0.2.5
课表组件、天气组件全面升级。
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:fi="using:FluentIcons.Avalonia"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d"
|
||||
d:DesignWidth="520"
|
||||
d:DesignHeight="340"
|
||||
x:Class="LanMontainDesktop.Views.Components.ClassScheduleSettingsWindow">
|
||||
<Border Background="{DynamicResource AdaptiveBackgroundBrush}"
|
||||
Padding="16">
|
||||
<Grid RowDefinitions="Auto,Auto,Auto,*"
|
||||
RowSpacing="10">
|
||||
<TextBlock x:Name="TitleTextBlock"
|
||||
Text="课表导入"
|
||||
FontSize="18"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
|
||||
|
||||
<TextBlock x:Name="DescriptionTextBlock"
|
||||
Grid.Row="1"
|
||||
Text="导入 ClassIsland 的 CSES 课表文件并选择启用项。"
|
||||
FontSize="12"
|
||||
TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
|
||||
|
||||
<Button x:Name="AddScheduleButton"
|
||||
Grid.Row="2"
|
||||
HorizontalAlignment="Left"
|
||||
MinWidth="132"
|
||||
Padding="12,8"
|
||||
Click="OnAddScheduleClick">
|
||||
<StackPanel Orientation="Horizontal"
|
||||
Spacing="6"
|
||||
VerticalAlignment="Center">
|
||||
<fi:FluentIcon Icon="Add" />
|
||||
<TextBlock x:Name="AddScheduleButtonTextBlock"
|
||||
Text="添加课表"
|
||||
VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<Grid Grid.Row="3">
|
||||
<ScrollViewer HorizontalScrollBarVisibility="Disabled"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel x:Name="ScheduleItemsPanel"
|
||||
Spacing="8" />
|
||||
</ScrollViewer>
|
||||
|
||||
<TextBlock x:Name="EmptyStateTextBlock"
|
||||
IsVisible="False"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
TextAlignment="Center"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
|
||||
Text="暂无导入课表" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Border>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,345 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Platform.Storage;
|
||||
using LanMontainDesktop.Models;
|
||||
using LanMontainDesktop.Services;
|
||||
|
||||
namespace LanMontainDesktop.Views.Components;
|
||||
|
||||
public partial class ClassScheduleSettingsWindow : UserControl
|
||||
{
|
||||
private readonly AppSettingsService _appSettingsService = new();
|
||||
private readonly LocalizationService _localizationService = new();
|
||||
private readonly List<ImportedClassScheduleSnapshot> _importedSchedules = [];
|
||||
private string _activeScheduleId = string.Empty;
|
||||
private string _languageCode = "zh-CN";
|
||||
|
||||
public event EventHandler? SettingsChanged;
|
||||
|
||||
public ClassScheduleSettingsWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
LoadState();
|
||||
ApplyLocalization();
|
||||
RenderImportedSchedules();
|
||||
}
|
||||
|
||||
private void LoadState()
|
||||
{
|
||||
var snapshot = _appSettingsService.Load();
|
||||
_languageCode = _localizationService.NormalizeLanguageCode(snapshot.LanguageCode);
|
||||
|
||||
_importedSchedules.Clear();
|
||||
foreach (var item in snapshot.ImportedClassSchedules)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(item.Id) ||
|
||||
string.IsNullOrWhiteSpace(item.FilePath))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_importedSchedules.Add(new ImportedClassScheduleSnapshot
|
||||
{
|
||||
Id = item.Id.Trim(),
|
||||
DisplayName = item.DisplayName?.Trim() ?? string.Empty,
|
||||
FilePath = item.FilePath.Trim()
|
||||
});
|
||||
}
|
||||
|
||||
_activeScheduleId = snapshot.ActiveImportedClassScheduleId?.Trim() ?? string.Empty;
|
||||
if (_importedSchedules.Count > 0 &&
|
||||
!_importedSchedules.Any(item => string.Equals(item.Id, _activeScheduleId, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
_activeScheduleId = _importedSchedules[0].Id;
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyLocalization()
|
||||
{
|
||||
TitleTextBlock.Text = L("schedule.settings.title", "课表导入");
|
||||
DescriptionTextBlock.Text = L(
|
||||
"schedule.settings.desc",
|
||||
"导入 ClassIsland 的 CSES 课表文件并选择启用项。");
|
||||
AddScheduleButtonTextBlock.Text = L("schedule.settings.add", "添加课表");
|
||||
EmptyStateTextBlock.Text = L("schedule.settings.empty", "暂无导入课表");
|
||||
}
|
||||
|
||||
private async void OnAddScheduleClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
var topLevel = TopLevel.GetTopLevel(this);
|
||||
var storageProvider = topLevel?.StorageProvider;
|
||||
if (storageProvider is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var files = await storageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
|
||||
{
|
||||
Title = L("schedule.settings.picker_title", "选择 ClassIsland 课表文件"),
|
||||
AllowMultiple = false,
|
||||
FileTypeFilter =
|
||||
[
|
||||
new FilePickerFileType(L("schedule.settings.picker_file_type", "ClassIsland CSES 课表"))
|
||||
{
|
||||
Patterns = ["*.cses", "*.yaml", "*.yml"]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
if (files.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var importedPath = await ImportScheduleFileAsync(files[0]);
|
||||
if (string.IsNullOrWhiteSpace(importedPath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var existing = _importedSchedules.FirstOrDefault(item =>
|
||||
string.Equals(item.FilePath, importedPath, StringComparison.OrdinalIgnoreCase));
|
||||
if (existing is not null)
|
||||
{
|
||||
_activeScheduleId = existing.Id;
|
||||
SaveState();
|
||||
RenderImportedSchedules();
|
||||
return;
|
||||
}
|
||||
|
||||
var displayName = Path.GetFileNameWithoutExtension(importedPath)?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(displayName))
|
||||
{
|
||||
displayName = L("schedule.settings.unnamed", "未命名课表");
|
||||
}
|
||||
|
||||
var imported = new ImportedClassScheduleSnapshot
|
||||
{
|
||||
Id = Guid.NewGuid().ToString("N"),
|
||||
DisplayName = displayName,
|
||||
FilePath = importedPath
|
||||
};
|
||||
|
||||
_importedSchedules.Add(imported);
|
||||
_activeScheduleId = imported.Id;
|
||||
SaveState();
|
||||
RenderImportedSchedules();
|
||||
}
|
||||
|
||||
private async Task<string?> ImportScheduleFileAsync(IStorageFile file)
|
||||
{
|
||||
try
|
||||
{
|
||||
var extension = Path.GetExtension(file.Name);
|
||||
if (string.IsNullOrWhiteSpace(extension))
|
||||
{
|
||||
extension = ".cses";
|
||||
}
|
||||
|
||||
var importedDirectory = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"LanMontainDesktop",
|
||||
"Schedules");
|
||||
Directory.CreateDirectory(importedDirectory);
|
||||
|
||||
var destinationPath = Path.Combine(
|
||||
importedDirectory,
|
||||
$"{DateTime.Now:yyyyMMdd_HHmmss}_{Guid.NewGuid():N}{extension}");
|
||||
|
||||
await using var sourceStream = await file.OpenReadAsync();
|
||||
await using var destinationStream = File.Create(destinationPath);
|
||||
await sourceStream.CopyToAsync(destinationStream);
|
||||
return destinationPath;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void RenderImportedSchedules()
|
||||
{
|
||||
ScheduleItemsPanel.Children.Clear();
|
||||
|
||||
if (_importedSchedules.Count == 0)
|
||||
{
|
||||
EmptyStateTextBlock.IsVisible = true;
|
||||
return;
|
||||
}
|
||||
|
||||
EmptyStateTextBlock.IsVisible = false;
|
||||
foreach (var item in _importedSchedules)
|
||||
{
|
||||
var selector = new RadioButton
|
||||
{
|
||||
GroupName = "class_schedule_imports",
|
||||
IsChecked = string.Equals(item.Id, _activeScheduleId, StringComparison.OrdinalIgnoreCase),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Tag = item.Id
|
||||
};
|
||||
selector.IsCheckedChanged += OnScheduleSelectionChanged;
|
||||
|
||||
var title = new TextBlock
|
||||
{
|
||||
Text = string.IsNullOrWhiteSpace(item.DisplayName)
|
||||
? L("schedule.settings.unnamed", "未命名课表")
|
||||
: item.DisplayName,
|
||||
FontSize = 14,
|
||||
FontWeight = FontWeight.SemiBold,
|
||||
Foreground = ResolveThemeBrush("AdaptiveTextPrimaryBrush", "#FFEFF3FF"),
|
||||
TextTrimming = TextTrimming.CharacterEllipsis
|
||||
};
|
||||
|
||||
var path = new TextBlock
|
||||
{
|
||||
Text = item.FilePath,
|
||||
FontSize = 11,
|
||||
Foreground = ResolveThemeBrush("AdaptiveTextSecondaryBrush", "#FF99A2B5"),
|
||||
TextTrimming = TextTrimming.CharacterEllipsis,
|
||||
TextWrapping = TextWrapping.NoWrap
|
||||
};
|
||||
|
||||
var textStack = new StackPanel
|
||||
{
|
||||
Spacing = 4,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Children = { title, path }
|
||||
};
|
||||
|
||||
var deleteButton = new Button
|
||||
{
|
||||
Content = L("schedule.settings.delete", "删除"),
|
||||
Tag = item.Id,
|
||||
Padding = new Thickness(10, 6),
|
||||
MinWidth = 64,
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
VerticalAlignment = VerticalAlignment.Center
|
||||
};
|
||||
deleteButton.Click += OnDeleteScheduleClick;
|
||||
|
||||
var rowGrid = new Grid
|
||||
{
|
||||
ColumnDefinitions = new ColumnDefinitions("Auto,*,Auto"),
|
||||
ColumnSpacing = 10
|
||||
};
|
||||
rowGrid.Children.Add(selector);
|
||||
rowGrid.Children.Add(textStack);
|
||||
rowGrid.Children.Add(deleteButton);
|
||||
Grid.SetColumn(selector, 0);
|
||||
Grid.SetColumn(textStack, 1);
|
||||
Grid.SetColumn(deleteButton, 2);
|
||||
|
||||
var rowBorder = new Border
|
||||
{
|
||||
Padding = new Thickness(10, 8),
|
||||
CornerRadius = new CornerRadius(12),
|
||||
Background = ResolveThemeBrush("AdaptiveSurfaceRaisedBrush", "#1AFFFFFF"),
|
||||
BorderBrush = ResolveThemeBrush("AdaptiveButtonBorderBrush", "#22000000"),
|
||||
BorderThickness = new Thickness(1),
|
||||
Child = rowGrid
|
||||
};
|
||||
|
||||
ScheduleItemsPanel.Children.Add(rowBorder);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnScheduleSelectionChanged(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is not RadioButton button ||
|
||||
button.IsChecked != true ||
|
||||
button.Tag is not string scheduleId)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.Equals(_activeScheduleId, scheduleId, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_activeScheduleId = scheduleId;
|
||||
SaveState();
|
||||
}
|
||||
|
||||
private void OnDeleteScheduleClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is not Button button || button.Tag is not string scheduleId)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var target = _importedSchedules.FirstOrDefault(item =>
|
||||
string.Equals(item.Id, scheduleId, StringComparison.OrdinalIgnoreCase));
|
||||
if (target is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_importedSchedules.Remove(target);
|
||||
TryDeleteImportedFile(target.FilePath);
|
||||
if (string.Equals(_activeScheduleId, scheduleId, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_activeScheduleId = _importedSchedules.Count > 0 ? _importedSchedules[0].Id : string.Empty;
|
||||
}
|
||||
|
||||
SaveState();
|
||||
RenderImportedSchedules();
|
||||
}
|
||||
|
||||
private void SaveState()
|
||||
{
|
||||
var snapshot = _appSettingsService.Load();
|
||||
snapshot.ImportedClassSchedules = _importedSchedules
|
||||
.Select(item => new ImportedClassScheduleSnapshot
|
||||
{
|
||||
Id = item.Id,
|
||||
DisplayName = item.DisplayName,
|
||||
FilePath = item.FilePath
|
||||
})
|
||||
.ToList();
|
||||
snapshot.ActiveImportedClassScheduleId = _activeScheduleId ?? string.Empty;
|
||||
_appSettingsService.Save(snapshot);
|
||||
SettingsChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private string L(string key, string fallback)
|
||||
{
|
||||
return _localizationService.GetString(_languageCode, key, fallback);
|
||||
}
|
||||
|
||||
private static void TryDeleteImportedFile(string? filePath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(filePath) || !File.Exists(filePath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
File.Delete(filePath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Keep settings operation resilient even when file deletion fails.
|
||||
}
|
||||
}
|
||||
|
||||
private IBrush ResolveThemeBrush(string key, string fallbackHex)
|
||||
{
|
||||
if (this.TryFindResource(key, out var value) && value is IBrush brush)
|
||||
{
|
||||
return brush;
|
||||
}
|
||||
|
||||
return new SolidColorBrush(Color.Parse(fallbackHex));
|
||||
}
|
||||
}
|
||||
59
LanMontainDesktop/Views/Components/ClassScheduleWidget.axaml
Normal file
59
LanMontainDesktop/Views/Components/ClassScheduleWidget.axaml
Normal file
@@ -0,0 +1,59 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="LanMontainDesktop.Views.Components.ClassScheduleWidget">
|
||||
<Border x:Name="RootBorder"
|
||||
ClipToBounds="True"
|
||||
CornerRadius="28"
|
||||
BorderThickness="1">
|
||||
<Grid x:Name="LayoutGrid"
|
||||
RowDefinitions="Auto,*">
|
||||
<Grid x:Name="HeaderGrid"
|
||||
ColumnDefinitions="*,Auto">
|
||||
<StackPanel x:Name="DateGroup"
|
||||
Orientation="Horizontal"
|
||||
VerticalAlignment="Top">
|
||||
<TextBlock x:Name="MonthTextBlock"
|
||||
Text="7"
|
||||
FontWeight="Bold"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock x:Name="SlashTextBlock"
|
||||
Text="/"
|
||||
FontWeight="Bold" />
|
||||
<TextBlock x:Name="DayTextBlock"
|
||||
Text="24"
|
||||
FontWeight="Bold"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel x:Name="MetaStack"
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Top">
|
||||
<TextBlock x:Name="WeekdayTextBlock"
|
||||
Text="周一"
|
||||
TextAlignment="Right"
|
||||
FontWeight="SemiBold" />
|
||||
<TextBlock x:Name="ClassCountTextBlock"
|
||||
Text="0节课"
|
||||
TextAlignment="Right"
|
||||
FontWeight="SemiBold" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.Row="1">
|
||||
<ScrollViewer x:Name="ContentScrollViewer"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
VerticalScrollBarVisibility="Disabled">
|
||||
<StackPanel x:Name="CourseListPanel" />
|
||||
</ScrollViewer>
|
||||
|
||||
<TextBlock x:Name="StatusTextBlock"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
TextAlignment="Center"
|
||||
IsVisible="False"
|
||||
TextWrapping="Wrap" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Border>
|
||||
</UserControl>
|
||||
543
LanMontainDesktop/Views/Components/ClassScheduleWidget.axaml.cs
Normal file
543
LanMontainDesktop/Views/Components/ClassScheduleWidget.axaml.cs
Normal file
@@ -0,0 +1,543 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Styling;
|
||||
using Avalonia.Threading;
|
||||
using LanMontainDesktop.Models;
|
||||
using LanMontainDesktop.Services;
|
||||
|
||||
namespace LanMontainDesktop.Views.Components;
|
||||
|
||||
public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget, ITimeZoneAwareComponentWidget
|
||||
{
|
||||
private sealed record CourseItemViewModel(
|
||||
string Name,
|
||||
string TimeRange,
|
||||
string Detail,
|
||||
bool IsCurrent);
|
||||
|
||||
private readonly DispatcherTimer _refreshTimer = new()
|
||||
{
|
||||
Interval = TimeSpan.FromMinutes(4)
|
||||
};
|
||||
|
||||
private readonly AppSettingsService _appSettingsService = new();
|
||||
private readonly LocalizationService _localizationService = new();
|
||||
private readonly IClassIslandScheduleDataService _scheduleService = new ClassIslandScheduleDataService();
|
||||
|
||||
private TimeZoneService? _timeZoneService;
|
||||
private double _currentCellSize = 48;
|
||||
private IReadOnlyList<CourseItemViewModel> _courseItems = Array.Empty<CourseItemViewModel>();
|
||||
private bool _isNightVisual = true;
|
||||
private string _languageCode = "zh-CN";
|
||||
|
||||
public ClassScheduleWidget()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
_refreshTimer.Tick += OnRefreshTimerTick;
|
||||
AttachedToVisualTree += OnAttachedToVisualTree;
|
||||
DetachedFromVisualTree += OnDetachedFromVisualTree;
|
||||
SizeChanged += OnSizeChanged;
|
||||
ActualThemeVariantChanged += OnActualThemeVariantChanged;
|
||||
|
||||
ApplyCellSize(_currentCellSize);
|
||||
RefreshSchedule();
|
||||
}
|
||||
|
||||
public void ApplyCellSize(double cellSize)
|
||||
{
|
||||
_currentCellSize = Math.Max(1, cellSize);
|
||||
ApplyAdaptiveLayout();
|
||||
RenderScheduleItems();
|
||||
}
|
||||
|
||||
public void SetTimeZoneService(TimeZoneService timeZoneService)
|
||||
{
|
||||
ClearTimeZoneService();
|
||||
_timeZoneService = timeZoneService;
|
||||
_timeZoneService.TimeZoneChanged += OnTimeZoneChanged;
|
||||
RefreshSchedule();
|
||||
}
|
||||
|
||||
public void ClearTimeZoneService()
|
||||
{
|
||||
if (_timeZoneService is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_timeZoneService.TimeZoneChanged -= OnTimeZoneChanged;
|
||||
_timeZoneService = null;
|
||||
}
|
||||
|
||||
private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
_refreshTimer.Start();
|
||||
RefreshSchedule();
|
||||
}
|
||||
|
||||
private void OnDetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
_refreshTimer.Stop();
|
||||
}
|
||||
|
||||
private void OnSizeChanged(object? sender, SizeChangedEventArgs e)
|
||||
{
|
||||
ApplyCellSize(_currentCellSize);
|
||||
}
|
||||
|
||||
private void OnActualThemeVariantChanged(object? sender, EventArgs e)
|
||||
{
|
||||
ApplyAdaptiveLayout();
|
||||
RenderScheduleItems();
|
||||
}
|
||||
|
||||
private void OnTimeZoneChanged(object? sender, EventArgs e)
|
||||
{
|
||||
RefreshSchedule();
|
||||
}
|
||||
|
||||
private void OnRefreshTimerTick(object? sender, EventArgs e)
|
||||
{
|
||||
RefreshSchedule();
|
||||
}
|
||||
|
||||
public void RefreshFromSettings()
|
||||
{
|
||||
RefreshSchedule();
|
||||
}
|
||||
|
||||
private void RefreshSchedule()
|
||||
{
|
||||
var appSettings = _appSettingsService.Load();
|
||||
_languageCode = _localizationService.NormalizeLanguageCode(appSettings.LanguageCode);
|
||||
var now = _timeZoneService?.GetCurrentTime() ?? DateTime.Now;
|
||||
UpdateHeader(now);
|
||||
|
||||
var importedSchedulePath = ResolveImportedSchedulePath(appSettings);
|
||||
var readResult = _scheduleService.Load(importedSchedulePath);
|
||||
if (!readResult.Success || readResult.Snapshot is null)
|
||||
{
|
||||
_courseItems = Array.Empty<CourseItemViewModel>();
|
||||
ShowStatus(L("schedule.widget.no_source", "未读取到 ClassIsland 课表"));
|
||||
RenderScheduleItems();
|
||||
return;
|
||||
}
|
||||
|
||||
var snapshot = readResult.Snapshot;
|
||||
var today = DateOnly.FromDateTime(now);
|
||||
if (!_scheduleService.TryResolveClassPlanForDate(snapshot, today, out var resolvedClassPlan))
|
||||
{
|
||||
_courseItems = Array.Empty<CourseItemViewModel>();
|
||||
ShowStatus(L("schedule.widget.no_class_today", "今天没有课程"));
|
||||
RenderScheduleItems();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!snapshot.TimeLayouts.TryGetValue(resolvedClassPlan.ClassPlan.TimeLayoutId, out var layout))
|
||||
{
|
||||
_courseItems = Array.Empty<CourseItemViewModel>();
|
||||
ShowStatus(L("schedule.widget.layout_missing", "课表时间布局缺失"));
|
||||
RenderScheduleItems();
|
||||
return;
|
||||
}
|
||||
|
||||
_courseItems = BuildCourseItemViewModels(snapshot, resolvedClassPlan.ClassPlan, layout, now);
|
||||
if (_courseItems.Count == 0)
|
||||
{
|
||||
ShowStatus(L("schedule.widget.no_class_today", "今天没有课程"));
|
||||
}
|
||||
else
|
||||
{
|
||||
HideStatus();
|
||||
}
|
||||
|
||||
RenderScheduleItems();
|
||||
}
|
||||
|
||||
private IReadOnlyList<CourseItemViewModel> BuildCourseItemViewModels(
|
||||
ClassIslandScheduleSnapshot snapshot,
|
||||
ClassIslandClassPlan classPlan,
|
||||
ClassIslandTimeLayout layout,
|
||||
DateTime now)
|
||||
{
|
||||
var teachingSlots = layout.Items
|
||||
.Where(static item => item.TimeType == 0)
|
||||
.ToList();
|
||||
if (teachingSlots.Count == 0 || classPlan.Classes.Count == 0)
|
||||
{
|
||||
return Array.Empty<CourseItemViewModel>();
|
||||
}
|
||||
|
||||
var result = new List<CourseItemViewModel>(teachingSlots.Count);
|
||||
var max = Math.Min(teachingSlots.Count, classPlan.Classes.Count);
|
||||
for (var i = 0; i < max; i++)
|
||||
{
|
||||
var classInfo = classPlan.Classes[i];
|
||||
if (!classInfo.IsEnabled)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var slot = teachingSlots[i];
|
||||
var subjectName = ResolveSubjectName(snapshot, classInfo.SubjectId);
|
||||
var detail = ResolveSubjectDetail(snapshot, classInfo.SubjectId);
|
||||
var isCurrent = now.TimeOfDay >= slot.StartTime && now.TimeOfDay <= slot.EndTime;
|
||||
result.Add(new CourseItemViewModel(
|
||||
Name: subjectName,
|
||||
TimeRange: $"{FormatTime(slot.StartTime)}-{FormatTime(slot.EndTime)}",
|
||||
Detail: detail,
|
||||
IsCurrent: isCurrent));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private string ResolveSubjectName(ClassIslandScheduleSnapshot snapshot, Guid? subjectId)
|
||||
{
|
||||
if (subjectId.HasValue &&
|
||||
snapshot.Subjects.TryGetValue(subjectId.Value, out var subject) &&
|
||||
!string.IsNullOrWhiteSpace(subject.Name))
|
||||
{
|
||||
return subject.Name.Trim();
|
||||
}
|
||||
|
||||
return L("schedule.widget.subject_fallback", "未命名课程");
|
||||
}
|
||||
|
||||
private string ResolveSubjectDetail(ClassIslandScheduleSnapshot snapshot, Guid? subjectId)
|
||||
{
|
||||
if (subjectId.HasValue &&
|
||||
snapshot.Subjects.TryGetValue(subjectId.Value, out var subject))
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(subject.TeacherName))
|
||||
{
|
||||
return subject.TeacherName.Trim();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(subject.Initial))
|
||||
{
|
||||
return subject.Initial.Trim();
|
||||
}
|
||||
}
|
||||
|
||||
return L("schedule.widget.detail_fallback", "未设置详情");
|
||||
}
|
||||
|
||||
private void UpdateHeader(DateTime now)
|
||||
{
|
||||
var month = now.Month.ToString(CultureInfo.InvariantCulture);
|
||||
var day = now.Day.ToString(CultureInfo.InvariantCulture);
|
||||
MonthTextBlock.Text = month;
|
||||
DayTextBlock.Text = day;
|
||||
WeekdayTextBlock.Text = FormatWeekday(now.DayOfWeek);
|
||||
ClassCountTextBlock.Text = FormatClassCount(_courseItems.Count);
|
||||
}
|
||||
|
||||
private string FormatClassCount(int count)
|
||||
{
|
||||
if (string.Equals(_languageCode, "zh-CN", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return string.Create(CultureInfo.InvariantCulture, $"{Math.Max(0, count)}节课");
|
||||
}
|
||||
|
||||
if (count == 1)
|
||||
{
|
||||
return "1 class";
|
||||
}
|
||||
|
||||
return string.Create(CultureInfo.InvariantCulture, $"{Math.Max(0, count)} classes");
|
||||
}
|
||||
|
||||
private string FormatWeekday(DayOfWeek dayOfWeek)
|
||||
{
|
||||
if (string.Equals(_languageCode, "zh-CN", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return dayOfWeek switch
|
||||
{
|
||||
DayOfWeek.Monday => "周一",
|
||||
DayOfWeek.Tuesday => "周二",
|
||||
DayOfWeek.Wednesday => "周三",
|
||||
DayOfWeek.Thursday => "周四",
|
||||
DayOfWeek.Friday => "周五",
|
||||
DayOfWeek.Saturday => "周六",
|
||||
_ => "周日"
|
||||
};
|
||||
}
|
||||
|
||||
return dayOfWeek.ToString()[..3];
|
||||
}
|
||||
|
||||
private static string? ResolveImportedSchedulePath(AppSettingsSnapshot snapshot)
|
||||
{
|
||||
if (snapshot.ImportedClassSchedules.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var activeId = snapshot.ActiveImportedClassScheduleId?.Trim() ?? string.Empty;
|
||||
ImportedClassScheduleSnapshot? selected = null;
|
||||
if (!string.IsNullOrWhiteSpace(activeId))
|
||||
{
|
||||
selected = snapshot.ImportedClassSchedules
|
||||
.FirstOrDefault(item => string.Equals(item.Id, activeId, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
selected ??= snapshot.ImportedClassSchedules[0];
|
||||
return selected.FilePath;
|
||||
}
|
||||
|
||||
private string L(string key, string fallback)
|
||||
{
|
||||
return _localizationService.GetString(_languageCode, key, fallback);
|
||||
}
|
||||
|
||||
private void ShowStatus(string text)
|
||||
{
|
||||
StatusTextBlock.Text = text;
|
||||
StatusTextBlock.IsVisible = true;
|
||||
}
|
||||
|
||||
private void HideStatus()
|
||||
{
|
||||
StatusTextBlock.Text = string.Empty;
|
||||
StatusTextBlock.IsVisible = false;
|
||||
}
|
||||
|
||||
private void RenderScheduleItems()
|
||||
{
|
||||
CourseListPanel.Children.Clear();
|
||||
ClassCountTextBlock.Text = FormatClassCount(_courseItems.Count);
|
||||
if (_courseItems.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var scale = ResolveScale();
|
||||
var bulletSize = Math.Clamp(10 * scale, 5, 12);
|
||||
var courseNameSize = Math.Clamp(42 * scale, 14, 42);
|
||||
var secondarySize = Math.Clamp(29 * scale, 10, 28);
|
||||
var lineSpacing = Math.Clamp(4 * scale, 1.5, 8);
|
||||
var itemPadding = new Thickness(
|
||||
Math.Clamp(6 * scale, 3, 10),
|
||||
Math.Clamp(4 * scale, 2, 8),
|
||||
Math.Clamp(4 * scale, 2, 8),
|
||||
Math.Clamp(4 * scale, 2, 8));
|
||||
var maxVisibleItems = ResolveMaxVisibleItems(scale);
|
||||
|
||||
var primaryBrush = CreateBrush(_isNightVisual ? "#F9FBFF" : "#151821");
|
||||
var secondaryBrush = CreateBrush(_isNightVisual ? "#848B99" : "#667084");
|
||||
var currentBrush = CreateBrush("#FF4D5A");
|
||||
var normalBulletBrush = CreateBrush(_isNightVisual ? "#B8BEC9" : "#9AA3B2");
|
||||
|
||||
var visibleItems = _courseItems.Take(maxVisibleItems).ToList();
|
||||
for (var i = 0; i < visibleItems.Count; i++)
|
||||
{
|
||||
var item = visibleItems[i];
|
||||
var bulletBrush = item.IsCurrent ? currentBrush : normalBulletBrush;
|
||||
|
||||
var bullet = new Border
|
||||
{
|
||||
Width = bulletSize,
|
||||
Height = bulletSize,
|
||||
CornerRadius = new CornerRadius(bulletSize * 0.5),
|
||||
Background = bulletBrush,
|
||||
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Top,
|
||||
Margin = new Thickness(0, Math.Clamp(8 * scale, 2, 12), 0, 0)
|
||||
};
|
||||
|
||||
var titleText = new TextBlock
|
||||
{
|
||||
Text = item.Name,
|
||||
FontSize = courseNameSize,
|
||||
FontWeight = ToVariableWeight(Lerp(620, 780, Math.Clamp((scale - 0.60) / 1.2, 0, 1))),
|
||||
Foreground = primaryBrush,
|
||||
TextTrimming = TextTrimming.CharacterEllipsis,
|
||||
TextWrapping = TextWrapping.NoWrap
|
||||
};
|
||||
|
||||
var timeText = new TextBlock
|
||||
{
|
||||
Text = item.TimeRange,
|
||||
FontSize = secondarySize,
|
||||
FontWeight = ToVariableWeight(Lerp(520, 680, Math.Clamp((scale - 0.60) / 1.2, 0, 1))),
|
||||
Foreground = secondaryBrush,
|
||||
TextTrimming = TextTrimming.CharacterEllipsis,
|
||||
TextWrapping = TextWrapping.NoWrap
|
||||
};
|
||||
|
||||
var detailText = new TextBlock
|
||||
{
|
||||
Text = item.Detail,
|
||||
FontSize = secondarySize,
|
||||
FontWeight = ToVariableWeight(Lerp(500, 640, Math.Clamp((scale - 0.60) / 1.2, 0, 1))),
|
||||
Foreground = secondaryBrush,
|
||||
TextTrimming = TextTrimming.CharacterEllipsis,
|
||||
TextWrapping = TextWrapping.NoWrap
|
||||
};
|
||||
|
||||
var textStack = new StackPanel
|
||||
{
|
||||
Spacing = lineSpacing,
|
||||
Children = { titleText, timeText, detailText }
|
||||
};
|
||||
|
||||
var itemGrid = new Grid
|
||||
{
|
||||
ColumnDefinitions = new ColumnDefinitions("Auto,*"),
|
||||
ColumnSpacing = Math.Clamp(10 * scale, 4, 14)
|
||||
};
|
||||
itemGrid.Children.Add(bullet);
|
||||
itemGrid.Children.Add(textStack);
|
||||
Grid.SetColumn(textStack, 1);
|
||||
|
||||
var itemBorder = new Border
|
||||
{
|
||||
Padding = itemPadding,
|
||||
Background = Brushes.Transparent,
|
||||
Child = itemGrid
|
||||
};
|
||||
|
||||
CourseListPanel.Children.Add(itemBorder);
|
||||
}
|
||||
}
|
||||
|
||||
private int ResolveMaxVisibleItems(double scale)
|
||||
{
|
||||
var height = Bounds.Height > 1 ? Bounds.Height : _currentCellSize * 4;
|
||||
var rootVerticalPadding = RootBorder.Padding.Top + RootBorder.Padding.Bottom;
|
||||
var headerEstimatedHeight = Math.Clamp(100 * scale, 54, 140);
|
||||
var itemEstimatedHeight = Math.Clamp(136 * scale, 72, 178);
|
||||
var available = Math.Max(1, height - rootVerticalPadding - headerEstimatedHeight);
|
||||
var count = (int)Math.Floor(available / Math.Max(1, itemEstimatedHeight));
|
||||
return Math.Clamp(count, 1, 6);
|
||||
}
|
||||
|
||||
private void ApplyAdaptiveLayout()
|
||||
{
|
||||
var scale = ResolveScale();
|
||||
_isNightVisual = ResolveNightMode();
|
||||
|
||||
var cornerRadius = Math.Clamp(_currentCellSize * 0.45, 24, 44);
|
||||
RootBorder.CornerRadius = new CornerRadius(cornerRadius);
|
||||
RootBorder.Background = _isNightVisual
|
||||
? CreateGradientBrush("#171A21", "#0C0E14")
|
||||
: CreateGradientBrush("#F7F8FC", "#ECEFF6");
|
||||
RootBorder.BorderBrush = CreateBrush(_isNightVisual ? "#24FFFFFF" : "#15000000");
|
||||
|
||||
var rootPadding = new Thickness(
|
||||
Math.Clamp(16 * scale, 10, 24),
|
||||
Math.Clamp(14 * scale, 9, 20),
|
||||
Math.Clamp(16 * scale, 10, 24),
|
||||
Math.Clamp(14 * scale, 8, 20));
|
||||
RootBorder.Padding = rootPadding;
|
||||
|
||||
LayoutGrid.RowSpacing = Math.Clamp(14 * scale, 6, 20);
|
||||
HeaderGrid.ColumnSpacing = Math.Clamp(10 * scale, 4, 16);
|
||||
DateGroup.Spacing = Math.Clamp(1.5 * scale, 0.5, 3);
|
||||
MetaStack.Spacing = Math.Clamp(6 * scale, 3, 10);
|
||||
CourseListPanel.Spacing = Math.Clamp(6 * scale, 3, 10);
|
||||
|
||||
var dateFont = Math.Clamp(66 * scale, 26, 82);
|
||||
MonthTextBlock.FontSize = dateFont;
|
||||
DayTextBlock.FontSize = dateFont;
|
||||
SlashTextBlock.FontSize = dateFont;
|
||||
|
||||
MonthTextBlock.Foreground = CreateBrush(_isNightVisual ? "#F8FAFF" : "#131722");
|
||||
DayTextBlock.Foreground = CreateBrush(_isNightVisual ? "#F8FAFF" : "#131722");
|
||||
SlashTextBlock.Foreground = CreateBrush("#FF3250");
|
||||
WeekdayTextBlock.Foreground = CreateBrush(_isNightVisual ? "#C6CBD5" : "#4B5463");
|
||||
ClassCountTextBlock.Foreground = CreateBrush(_isNightVisual ? "#8D95A4" : "#738095");
|
||||
StatusTextBlock.Foreground = CreateBrush(_isNightVisual ? "#9AA2B1" : "#4B5565");
|
||||
|
||||
WeekdayTextBlock.FontSize = Math.Clamp(34 * scale, 13, 32);
|
||||
ClassCountTextBlock.FontSize = Math.Clamp(40 * scale, 14, 36);
|
||||
StatusTextBlock.FontSize = Math.Clamp(30 * scale, 12, 30);
|
||||
|
||||
WeekdayTextBlock.FontWeight = ToVariableWeight(Lerp(560, 700, Math.Clamp((scale - 0.60) / 1.2, 0, 1)));
|
||||
ClassCountTextBlock.FontWeight = ToVariableWeight(Lerp(560, 680, Math.Clamp((scale - 0.60) / 1.2, 0, 1)));
|
||||
}
|
||||
|
||||
private static string FormatTime(TimeSpan time)
|
||||
{
|
||||
return string.Create(CultureInfo.InvariantCulture, $"{time.Hours}:{time.Minutes:00}");
|
||||
}
|
||||
|
||||
private double ResolveScale()
|
||||
{
|
||||
var cellScale = Math.Clamp(_currentCellSize / 44d, 0.58, 2.2);
|
||||
var widthScale = Bounds.Width > 1 ? Math.Clamp(Bounds.Width / 230d, 0.52, 2.4) : 1;
|
||||
var heightScale = Bounds.Height > 1 ? Math.Clamp(Bounds.Height / 440d, 0.52, 2.4) : 1;
|
||||
return Math.Clamp(Math.Min(cellScale, Math.Min(widthScale, heightScale) * 1.04), 0.52, 2.2);
|
||||
}
|
||||
|
||||
private bool ResolveNightMode()
|
||||
{
|
||||
if (ActualThemeVariant == ThemeVariant.Dark)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ActualThemeVariant == ThemeVariant.Light)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.TryFindResource("AdaptiveSurfaceBaseBrush", out var value) &&
|
||||
value is ISolidColorBrush brush)
|
||||
{
|
||||
return CalculateRelativeLuminance(brush.Color) < 0.45;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static double CalculateRelativeLuminance(Color color)
|
||||
{
|
||||
static double ToLinear(double channel)
|
||||
{
|
||||
return channel <= 0.03928
|
||||
? channel / 12.92
|
||||
: Math.Pow((channel + 0.055) / 1.055, 2.4);
|
||||
}
|
||||
|
||||
var r = ToLinear(color.R / 255d);
|
||||
var g = ToLinear(color.G / 255d);
|
||||
var b = ToLinear(color.B / 255d);
|
||||
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
||||
}
|
||||
|
||||
private static FontWeight ToVariableWeight(double value)
|
||||
{
|
||||
return (FontWeight)(int)Math.Clamp(Math.Round(value), 1, 1000);
|
||||
}
|
||||
|
||||
private static double Lerp(double from, double to, double t)
|
||||
{
|
||||
return from + ((to - from) * t);
|
||||
}
|
||||
|
||||
private static IBrush CreateBrush(string colorHex)
|
||||
{
|
||||
return new SolidColorBrush(Color.Parse(colorHex));
|
||||
}
|
||||
|
||||
private static IBrush CreateGradientBrush(string fromHex, string toHex)
|
||||
{
|
||||
return new LinearGradientBrush
|
||||
{
|
||||
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
|
||||
EndPoint = new RelativePoint(1, 1, RelativeUnit.Relative),
|
||||
GradientStops = new GradientStops
|
||||
{
|
||||
new GradientStop(Color.Parse(fromHex), 0),
|
||||
new GradientStop(Color.Parse(toHex), 1)
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -145,6 +145,16 @@ public sealed class DesktopComponentRuntimeRegistry
|
||||
"component.multiday_weather",
|
||||
() => new MultiDayWeatherWidget(),
|
||||
cellSize => Math.Clamp(cellSize * 0.45, 24, 44)),
|
||||
new DesktopComponentRuntimeRegistration(
|
||||
BuiltInComponentIds.DesktopExtendedWeather,
|
||||
"component.extended_weather",
|
||||
() => new ExtendedWeatherWidget(),
|
||||
cellSize => Math.Clamp(cellSize * 0.45, 24, 44)),
|
||||
new DesktopComponentRuntimeRegistration(
|
||||
BuiltInComponentIds.DesktopClassSchedule,
|
||||
"component.class_schedule",
|
||||
() => new ClassScheduleWidget(),
|
||||
cellSize => Math.Clamp(cellSize * 0.45, 24, 44)),
|
||||
new DesktopComponentRuntimeRegistration(
|
||||
BuiltInComponentIds.DesktopWhiteboard,
|
||||
"component.whiteboard",
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:local="using:LanMontainDesktop.Views.Components"
|
||||
mc:Ignorable="d"
|
||||
d:DesignWidth="640"
|
||||
d:DesignHeight="640"
|
||||
x:Class="LanMontainDesktop.Views.Components.ExtendedWeatherWidget">
|
||||
|
||||
<Grid x:Name="ContainerGrid"
|
||||
RowDefinitions="*,*"
|
||||
RowSpacing="8">
|
||||
<local:HourlyWeatherWidget x:Name="HourlyHost"
|
||||
Grid.Row="0" />
|
||||
<local:MultiDayWeatherWidget x:Name="MultiDayHost"
|
||||
Grid.Row="1" />
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,48 @@
|
||||
using System;
|
||||
using Avalonia.Controls;
|
||||
using LanMontainDesktop.Services;
|
||||
|
||||
namespace LanMontainDesktop.Views.Components;
|
||||
|
||||
public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidget, ITimeZoneAwareComponentWidget, IWeatherInfoAwareComponentWidget
|
||||
{
|
||||
private TimeZoneService? _timeZoneService;
|
||||
private IWeatherInfoService? _weatherInfoService;
|
||||
private double _currentCellSize = 48;
|
||||
|
||||
public ExtendedWeatherWidget()
|
||||
{
|
||||
InitializeComponent();
|
||||
ApplyCellSize(_currentCellSize);
|
||||
}
|
||||
|
||||
public void ApplyCellSize(double cellSize)
|
||||
{
|
||||
_currentCellSize = Math.Max(1, cellSize);
|
||||
var metrics = HyperOS3WeatherTheme.ResolveMetrics(HyperOS3WeatherWidgetKind.Extended4x4);
|
||||
ContainerGrid.RowSpacing = Math.Clamp(_currentCellSize * metrics.SectionGap * 0.22, 6, 18);
|
||||
HourlyHost.ApplyCellSize(_currentCellSize);
|
||||
MultiDayHost.ApplyCellSize(_currentCellSize);
|
||||
}
|
||||
|
||||
public void SetTimeZoneService(TimeZoneService timeZoneService)
|
||||
{
|
||||
_timeZoneService = timeZoneService;
|
||||
HourlyHost.SetTimeZoneService(timeZoneService);
|
||||
MultiDayHost.SetTimeZoneService(timeZoneService);
|
||||
}
|
||||
|
||||
public void ClearTimeZoneService()
|
||||
{
|
||||
HourlyHost.ClearTimeZoneService();
|
||||
MultiDayHost.ClearTimeZoneService();
|
||||
_timeZoneService = null;
|
||||
}
|
||||
|
||||
public void SetWeatherInfoService(IWeatherInfoService weatherInfoService)
|
||||
{
|
||||
_weatherInfoService = weatherInfoService;
|
||||
HourlyHost.SetWeatherInfoService(weatherInfoService);
|
||||
MultiDayHost.SetWeatherInfoService(weatherInfoService);
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,7 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
|
||||
string Tint,
|
||||
string PrimaryText,
|
||||
string SecondaryText,
|
||||
string TertiaryText,
|
||||
string ParticleColor);
|
||||
|
||||
private readonly record struct WeatherMotionProfile(
|
||||
@@ -81,20 +82,6 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
|
||||
Symbol Icon,
|
||||
string TemperatureText);
|
||||
|
||||
private static readonly IReadOnlyDictionary<WeatherVisualKind, string> WeatherBackgroundAssets =
|
||||
new Dictionary<WeatherVisualKind, string>
|
||||
{
|
||||
[WeatherVisualKind.ClearDay] = "avares://LanMontainDesktop/Assets/Weather/clear_day.jpg",
|
||||
[WeatherVisualKind.ClearNight] = "avares://LanMontainDesktop/Assets/Weather/clear_night.jpg",
|
||||
[WeatherVisualKind.CloudyDay] = "avares://LanMontainDesktop/Assets/Weather/cloudy_day.jpg",
|
||||
[WeatherVisualKind.CloudyNight] = "avares://LanMontainDesktop/Assets/Weather/cloudy_night.jpg",
|
||||
[WeatherVisualKind.RainLight] = "avares://LanMontainDesktop/Assets/Weather/rain_light.jpg",
|
||||
[WeatherVisualKind.RainHeavy] = "avares://LanMontainDesktop/Assets/Weather/rain_heavy.jpg",
|
||||
[WeatherVisualKind.Storm] = "avares://LanMontainDesktop/Assets/Weather/storm_dark.jpg",
|
||||
[WeatherVisualKind.Snow] = "avares://LanMontainDesktop/Assets/Weather/snow_soft.jpg",
|
||||
[WeatherVisualKind.Fog] = "avares://LanMontainDesktop/Assets/Weather/fog_haze.jpg"
|
||||
};
|
||||
|
||||
private static readonly IWeatherInfoService DefaultWeatherInfoService = new XiaomiWeatherService();
|
||||
|
||||
private readonly DispatcherTimer _refreshTimer = new()
|
||||
@@ -110,6 +97,7 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
|
||||
private readonly AppSettingsService _settingsService = new();
|
||||
private readonly LocalizationService _localizationService = new();
|
||||
private readonly Dictionary<WeatherVisualKind, IBrush> _backgroundBrushCache = new();
|
||||
private readonly Dictionary<HyperOS3WeatherVisualKind, IBrush> _particleBrushCache = new();
|
||||
private readonly List<Border> _particleVisuals = new();
|
||||
private readonly List<ParticleState> _particleStates = new();
|
||||
private readonly Random _particleRandom = new();
|
||||
@@ -223,10 +211,11 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
|
||||
public void ApplyCellSize(double cellSize)
|
||||
{
|
||||
_currentCellSize = Math.Max(1, cellSize);
|
||||
var metrics = HyperOS3WeatherTheme.ResolveMetrics(HyperOS3WeatherWidgetKind.Hourly4x2);
|
||||
var scale = ResolveScale();
|
||||
var hostWidth = Bounds.Width > 1 ? Bounds.Width : Math.Max(140, _currentCellSize * 4);
|
||||
var hostHeight = Bounds.Height > 1 ? Bounds.Height : Math.Max(78, _currentCellSize * 2);
|
||||
var cornerRadius = Math.Clamp(_currentCellSize * 0.45, 24, 44);
|
||||
var cornerRadius = Math.Clamp(_currentCellSize * metrics.CornerRadiusScale, 24, 44);
|
||||
|
||||
RootBorder.CornerRadius = new CornerRadius(cornerRadius);
|
||||
BackgroundImageLayer.CornerRadius = new CornerRadius(cornerRadius);
|
||||
@@ -235,8 +224,8 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
|
||||
BackgroundLightLayer.CornerRadius = new CornerRadius(cornerRadius);
|
||||
BackgroundShadeLayer.CornerRadius = new CornerRadius(cornerRadius);
|
||||
ContentPaddingBorder.Padding = new Thickness(
|
||||
Math.Clamp(Math.Min(20 * scale, hostWidth * 0.028), 3, 18),
|
||||
Math.Clamp(Math.Min(14 * scale, hostHeight * 0.060), 2, 14));
|
||||
Math.Clamp(Math.Min((_currentCellSize * metrics.HorizontalPaddingScale) * scale, hostWidth * 0.028), 3, 18),
|
||||
Math.Clamp(Math.Min((_currentCellSize * metrics.VerticalPaddingScale) * scale, hostHeight * 0.060), 2, 14));
|
||||
ApplyAdaptiveTypography();
|
||||
ResetParticles();
|
||||
}
|
||||
@@ -319,26 +308,10 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
|
||||
|
||||
private bool ResolveIsNight(WeatherSnapshot snapshot)
|
||||
{
|
||||
if (snapshot.ObservationTime.HasValue)
|
||||
{
|
||||
var observed = snapshot.ObservationTime.Value;
|
||||
try
|
||||
{
|
||||
if (_timeZoneService is not null)
|
||||
{
|
||||
var zoned = TimeZoneInfo.ConvertTime(observed, _timeZoneService.CurrentTimeZone);
|
||||
return zoned.Hour < 6 || zoned.Hour >= 18;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// fall through to local clock
|
||||
}
|
||||
|
||||
return observed.Hour < 6 || observed.Hour >= 18;
|
||||
}
|
||||
|
||||
return IsNightNow();
|
||||
return HyperOS3WeatherTheme.ResolveIsNightPreferred(
|
||||
snapshot,
|
||||
_timeZoneService?.CurrentTimeZone,
|
||||
_timeZoneService?.GetCurrentTime() ?? DateTime.Now);
|
||||
}
|
||||
|
||||
private bool IsNightNow()
|
||||
@@ -557,11 +530,11 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
|
||||
BackgroundTintLayer.Background = CreateSolidBrush(palette.Tint);
|
||||
|
||||
var primary = CreateSolidBrush(palette.PrimaryText);
|
||||
var particleBrush = CreateSolidBrush(palette.ParticleColor);
|
||||
var particleBrush = ResolveParticleBrush(ToThemeKind(kind), palette.ParticleColor);
|
||||
var isNightVisual = kind is WeatherVisualKind.ClearNight or WeatherVisualKind.CloudyNight;
|
||||
var conditionSecondary = CreateSolidBrush(palette.SecondaryText, isNightVisual ? (byte)0xF0 : (byte)0xE6);
|
||||
var rangeSecondary = CreateSolidBrush(palette.SecondaryText, isNightVisual ? (byte)0xE8 : (byte)0xD6);
|
||||
var forecastTimeBrush = CreateSolidBrush(palette.SecondaryText, isNightVisual ? (byte)0xDA : (byte)0xC6);
|
||||
var forecastTimeBrush = CreateSolidBrush(palette.TertiaryText, isNightVisual ? (byte)0xDA : (byte)0xC6);
|
||||
var forecastTempBrush = CreateSolidBrush(palette.PrimaryText, isNightVisual ? (byte)0xF4 : (byte)0xEA);
|
||||
HourlyPanelBorder.Background = CreateSolidBrush(isNightVisual ? "#1BFFFFFF" : "#1EFFFFFF");
|
||||
LocationIcon.Foreground = primary;
|
||||
@@ -593,7 +566,8 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
|
||||
return cached;
|
||||
}
|
||||
|
||||
if (WeatherBackgroundAssets.TryGetValue(kind, out var uriText))
|
||||
var uriText = HyperOS3WeatherTheme.ResolveBackgroundAsset(ToThemeKind(kind));
|
||||
if (!string.IsNullOrWhiteSpace(uriText))
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -621,104 +595,89 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
|
||||
return gradientBrush;
|
||||
}
|
||||
|
||||
private IBrush ResolveParticleBrush(HyperOS3WeatherVisualKind kind, string fallbackColor)
|
||||
{
|
||||
if (_particleBrushCache.TryGetValue(kind, out var cached))
|
||||
{
|
||||
return cached;
|
||||
}
|
||||
|
||||
var uriText = HyperOS3WeatherTheme.ResolveParticleAsset(kind);
|
||||
if (!string.IsNullOrWhiteSpace(uriText))
|
||||
{
|
||||
try
|
||||
{
|
||||
var uri = new Uri(uriText, UriKind.Absolute);
|
||||
using var stream = AssetLoader.Open(uri);
|
||||
var bitmap = new Bitmap(stream);
|
||||
var imageBrush = new ImageBrush
|
||||
{
|
||||
Source = bitmap,
|
||||
Stretch = Stretch.UniformToFill,
|
||||
AlignmentX = AlignmentX.Center,
|
||||
AlignmentY = AlignmentY.Center
|
||||
};
|
||||
_particleBrushCache[kind] = imageBrush;
|
||||
return imageBrush;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Fall through to solid particle color when the image cannot be loaded.
|
||||
}
|
||||
}
|
||||
|
||||
var solidBrush = CreateSolidBrush(fallbackColor);
|
||||
_particleBrushCache[kind] = solidBrush;
|
||||
return solidBrush;
|
||||
}
|
||||
|
||||
private static WeatherVisualKind ResolveVisualKind(int? weatherCode, bool isNight)
|
||||
{
|
||||
return weatherCode switch
|
||||
return HyperOS3WeatherTheme.ResolveVisualKind(weatherCode, isNight) switch
|
||||
{
|
||||
0 => isNight ? WeatherVisualKind.ClearNight : WeatherVisualKind.ClearDay,
|
||||
1 or 2 => isNight ? WeatherVisualKind.CloudyNight : WeatherVisualKind.CloudyDay,
|
||||
3 or 7 => WeatherVisualKind.RainLight,
|
||||
8 or 9 => WeatherVisualKind.RainHeavy,
|
||||
4 => WeatherVisualKind.Storm,
|
||||
13 or 14 or 15 or 16 => WeatherVisualKind.Snow,
|
||||
18 or 32 => WeatherVisualKind.Fog,
|
||||
_ => isNight ? WeatherVisualKind.CloudyNight : WeatherVisualKind.CloudyDay
|
||||
HyperOS3WeatherVisualKind.ClearDay => WeatherVisualKind.ClearDay,
|
||||
HyperOS3WeatherVisualKind.ClearNight => WeatherVisualKind.ClearNight,
|
||||
HyperOS3WeatherVisualKind.CloudyDay => WeatherVisualKind.CloudyDay,
|
||||
HyperOS3WeatherVisualKind.CloudyNight => WeatherVisualKind.CloudyNight,
|
||||
HyperOS3WeatherVisualKind.RainLight => WeatherVisualKind.RainLight,
|
||||
HyperOS3WeatherVisualKind.RainHeavy => WeatherVisualKind.RainHeavy,
|
||||
HyperOS3WeatherVisualKind.Storm => WeatherVisualKind.Storm,
|
||||
HyperOS3WeatherVisualKind.Snow => WeatherVisualKind.Snow,
|
||||
_ => WeatherVisualKind.Fog
|
||||
};
|
||||
}
|
||||
|
||||
private static WeatherVisualPalette ResolvePalette(WeatherVisualKind kind)
|
||||
{
|
||||
return kind switch
|
||||
{
|
||||
WeatherVisualKind.ClearDay => new WeatherVisualPalette(
|
||||
GradientFrom: "#4F92E8",
|
||||
GradientTo: "#83C5FF",
|
||||
Tint: "#234D87",
|
||||
PrimaryText: "#FFFFFFFF",
|
||||
SecondaryText: "#EEF5FF",
|
||||
ParticleColor: "#00FFFFFF"),
|
||||
WeatherVisualKind.ClearNight => new WeatherVisualPalette(
|
||||
GradientFrom: "#0E2B72",
|
||||
GradientTo: "#193A85",
|
||||
Tint: "#0A1E52",
|
||||
PrimaryText: "#FFFFFFFF",
|
||||
SecondaryText: "#CFE0FF",
|
||||
ParticleColor: "#00FFFFFF"),
|
||||
WeatherVisualKind.CloudyDay => new WeatherVisualPalette(
|
||||
GradientFrom: "#4A72B3",
|
||||
GradientTo: "#6A8EC2",
|
||||
Tint: "#2A487C",
|
||||
PrimaryText: "#FFFFFFFF",
|
||||
SecondaryText: "#EAF2FF",
|
||||
ParticleColor: "#16FFFFFF"),
|
||||
WeatherVisualKind.CloudyNight => new WeatherVisualPalette(
|
||||
GradientFrom: "#102A6B",
|
||||
GradientTo: "#193A80",
|
||||
Tint: "#0B1F51",
|
||||
PrimaryText: "#FFFFFFFF",
|
||||
SecondaryText: "#D5E4FF",
|
||||
ParticleColor: "#24FFFFFF"),
|
||||
WeatherVisualKind.RainLight => new WeatherVisualPalette(
|
||||
GradientFrom: "#32588A",
|
||||
GradientTo: "#4D74A8",
|
||||
Tint: "#1F3454",
|
||||
PrimaryText: "#FFFFFFFF",
|
||||
SecondaryText: "#E6F0FF",
|
||||
ParticleColor: "#88D7E8FF"),
|
||||
WeatherVisualKind.RainHeavy => new WeatherVisualPalette(
|
||||
GradientFrom: "#253F66",
|
||||
GradientTo: "#36567F",
|
||||
Tint: "#17263E",
|
||||
PrimaryText: "#FFFFFFFF",
|
||||
SecondaryText: "#DCE9FF",
|
||||
ParticleColor: "#A2CDE1FF"),
|
||||
WeatherVisualKind.Storm => new WeatherVisualPalette(
|
||||
GradientFrom: "#293A67",
|
||||
GradientTo: "#3A4F78",
|
||||
Tint: "#161E35",
|
||||
PrimaryText: "#FFFFFFFF",
|
||||
SecondaryText: "#DCE4F8",
|
||||
ParticleColor: "#A8C2D6F2"),
|
||||
WeatherVisualKind.Snow => new WeatherVisualPalette(
|
||||
GradientFrom: "#D1E8FF",
|
||||
GradientTo: "#A7D0F4",
|
||||
Tint: "#607C9D",
|
||||
PrimaryText: "#FF10253D",
|
||||
SecondaryText: "#FF2B435E",
|
||||
ParticleColor: "#CCFFFFFF"),
|
||||
_ => new WeatherVisualPalette(
|
||||
GradientFrom: "#445B7A",
|
||||
GradientTo: "#5B738F",
|
||||
Tint: "#2A3E56",
|
||||
PrimaryText: "#FFFFFFFF",
|
||||
SecondaryText: "#E7EDF6",
|
||||
ParticleColor: "#88E4EDF7")
|
||||
};
|
||||
var palette = HyperOS3WeatherTheme.ResolvePalette(ToThemeKind(kind));
|
||||
return new WeatherVisualPalette(
|
||||
palette.GradientFrom,
|
||||
palette.GradientTo,
|
||||
palette.Tint,
|
||||
palette.PrimaryText,
|
||||
palette.SecondaryText,
|
||||
palette.TertiaryText,
|
||||
palette.ParticleColor);
|
||||
}
|
||||
|
||||
private static Symbol ResolveWeatherSymbol(WeatherVisualKind kind)
|
||||
{
|
||||
return HyperOS3WeatherTheme.ResolveWeatherSymbol(ToThemeKind(kind));
|
||||
}
|
||||
|
||||
private static HyperOS3WeatherVisualKind ToThemeKind(WeatherVisualKind kind)
|
||||
{
|
||||
return kind switch
|
||||
{
|
||||
WeatherVisualKind.ClearDay => Symbol.WeatherSunny,
|
||||
WeatherVisualKind.ClearNight => Symbol.WeatherMoon,
|
||||
WeatherVisualKind.CloudyDay => Symbol.WeatherPartlyCloudyDay,
|
||||
WeatherVisualKind.CloudyNight => Symbol.WeatherPartlyCloudyNight,
|
||||
WeatherVisualKind.RainLight => Symbol.WeatherRainShowersDay,
|
||||
WeatherVisualKind.RainHeavy => Symbol.WeatherRain,
|
||||
WeatherVisualKind.Storm => Symbol.WeatherThunderstorm,
|
||||
WeatherVisualKind.Snow => Symbol.WeatherSnow,
|
||||
_ => Symbol.WeatherFog
|
||||
WeatherVisualKind.ClearDay => HyperOS3WeatherVisualKind.ClearDay,
|
||||
WeatherVisualKind.ClearNight => HyperOS3WeatherVisualKind.ClearNight,
|
||||
WeatherVisualKind.CloudyDay => HyperOS3WeatherVisualKind.CloudyDay,
|
||||
WeatherVisualKind.CloudyNight => HyperOS3WeatherVisualKind.CloudyNight,
|
||||
WeatherVisualKind.RainLight => HyperOS3WeatherVisualKind.RainLight,
|
||||
WeatherVisualKind.RainHeavy => HyperOS3WeatherVisualKind.RainHeavy,
|
||||
WeatherVisualKind.Storm => HyperOS3WeatherVisualKind.Storm,
|
||||
WeatherVisualKind.Snow => HyperOS3WeatherVisualKind.Snow,
|
||||
_ => HyperOS3WeatherVisualKind.Fog
|
||||
};
|
||||
}
|
||||
|
||||
@@ -994,18 +953,8 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
|
||||
|
||||
private static string ResolveWeatherIconAccent(Symbol symbol, bool isNightVisual)
|
||||
{
|
||||
return symbol switch
|
||||
{
|
||||
Symbol.WeatherSunny => isNightVisual ? "#FFD978" : "#F7C40A",
|
||||
Symbol.WeatherMoon => "#F3D38C",
|
||||
Symbol.WeatherPartlyCloudyDay => "#75B0FF",
|
||||
Symbol.WeatherPartlyCloudyNight => "#8AB6FF",
|
||||
Symbol.WeatherRainShowersDay => "#9ECBFF",
|
||||
Symbol.WeatherRain => "#8DBDF5",
|
||||
Symbol.WeatherThunderstorm => "#F4D16E",
|
||||
Symbol.WeatherSnow => "#C7E6FF",
|
||||
_ => isNightVisual ? "#D5E2F4" : "#E2ECFA"
|
||||
};
|
||||
var kind = isNightVisual ? HyperOS3WeatherVisualKind.ClearNight : HyperOS3WeatherVisualKind.ClearDay;
|
||||
return HyperOS3WeatherTheme.ResolveIconAccent(kind, symbol);
|
||||
}
|
||||
|
||||
private static string ResolvePreciseDisplayLocation(string? rawName, string languageCode, string fallback)
|
||||
@@ -1154,6 +1103,7 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
|
||||
private void ApplyAdaptiveTypography()
|
||||
{
|
||||
var (layoutWidth, layoutHeight) = ResolveLayoutViewport();
|
||||
var metrics = HyperOS3WeatherTheme.ResolveMetrics(HyperOS3WeatherWidgetKind.Hourly4x2);
|
||||
var scale = ResolveScale(layoutWidth, layoutHeight);
|
||||
var densityBoost = scale <= 0.55 ? 0.80 : scale <= 0.72 ? 0.88 : scale <= 0.92 ? 0.95 : scale >= 1.45 ? 1.06 : 1.0;
|
||||
var compactness = Math.Clamp((0.88 - scale) / 0.50, 0, 1);
|
||||
@@ -1162,9 +1112,9 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
|
||||
var conditionLength = Math.Max(1, ConditionTextBlock.Text?.Length ?? 2);
|
||||
var conditionCompression = conditionLength >= 12 ? 0.72 : conditionLength >= 8 ? 0.85 : conditionLength >= 6 ? 0.92 : 1.0;
|
||||
|
||||
ContentGrid.RowSpacing = Math.Clamp(layoutHeight * Lerp(0.030, 0.018, compactness), 2, 14);
|
||||
TopRowGrid.ColumnSpacing = Math.Clamp(layoutWidth * 0.014, 3, 14);
|
||||
BottomInfoStack.Spacing = Math.Clamp(layoutHeight * 0.016, 2, 10);
|
||||
ContentGrid.RowSpacing = Math.Clamp(Math.Max(metrics.MainGap * scale, layoutHeight * Lerp(0.030, 0.018, compactness)), 2, 14);
|
||||
TopRowGrid.ColumnSpacing = Math.Clamp(Math.Max(metrics.MainGap * scale, layoutWidth * 0.014), 3, 14);
|
||||
BottomInfoStack.Spacing = Math.Clamp(Math.Max(metrics.SectionGap * scale, layoutHeight * 0.016), 2, 10);
|
||||
BottomInfoStack.Margin = new Thickness(0, 0, 0, Math.Clamp(layoutHeight * 0.018, 0, 10));
|
||||
ConditionRangeStack.Spacing = Math.Clamp(layoutWidth * 0.010, 3, 12);
|
||||
ConditionRangeStack.Margin = new Thickness(0, 0, 0, Math.Clamp(layoutHeight * 0.018, 0, 10));
|
||||
@@ -1179,12 +1129,12 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
|
||||
var middleBandHeight = Math.Max(24, layoutHeight * 0.30);
|
||||
var bottomBandHeight = Math.Max(22, layoutHeight - topBandHeight - middleBandHeight - (ContentGrid.RowSpacing * 2));
|
||||
|
||||
LocationIcon.FontSize = Math.Min(Math.Clamp(24 * scale * densityBoost, 9, 30), topBandHeight * 0.58);
|
||||
CityTextBlock.FontSize = Math.Min(Math.Clamp(40 * scale * cityCompression * densityBoost, 12, 46), topBandHeight * 0.76);
|
||||
WeatherIconSymbol.FontSize = Math.Min(Math.Clamp(52 * scale * densityBoost, 12, 56), topBandHeight * 0.95);
|
||||
TemperatureTextBlock.FontSize = Math.Min(Math.Clamp(134 * scale * densityBoost, 26, 138), middleBandHeight * 0.92);
|
||||
ConditionTextBlock.FontSize = Math.Min(Math.Clamp(32 * scale * conditionCompression * densityBoost, 9, 40), middleBandHeight * 0.42);
|
||||
RangeTextBlock.FontSize = Math.Min(Math.Clamp(37 * scale * densityBoost, 10, 46), middleBandHeight * 0.50);
|
||||
LocationIcon.FontSize = Math.Min(Math.Clamp((metrics.IconFont * 0.6) * scale * densityBoost, 9, 30), topBandHeight * 0.58);
|
||||
CityTextBlock.FontSize = Math.Min(Math.Clamp((metrics.PrimaryTextFont * 1.42) * scale * cityCompression * densityBoost, 12, 46), topBandHeight * 0.76);
|
||||
WeatherIconSymbol.FontSize = Math.Min(Math.Clamp((metrics.IconFont * 1.02) * scale * densityBoost, 12, 56), topBandHeight * 0.95);
|
||||
TemperatureTextBlock.FontSize = Math.Min(Math.Clamp((metrics.PrimaryTemperatureFont * 1.40) * scale * densityBoost, 26, 138), middleBandHeight * 0.92);
|
||||
ConditionTextBlock.FontSize = Math.Min(Math.Clamp((metrics.PrimaryTextFont * 1.14) * scale * conditionCompression * densityBoost, 9, 40), middleBandHeight * 0.42);
|
||||
RangeTextBlock.FontSize = Math.Min(Math.Clamp((metrics.SecondaryTextFont * 1.54) * scale * densityBoost, 10, 46), middleBandHeight * 0.50);
|
||||
TemperatureTextBlock.Margin = new Thickness(0, Math.Clamp(layoutHeight * 0.008, 0, 6), 0, Math.Clamp(layoutHeight * 0.012, 0, 8));
|
||||
|
||||
var weightProgress = Math.Clamp((scale - 0.34) / 1.18, 0, 1);
|
||||
@@ -1219,13 +1169,13 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
|
||||
var hourlyIconMaxByHeight = Math.Clamp(hourlyLineHeight * 1.05, 8, 30);
|
||||
|
||||
var hourlyTimeSize = Math.Min(
|
||||
Math.Clamp(24 * scale * densityBoost, 8, 30),
|
||||
Math.Clamp((metrics.CaptionFont * 1.20) * scale * densityBoost, 8, 30),
|
||||
Math.Min(hourlyTimeMaxByWidth, hourlyTimeMaxByHeight));
|
||||
var hourlyIconSize = Math.Min(
|
||||
Math.Clamp(30 * scale * densityBoost, 8, 34),
|
||||
Math.Clamp((metrics.IconFont * 0.64) * scale * densityBoost, 8, 34),
|
||||
Math.Min(hourlyIconMaxByWidth, hourlyIconMaxByHeight));
|
||||
var hourlyTempSize = Math.Min(
|
||||
Math.Clamp(32 * scale * densityBoost, 8, 34),
|
||||
Math.Clamp((metrics.SecondaryTextFont * 1.34) * scale * densityBoost, 8, 34),
|
||||
Math.Min(hourlyTempMaxByWidth, hourlyTempMaxByHeight));
|
||||
|
||||
for (var i = 0; i < _hourlyTimeBlocks.Length; i++)
|
||||
@@ -1256,81 +1206,25 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
|
||||
|
||||
private WeatherMotionProfile ResolveMotionProfile(WeatherVisualKind kind)
|
||||
{
|
||||
return kind switch
|
||||
{
|
||||
WeatherVisualKind.ClearDay => new WeatherMotionProfile(
|
||||
DriftX: 8.0, DriftY: 4.0, ZoomBase: 1.055, ZoomAmplitude: 0.012,
|
||||
MotionOpacityBase: 0.22, MotionOpacityPulse: 0.05,
|
||||
LightOpacityBase: 0.68, LightOpacityPulse: 0.08,
|
||||
ShadeOpacityBase: 0.72, ShadeOpacityPulse: 0.03,
|
||||
PhaseStep: 0.015, ParticleCount: 0,
|
||||
ParticleSpeedMin: 0, ParticleSpeedMax: 0,
|
||||
ParticleLengthMin: 0, ParticleLengthMax: 0, ParticleDriftPerTick: 0),
|
||||
WeatherVisualKind.ClearNight => new WeatherMotionProfile(
|
||||
DriftX: 10.0, DriftY: 6.0, ZoomBase: 1.060, ZoomAmplitude: 0.014,
|
||||
MotionOpacityBase: 0.28, MotionOpacityPulse: 0.06,
|
||||
LightOpacityBase: 0.58, LightOpacityPulse: 0.07,
|
||||
ShadeOpacityBase: 0.82, ShadeOpacityPulse: 0.04,
|
||||
PhaseStep: 0.018, ParticleCount: 0,
|
||||
ParticleSpeedMin: 0, ParticleSpeedMax: 0,
|
||||
ParticleLengthMin: 0, ParticleLengthMax: 0, ParticleDriftPerTick: 0),
|
||||
WeatherVisualKind.CloudyDay => new WeatherMotionProfile(
|
||||
DriftX: 12.0, DriftY: 7.0, ZoomBase: 1.060, ZoomAmplitude: 0.013,
|
||||
MotionOpacityBase: 0.32, MotionOpacityPulse: 0.06,
|
||||
LightOpacityBase: 0.62, LightOpacityPulse: 0.07,
|
||||
ShadeOpacityBase: 0.80, ShadeOpacityPulse: 0.03,
|
||||
PhaseStep: 0.020, ParticleCount: 6,
|
||||
ParticleSpeedMin: 0.30, ParticleSpeedMax: 0.70,
|
||||
ParticleLengthMin: 14, ParticleLengthMax: 28, ParticleDriftPerTick: 0.10),
|
||||
WeatherVisualKind.CloudyNight => new WeatherMotionProfile(
|
||||
DriftX: 14.0, DriftY: 8.0, ZoomBase: 1.065, ZoomAmplitude: 0.013,
|
||||
MotionOpacityBase: 0.34, MotionOpacityPulse: 0.07,
|
||||
LightOpacityBase: 0.54, LightOpacityPulse: 0.06,
|
||||
ShadeOpacityBase: 0.85, ShadeOpacityPulse: 0.03,
|
||||
PhaseStep: 0.021, ParticleCount: 8,
|
||||
ParticleSpeedMin: 0.35, ParticleSpeedMax: 0.80,
|
||||
ParticleLengthMin: 16, ParticleLengthMax: 30, ParticleDriftPerTick: 0.12),
|
||||
WeatherVisualKind.RainLight => new WeatherMotionProfile(
|
||||
DriftX: 6.0, DriftY: 10.0, ZoomBase: 1.050, ZoomAmplitude: 0.010,
|
||||
MotionOpacityBase: 0.30, MotionOpacityPulse: 0.08,
|
||||
LightOpacityBase: 0.50, LightOpacityPulse: 0.04,
|
||||
ShadeOpacityBase: 0.84, ShadeOpacityPulse: 0.04,
|
||||
PhaseStep: 0.030, ParticleCount: 18,
|
||||
ParticleSpeedMin: 1.80, ParticleSpeedMax: 3.20,
|
||||
ParticleLengthMin: 14, ParticleLengthMax: 26, ParticleDriftPerTick: 0.70),
|
||||
WeatherVisualKind.RainHeavy => new WeatherMotionProfile(
|
||||
DriftX: 5.0, DriftY: 11.0, ZoomBase: 1.045, ZoomAmplitude: 0.010,
|
||||
MotionOpacityBase: 0.34, MotionOpacityPulse: 0.10,
|
||||
LightOpacityBase: 0.42, LightOpacityPulse: 0.03,
|
||||
ShadeOpacityBase: 0.88, ShadeOpacityPulse: 0.05,
|
||||
PhaseStep: 0.036, ParticleCount: 30,
|
||||
ParticleSpeedMin: 2.80, ParticleSpeedMax: 4.80,
|
||||
ParticleLengthMin: 18, ParticleLengthMax: 34, ParticleDriftPerTick: 0.92),
|
||||
WeatherVisualKind.Storm => new WeatherMotionProfile(
|
||||
DriftX: 4.0, DriftY: 12.0, ZoomBase: 1.042, ZoomAmplitude: 0.012,
|
||||
MotionOpacityBase: 0.38, MotionOpacityPulse: 0.12,
|
||||
LightOpacityBase: 0.36, LightOpacityPulse: 0.02,
|
||||
ShadeOpacityBase: 0.91, ShadeOpacityPulse: 0.04,
|
||||
PhaseStep: 0.042, ParticleCount: 34,
|
||||
ParticleSpeedMin: 3.60, ParticleSpeedMax: 5.80,
|
||||
ParticleLengthMin: 20, ParticleLengthMax: 36, ParticleDriftPerTick: 1.08),
|
||||
WeatherVisualKind.Snow => new WeatherMotionProfile(
|
||||
DriftX: 9.0, DriftY: 7.0, ZoomBase: 1.055, ZoomAmplitude: 0.012,
|
||||
MotionOpacityBase: 0.28, MotionOpacityPulse: 0.06,
|
||||
LightOpacityBase: 0.74, LightOpacityPulse: 0.08,
|
||||
ShadeOpacityBase: 0.68, ShadeOpacityPulse: 0.03,
|
||||
PhaseStep: 0.020, ParticleCount: 24,
|
||||
ParticleSpeedMin: 0.60, ParticleSpeedMax: 1.60,
|
||||
ParticleLengthMin: 3.0, ParticleLengthMax: 8.5, ParticleDriftPerTick: 0.24),
|
||||
_ => new WeatherMotionProfile(
|
||||
DriftX: 7.0, DriftY: 5.0, ZoomBase: 1.050, ZoomAmplitude: 0.011,
|
||||
MotionOpacityBase: 0.30, MotionOpacityPulse: 0.05,
|
||||
LightOpacityBase: 0.58, LightOpacityPulse: 0.05,
|
||||
ShadeOpacityBase: 0.86, ShadeOpacityPulse: 0.03,
|
||||
PhaseStep: 0.018, ParticleCount: 10,
|
||||
ParticleSpeedMin: 0.25, ParticleSpeedMax: 0.70,
|
||||
ParticleLengthMin: 16, ParticleLengthMax: 34, ParticleDriftPerTick: 0.12)
|
||||
};
|
||||
var motion = HyperOS3WeatherTheme.ResolveMotion(ToThemeKind(kind));
|
||||
return new WeatherMotionProfile(
|
||||
motion.DriftX,
|
||||
motion.DriftY,
|
||||
motion.ZoomBase,
|
||||
motion.ZoomAmplitude,
|
||||
motion.MotionOpacityBase,
|
||||
motion.MotionOpacityPulse,
|
||||
motion.LightOpacityBase,
|
||||
motion.LightOpacityPulse,
|
||||
motion.ShadeOpacityBase,
|
||||
motion.ShadeOpacityPulse,
|
||||
motion.PhaseStep,
|
||||
motion.ParticleCount,
|
||||
motion.ParticleSpeedMin,
|
||||
motion.ParticleSpeedMax,
|
||||
motion.ParticleLengthMin,
|
||||
motion.ParticleLengthMax,
|
||||
motion.ParticleDriftPerTick);
|
||||
}
|
||||
|
||||
private void ResetAnimationState()
|
||||
|
||||
434
LanMontainDesktop/Views/Components/HyperOS3WeatherTheme.cs
Normal file
434
LanMontainDesktop/Views/Components/HyperOS3WeatherTheme.cs
Normal file
@@ -0,0 +1,434 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using FluentIcons.Common;
|
||||
using LanMontainDesktop.Models;
|
||||
|
||||
namespace LanMontainDesktop.Views.Components;
|
||||
|
||||
public enum HyperOS3WeatherVisualKind
|
||||
{
|
||||
ClearDay,
|
||||
ClearNight,
|
||||
CloudyDay,
|
||||
CloudyNight,
|
||||
RainLight,
|
||||
RainHeavy,
|
||||
Storm,
|
||||
Snow,
|
||||
Fog
|
||||
}
|
||||
|
||||
public enum HyperOS3WeatherWidgetKind
|
||||
{
|
||||
Realtime2x2,
|
||||
Hourly4x2,
|
||||
MultiDay4x2,
|
||||
WeatherClock2x1,
|
||||
Extended4x4
|
||||
}
|
||||
|
||||
public readonly record struct HyperOS3WeatherPalette(
|
||||
string GradientFrom,
|
||||
string GradientTo,
|
||||
string Tint,
|
||||
string PrimaryText,
|
||||
string SecondaryText,
|
||||
string TertiaryText,
|
||||
string ParticleColor);
|
||||
|
||||
public readonly record struct HyperOS3WeatherMotion(
|
||||
double DriftX,
|
||||
double DriftY,
|
||||
double ZoomBase,
|
||||
double ZoomAmplitude,
|
||||
double MotionOpacityBase,
|
||||
double MotionOpacityPulse,
|
||||
double LightOpacityBase,
|
||||
double LightOpacityPulse,
|
||||
double ShadeOpacityBase,
|
||||
double ShadeOpacityPulse,
|
||||
double PhaseStep,
|
||||
int ParticleCount,
|
||||
double ParticleSpeedMin,
|
||||
double ParticleSpeedMax,
|
||||
double ParticleLengthMin,
|
||||
double ParticleLengthMax,
|
||||
double ParticleDriftPerTick);
|
||||
|
||||
public readonly record struct HyperOS3WeatherMetrics(
|
||||
double CornerRadiusScale,
|
||||
double HorizontalPaddingScale,
|
||||
double VerticalPaddingScale,
|
||||
double PrimaryTemperatureFont,
|
||||
double PrimaryTextFont,
|
||||
double SecondaryTextFont,
|
||||
double CaptionFont,
|
||||
double IconFont,
|
||||
double MainGap,
|
||||
double SectionGap);
|
||||
|
||||
public static class HyperOS3WeatherTheme
|
||||
{
|
||||
private static readonly HyperOS3WeatherPalette FallbackPalette = new(
|
||||
GradientFrom: "#7187A8",
|
||||
GradientTo: "#92A5C2",
|
||||
Tint: "#3C4E66",
|
||||
PrimaryText: "#FFFFFFFF",
|
||||
SecondaryText: "#E4ECF7",
|
||||
TertiaryText: "#C9D4E4",
|
||||
ParticleColor: "#66EAF2FF");
|
||||
|
||||
private static readonly HyperOS3WeatherMotion FallbackMotion = new(
|
||||
DriftX: 8.0, DriftY: 6.0, ZoomBase: 1.050, ZoomAmplitude: 0.010,
|
||||
MotionOpacityBase: 0.28, MotionOpacityPulse: 0.05,
|
||||
LightOpacityBase: 0.62, LightOpacityPulse: 0.05,
|
||||
ShadeOpacityBase: 0.83, ShadeOpacityPulse: 0.03,
|
||||
PhaseStep: 0.018, ParticleCount: 10,
|
||||
ParticleSpeedMin: 0.25, ParticleSpeedMax: 0.70,
|
||||
ParticleLengthMin: 16, ParticleLengthMax: 34, ParticleDriftPerTick: 0.12);
|
||||
|
||||
private static readonly IReadOnlyDictionary<HyperOS3WeatherVisualKind, string> BackgroundAssets =
|
||||
new Dictionary<HyperOS3WeatherVisualKind, string>
|
||||
{
|
||||
[HyperOS3WeatherVisualKind.ClearDay] = "avares://LanMontainDesktop/Assets/Weather/HyperOS3/hyper_cross_sky_day.png",
|
||||
[HyperOS3WeatherVisualKind.ClearNight] = "avares://LanMontainDesktop/Assets/Weather/HyperOS3/hyper_cross_sky_night.png",
|
||||
[HyperOS3WeatherVisualKind.CloudyDay] = "avares://LanMontainDesktop/Assets/Weather/HyperOS3/hyper_sky_front.png",
|
||||
[HyperOS3WeatherVisualKind.CloudyNight] = "avares://LanMontainDesktop/Assets/Weather/HyperOS3/hyper_sky_back.png",
|
||||
[HyperOS3WeatherVisualKind.RainLight] = "avares://LanMontainDesktop/Assets/Weather/HyperOS3/hyper_sky_front.png",
|
||||
[HyperOS3WeatherVisualKind.RainHeavy] = "avares://LanMontainDesktop/Assets/Weather/HyperOS3/hyper_sky_back.png",
|
||||
[HyperOS3WeatherVisualKind.Storm] = "avares://LanMontainDesktop/Assets/Weather/HyperOS3/hyper_cross_sky_night.png",
|
||||
[HyperOS3WeatherVisualKind.Snow] = "avares://LanMontainDesktop/Assets/Weather/HyperOS3/hyper_sky_top.png",
|
||||
[HyperOS3WeatherVisualKind.Fog] = "avares://LanMontainDesktop/Assets/Weather/HyperOS3/hyper_sky_back.png"
|
||||
};
|
||||
|
||||
private static readonly IReadOnlyDictionary<HyperOS3WeatherVisualKind, HyperOS3WeatherPalette> Palettes =
|
||||
new Dictionary<HyperOS3WeatherVisualKind, HyperOS3WeatherPalette>
|
||||
{
|
||||
[HyperOS3WeatherVisualKind.ClearDay] = new(
|
||||
GradientFrom: "#2D87DA",
|
||||
GradientTo: "#79BAF2",
|
||||
Tint: "#2E6CB5",
|
||||
PrimaryText: "#F7FCFF",
|
||||
SecondaryText: "#E8F1FD",
|
||||
TertiaryText: "#D6E5F8",
|
||||
ParticleColor: "#00FFFFFF"),
|
||||
[HyperOS3WeatherVisualKind.ClearNight] = new(
|
||||
GradientFrom: "#5A6B85",
|
||||
GradientTo: "#9DADC2",
|
||||
Tint: "#495B78",
|
||||
PrimaryText: "#F9FBFF",
|
||||
SecondaryText: "#E2EAF6",
|
||||
TertiaryText: "#C6D2E3",
|
||||
ParticleColor: "#00FFFFFF"),
|
||||
[HyperOS3WeatherVisualKind.CloudyDay] = new(
|
||||
GradientFrom: "#5F88B6",
|
||||
GradientTo: "#8FB0D1",
|
||||
Tint: "#496F98",
|
||||
PrimaryText: "#F8FCFF",
|
||||
SecondaryText: "#E4EDF8",
|
||||
TertiaryText: "#CBD9EA",
|
||||
ParticleColor: "#26FFFFFF"),
|
||||
[HyperOS3WeatherVisualKind.CloudyNight] = new(
|
||||
GradientFrom: "#556A85",
|
||||
GradientTo: "#95A5BC",
|
||||
Tint: "#43566E",
|
||||
PrimaryText: "#F6FAFF",
|
||||
SecondaryText: "#DEE7F4",
|
||||
TertiaryText: "#C1CDDE",
|
||||
ParticleColor: "#30F0F5FF"),
|
||||
[HyperOS3WeatherVisualKind.RainLight] = new(
|
||||
GradientFrom: "#5A7DA7",
|
||||
GradientTo: "#8FAAC8",
|
||||
Tint: "#3F5F84",
|
||||
PrimaryText: "#F8FBFF",
|
||||
SecondaryText: "#E3EAF5",
|
||||
TertiaryText: "#C4D0E0",
|
||||
ParticleColor: "#88D7E8FF"),
|
||||
[HyperOS3WeatherVisualKind.RainHeavy] = new(
|
||||
GradientFrom: "#4C678A",
|
||||
GradientTo: "#7D95AF",
|
||||
Tint: "#354C69",
|
||||
PrimaryText: "#F9FCFF",
|
||||
SecondaryText: "#E0E8F4",
|
||||
TertiaryText: "#C0CBDA",
|
||||
ParticleColor: "#A2CDE1FF"),
|
||||
[HyperOS3WeatherVisualKind.Storm] = new(
|
||||
GradientFrom: "#435D7B",
|
||||
GradientTo: "#6F869F",
|
||||
Tint: "#2B3D53",
|
||||
PrimaryText: "#F9FCFF",
|
||||
SecondaryText: "#DBE5F2",
|
||||
TertiaryText: "#B9C5D7",
|
||||
ParticleColor: "#A8C2D6F2"),
|
||||
[HyperOS3WeatherVisualKind.Snow] = new(
|
||||
GradientFrom: "#9FB7D0",
|
||||
GradientTo: "#B7CAE0",
|
||||
Tint: "#6D839D",
|
||||
PrimaryText: "#F8FBFF",
|
||||
SecondaryText: "#E5EDF7",
|
||||
TertiaryText: "#CDD9E7",
|
||||
ParticleColor: "#CCFFFFFF"),
|
||||
[HyperOS3WeatherVisualKind.Fog] = new(
|
||||
GradientFrom: "#687E9A",
|
||||
GradientTo: "#9AACBE",
|
||||
Tint: "#4B6078",
|
||||
PrimaryText: "#F8FBFF",
|
||||
SecondaryText: "#E3EAF4",
|
||||
TertiaryText: "#C4D0DF",
|
||||
ParticleColor: "#88E4EDF7")
|
||||
};
|
||||
|
||||
private static readonly IReadOnlyDictionary<HyperOS3WeatherVisualKind, HyperOS3WeatherMotion> Motions =
|
||||
new Dictionary<HyperOS3WeatherVisualKind, HyperOS3WeatherMotion>
|
||||
{
|
||||
[HyperOS3WeatherVisualKind.ClearDay] = new(
|
||||
DriftX: 8.0, DriftY: 4.0, ZoomBase: 1.055, ZoomAmplitude: 0.012,
|
||||
MotionOpacityBase: 0.22, MotionOpacityPulse: 0.05,
|
||||
LightOpacityBase: 0.68, LightOpacityPulse: 0.08,
|
||||
ShadeOpacityBase: 0.72, ShadeOpacityPulse: 0.03,
|
||||
PhaseStep: 0.015, ParticleCount: 0,
|
||||
ParticleSpeedMin: 0, ParticleSpeedMax: 0,
|
||||
ParticleLengthMin: 0, ParticleLengthMax: 0, ParticleDriftPerTick: 0),
|
||||
[HyperOS3WeatherVisualKind.ClearNight] = new(
|
||||
DriftX: 10.0, DriftY: 6.0, ZoomBase: 1.060, ZoomAmplitude: 0.014,
|
||||
MotionOpacityBase: 0.28, MotionOpacityPulse: 0.06,
|
||||
LightOpacityBase: 0.58, LightOpacityPulse: 0.07,
|
||||
ShadeOpacityBase: 0.82, ShadeOpacityPulse: 0.04,
|
||||
PhaseStep: 0.018, 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,
|
||||
LightOpacityBase: 0.62, LightOpacityPulse: 0.07,
|
||||
ShadeOpacityBase: 0.80, ShadeOpacityPulse: 0.03,
|
||||
PhaseStep: 0.020, ParticleCount: 6,
|
||||
ParticleSpeedMin: 0.30, ParticleSpeedMax: 0.70,
|
||||
ParticleLengthMin: 14, ParticleLengthMax: 28, ParticleDriftPerTick: 0.10),
|
||||
[HyperOS3WeatherVisualKind.CloudyNight] = new(
|
||||
DriftX: 14.0, DriftY: 8.0, ZoomBase: 1.065, ZoomAmplitude: 0.013,
|
||||
MotionOpacityBase: 0.34, MotionOpacityPulse: 0.07,
|
||||
LightOpacityBase: 0.54, LightOpacityPulse: 0.06,
|
||||
ShadeOpacityBase: 0.85, ShadeOpacityPulse: 0.03,
|
||||
PhaseStep: 0.021, ParticleCount: 8,
|
||||
ParticleSpeedMin: 0.35, ParticleSpeedMax: 0.80,
|
||||
ParticleLengthMin: 16, ParticleLengthMax: 30, ParticleDriftPerTick: 0.12),
|
||||
[HyperOS3WeatherVisualKind.RainLight] = new(
|
||||
DriftX: 6.0, DriftY: 10.0, ZoomBase: 1.050, ZoomAmplitude: 0.010,
|
||||
MotionOpacityBase: 0.30, MotionOpacityPulse: 0.08,
|
||||
LightOpacityBase: 0.50, LightOpacityPulse: 0.04,
|
||||
ShadeOpacityBase: 0.84, ShadeOpacityPulse: 0.04,
|
||||
PhaseStep: 0.030, ParticleCount: 18,
|
||||
ParticleSpeedMin: 1.80, ParticleSpeedMax: 3.20,
|
||||
ParticleLengthMin: 14, ParticleLengthMax: 26, ParticleDriftPerTick: 0.70),
|
||||
[HyperOS3WeatherVisualKind.RainHeavy] = new(
|
||||
DriftX: 5.0, DriftY: 11.0, ZoomBase: 1.045, ZoomAmplitude: 0.010,
|
||||
MotionOpacityBase: 0.34, MotionOpacityPulse: 0.10,
|
||||
LightOpacityBase: 0.42, LightOpacityPulse: 0.03,
|
||||
ShadeOpacityBase: 0.88, ShadeOpacityPulse: 0.05,
|
||||
PhaseStep: 0.036, ParticleCount: 30,
|
||||
ParticleSpeedMin: 2.80, ParticleSpeedMax: 4.80,
|
||||
ParticleLengthMin: 18, ParticleLengthMax: 34, ParticleDriftPerTick: 0.92),
|
||||
[HyperOS3WeatherVisualKind.Storm] = new(
|
||||
DriftX: 4.0, DriftY: 12.0, ZoomBase: 1.042, ZoomAmplitude: 0.012,
|
||||
MotionOpacityBase: 0.38, MotionOpacityPulse: 0.12,
|
||||
LightOpacityBase: 0.36, LightOpacityPulse: 0.02,
|
||||
ShadeOpacityBase: 0.91, ShadeOpacityPulse: 0.04,
|
||||
PhaseStep: 0.042, ParticleCount: 34,
|
||||
ParticleSpeedMin: 3.60, ParticleSpeedMax: 5.80,
|
||||
ParticleLengthMin: 20, ParticleLengthMax: 36, ParticleDriftPerTick: 1.08),
|
||||
[HyperOS3WeatherVisualKind.Snow] = new(
|
||||
DriftX: 9.0, DriftY: 7.0, ZoomBase: 1.055, ZoomAmplitude: 0.012,
|
||||
MotionOpacityBase: 0.28, MotionOpacityPulse: 0.06,
|
||||
LightOpacityBase: 0.74, LightOpacityPulse: 0.08,
|
||||
ShadeOpacityBase: 0.68, ShadeOpacityPulse: 0.03,
|
||||
PhaseStep: 0.020, ParticleCount: 24,
|
||||
ParticleSpeedMin: 0.60, ParticleSpeedMax: 1.60,
|
||||
ParticleLengthMin: 3.0, ParticleLengthMax: 8.5, ParticleDriftPerTick: 0.24),
|
||||
[HyperOS3WeatherVisualKind.Fog] = new(
|
||||
DriftX: 7.0, DriftY: 5.0, ZoomBase: 1.050, ZoomAmplitude: 0.011,
|
||||
MotionOpacityBase: 0.30, MotionOpacityPulse: 0.05,
|
||||
LightOpacityBase: 0.58, LightOpacityPulse: 0.05,
|
||||
ShadeOpacityBase: 0.86, ShadeOpacityPulse: 0.03,
|
||||
PhaseStep: 0.018, ParticleCount: 10,
|
||||
ParticleSpeedMin: 0.25, ParticleSpeedMax: 0.70,
|
||||
ParticleLengthMin: 16, ParticleLengthMax: 34, ParticleDriftPerTick: 0.12)
|
||||
};
|
||||
|
||||
private static readonly IReadOnlyDictionary<HyperOS3WeatherWidgetKind, HyperOS3WeatherMetrics> Metrics =
|
||||
new Dictionary<HyperOS3WeatherWidgetKind, HyperOS3WeatherMetrics>
|
||||
{
|
||||
[HyperOS3WeatherWidgetKind.Realtime2x2] = new(0.45, 0.38, 0.38, 108, 30, 30, 24, 40, 8, 4),
|
||||
[HyperOS3WeatherWidgetKind.Hourly4x2] = new(0.45, 0.32, 0.30, 96, 28, 24, 20, 30, 8, 4),
|
||||
[HyperOS3WeatherWidgetKind.MultiDay4x2] = new(0.45, 0.32, 0.30, 96, 28, 24, 20, 30, 8, 4),
|
||||
[HyperOS3WeatherWidgetKind.WeatherClock2x1] = new(0.40, 0.18, 0.14, 42, 18, 15, 12, 18, 4, 3),
|
||||
[HyperOS3WeatherWidgetKind.Extended4x4] = new(0.45, 0.28, 0.28, 88, 24, 20, 18, 24, 8, 6)
|
||||
};
|
||||
|
||||
public static HyperOS3WeatherVisualKind ResolveVisualKind(int? weatherCode, bool isNight)
|
||||
{
|
||||
return 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
|
||||
};
|
||||
}
|
||||
|
||||
public static HyperOS3WeatherPalette ResolvePalette(HyperOS3WeatherVisualKind kind)
|
||||
{
|
||||
return Palettes.TryGetValue(kind, out var palette) ? palette : FallbackPalette;
|
||||
}
|
||||
|
||||
public static HyperOS3WeatherMotion ResolveMotion(HyperOS3WeatherVisualKind kind)
|
||||
{
|
||||
return Motions.TryGetValue(kind, out var motion) ? motion : FallbackMotion;
|
||||
}
|
||||
|
||||
public static HyperOS3WeatherMetrics ResolveMetrics(HyperOS3WeatherWidgetKind kind)
|
||||
{
|
||||
return Metrics.TryGetValue(kind, out var metrics)
|
||||
? metrics
|
||||
: Metrics[HyperOS3WeatherWidgetKind.Realtime2x2];
|
||||
}
|
||||
|
||||
public static string? ResolveBackgroundAsset(HyperOS3WeatherVisualKind kind)
|
||||
{
|
||||
return BackgroundAssets.TryGetValue(kind, out var asset) ? asset : null;
|
||||
}
|
||||
|
||||
public static string ResolveSunCoreAsset()
|
||||
{
|
||||
return "avares://LanMontainDesktop/Assets/Weather/HyperOS3/hyper_sun_core.png";
|
||||
}
|
||||
|
||||
public static string ResolveSunRingAsset()
|
||||
{
|
||||
return "avares://LanMontainDesktop/Assets/Weather/HyperOS3/hyper_sun_ring.png";
|
||||
}
|
||||
|
||||
public static string? ResolveParticleAsset(HyperOS3WeatherVisualKind kind)
|
||||
{
|
||||
return kind switch
|
||||
{
|
||||
HyperOS3WeatherVisualKind.RainLight or HyperOS3WeatherVisualKind.RainHeavy or HyperOS3WeatherVisualKind.Storm
|
||||
=> "avares://LanMontainDesktop/Assets/Weather/HyperOS3/hyper_rain_drop.png",
|
||||
HyperOS3WeatherVisualKind.Snow
|
||||
=> "avares://LanMontainDesktop/Assets/Weather/HyperOS3/hyper_snow_flake.png",
|
||||
HyperOS3WeatherVisualKind.Fog
|
||||
=> "avares://LanMontainDesktop/Assets/Weather/HyperOS3/hyper_fog.png",
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
public static Symbol ResolveWeatherSymbol(HyperOS3WeatherVisualKind kind)
|
||||
{
|
||||
return kind switch
|
||||
{
|
||||
HyperOS3WeatherVisualKind.ClearDay => Symbol.WeatherSunny,
|
||||
HyperOS3WeatherVisualKind.ClearNight => Symbol.WeatherMoon,
|
||||
HyperOS3WeatherVisualKind.CloudyDay => Symbol.WeatherPartlyCloudyDay,
|
||||
HyperOS3WeatherVisualKind.CloudyNight => Symbol.WeatherPartlyCloudyNight,
|
||||
HyperOS3WeatherVisualKind.RainLight => Symbol.WeatherRainShowersDay,
|
||||
HyperOS3WeatherVisualKind.RainHeavy => Symbol.WeatherRain,
|
||||
HyperOS3WeatherVisualKind.Storm => Symbol.WeatherThunderstorm,
|
||||
HyperOS3WeatherVisualKind.Snow => Symbol.WeatherSnow,
|
||||
_ => Symbol.WeatherFog
|
||||
};
|
||||
}
|
||||
|
||||
public static string ResolveIconAccent(HyperOS3WeatherVisualKind kind, Symbol symbol)
|
||||
{
|
||||
var isNight = kind is HyperOS3WeatherVisualKind.ClearNight or HyperOS3WeatherVisualKind.CloudyNight;
|
||||
return symbol switch
|
||||
{
|
||||
Symbol.WeatherSunny => isNight ? "#F0D18A" : "#F5C65C",
|
||||
Symbol.WeatherMoon => "#EED49A",
|
||||
Symbol.WeatherPartlyCloudyDay => "#F3D68E",
|
||||
Symbol.WeatherPartlyCloudyNight => "#CFDCFF",
|
||||
Symbol.WeatherRainShowersDay => "#C7DCF9",
|
||||
Symbol.WeatherRain => "#BCD4F4",
|
||||
Symbol.WeatherThunderstorm => "#F0D38B",
|
||||
Symbol.WeatherSnow => "#EBF5FF",
|
||||
Symbol.WeatherFog => "#E3EBF6",
|
||||
_ => isNight ? "#D2DDEE" : "#E5EEF9"
|
||||
};
|
||||
}
|
||||
|
||||
public static bool ResolveIsNightPreferred(
|
||||
WeatherSnapshot snapshot,
|
||||
TimeZoneInfo? timeZone,
|
||||
DateTime fallbackLocalTime)
|
||||
{
|
||||
if (snapshot.Current.IsDaylight.HasValue)
|
||||
{
|
||||
return !snapshot.Current.IsDaylight.Value;
|
||||
}
|
||||
|
||||
var referenceTime = snapshot.ObservationTime?.DateTime ?? fallbackLocalTime;
|
||||
if (snapshot.ObservationTime.HasValue && timeZone is not null)
|
||||
{
|
||||
referenceTime = TimeZoneInfo.ConvertTime(snapshot.ObservationTime.Value, timeZone).DateTime;
|
||||
}
|
||||
|
||||
var date = DateOnly.FromDateTime(referenceTime);
|
||||
var todayForecast = snapshot.DailyForecasts.FirstOrDefault(item => item.Date == date);
|
||||
if (todayForecast is not null &&
|
||||
TryParseClockTime(todayForecast.SunriseTime, out var sunrise) &&
|
||||
TryParseClockTime(todayForecast.SunsetTime, out var sunset) &&
|
||||
sunrise < sunset)
|
||||
{
|
||||
var time = referenceTime.TimeOfDay;
|
||||
return time < sunrise || time >= sunset;
|
||||
}
|
||||
|
||||
if (snapshot.ObservationTime.HasValue)
|
||||
{
|
||||
var observed = snapshot.ObservationTime.Value;
|
||||
if (timeZone is not null)
|
||||
{
|
||||
observed = TimeZoneInfo.ConvertTime(observed, timeZone);
|
||||
}
|
||||
|
||||
return observed.Hour < 6 || observed.Hour >= 18;
|
||||
}
|
||||
|
||||
return fallbackLocalTime.Hour < 6 || fallbackLocalTime.Hour >= 18;
|
||||
}
|
||||
|
||||
private static bool TryParseClockTime(string? text, out TimeSpan value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
var candidate = text.Trim();
|
||||
if (TimeSpan.TryParse(candidate, CultureInfo.InvariantCulture, out value))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (DateTimeOffset.TryParse(candidate, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out var dto))
|
||||
{
|
||||
value = dto.TimeOfDay;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (DateTime.TryParse(candidate, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out var dt))
|
||||
{
|
||||
value = dt.TimeOfDay;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,7 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
|
||||
string Tint,
|
||||
string PrimaryText,
|
||||
string SecondaryText,
|
||||
string TertiaryText,
|
||||
string ParticleColor);
|
||||
|
||||
private readonly record struct WeatherMotionProfile(
|
||||
@@ -81,20 +82,6 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
|
||||
Symbol Icon,
|
||||
string TemperatureText);
|
||||
|
||||
private static readonly IReadOnlyDictionary<WeatherVisualKind, string> WeatherBackgroundAssets =
|
||||
new Dictionary<WeatherVisualKind, string>
|
||||
{
|
||||
[WeatherVisualKind.ClearDay] = "avares://LanMontainDesktop/Assets/Weather/clear_day.jpg",
|
||||
[WeatherVisualKind.ClearNight] = "avares://LanMontainDesktop/Assets/Weather/clear_night.jpg",
|
||||
[WeatherVisualKind.CloudyDay] = "avares://LanMontainDesktop/Assets/Weather/cloudy_day.jpg",
|
||||
[WeatherVisualKind.CloudyNight] = "avares://LanMontainDesktop/Assets/Weather/cloudy_night.jpg",
|
||||
[WeatherVisualKind.RainLight] = "avares://LanMontainDesktop/Assets/Weather/rain_light.jpg",
|
||||
[WeatherVisualKind.RainHeavy] = "avares://LanMontainDesktop/Assets/Weather/rain_heavy.jpg",
|
||||
[WeatherVisualKind.Storm] = "avares://LanMontainDesktop/Assets/Weather/storm_dark.jpg",
|
||||
[WeatherVisualKind.Snow] = "avares://LanMontainDesktop/Assets/Weather/snow_soft.jpg",
|
||||
[WeatherVisualKind.Fog] = "avares://LanMontainDesktop/Assets/Weather/fog_haze.jpg"
|
||||
};
|
||||
|
||||
private static readonly IWeatherInfoService DefaultWeatherInfoService = new XiaomiWeatherService();
|
||||
|
||||
private readonly DispatcherTimer _refreshTimer = new()
|
||||
@@ -110,6 +97,7 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
|
||||
private readonly AppSettingsService _settingsService = new();
|
||||
private readonly LocalizationService _localizationService = new();
|
||||
private readonly Dictionary<WeatherVisualKind, IBrush> _backgroundBrushCache = new();
|
||||
private readonly Dictionary<HyperOS3WeatherVisualKind, IBrush> _particleBrushCache = new();
|
||||
private readonly List<Border> _particleVisuals = new();
|
||||
private readonly List<ParticleState> _particleStates = new();
|
||||
private readonly Random _particleRandom = new();
|
||||
@@ -223,10 +211,11 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
|
||||
public void ApplyCellSize(double cellSize)
|
||||
{
|
||||
_currentCellSize = Math.Max(1, cellSize);
|
||||
var metrics = HyperOS3WeatherTheme.ResolveMetrics(HyperOS3WeatherWidgetKind.MultiDay4x2);
|
||||
var scale = ResolveScale();
|
||||
var hostWidth = Bounds.Width > 1 ? Bounds.Width : Math.Max(140, _currentCellSize * 4);
|
||||
var hostHeight = Bounds.Height > 1 ? Bounds.Height : Math.Max(78, _currentCellSize * 2);
|
||||
var cornerRadius = Math.Clamp(_currentCellSize * 0.45, 24, 44);
|
||||
var cornerRadius = Math.Clamp(_currentCellSize * metrics.CornerRadiusScale, 24, 44);
|
||||
|
||||
RootBorder.CornerRadius = new CornerRadius(cornerRadius);
|
||||
BackgroundImageLayer.CornerRadius = new CornerRadius(cornerRadius);
|
||||
@@ -235,8 +224,8 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
|
||||
BackgroundLightLayer.CornerRadius = new CornerRadius(cornerRadius);
|
||||
BackgroundShadeLayer.CornerRadius = new CornerRadius(cornerRadius);
|
||||
ContentPaddingBorder.Padding = new Thickness(
|
||||
Math.Clamp(Math.Min(20 * scale, hostWidth * 0.028), 3, 18),
|
||||
Math.Clamp(Math.Min(14 * scale, hostHeight * 0.060), 2, 14));
|
||||
Math.Clamp(Math.Min((_currentCellSize * metrics.HorizontalPaddingScale) * scale, hostWidth * 0.028), 3, 18),
|
||||
Math.Clamp(Math.Min((_currentCellSize * metrics.VerticalPaddingScale) * scale, hostHeight * 0.060), 2, 14));
|
||||
ApplyAdaptiveTypography();
|
||||
ResetParticles();
|
||||
}
|
||||
@@ -319,26 +308,10 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
|
||||
|
||||
private bool ResolveIsNight(WeatherSnapshot snapshot)
|
||||
{
|
||||
if (snapshot.ObservationTime.HasValue)
|
||||
{
|
||||
var observed = snapshot.ObservationTime.Value;
|
||||
try
|
||||
{
|
||||
if (_timeZoneService is not null)
|
||||
{
|
||||
var zoned = TimeZoneInfo.ConvertTime(observed, _timeZoneService.CurrentTimeZone);
|
||||
return zoned.Hour < 6 || zoned.Hour >= 18;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// fall through to local clock
|
||||
}
|
||||
|
||||
return observed.Hour < 6 || observed.Hour >= 18;
|
||||
}
|
||||
|
||||
return IsNightNow();
|
||||
return HyperOS3WeatherTheme.ResolveIsNightPreferred(
|
||||
snapshot,
|
||||
_timeZoneService?.CurrentTimeZone,
|
||||
_timeZoneService?.GetCurrentTime() ?? DateTime.Now);
|
||||
}
|
||||
|
||||
private bool IsNightNow()
|
||||
@@ -556,11 +529,11 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
|
||||
BackgroundTintLayer.Background = CreateSolidBrush(palette.Tint);
|
||||
|
||||
var primary = CreateSolidBrush(palette.PrimaryText);
|
||||
var particleBrush = CreateSolidBrush(palette.ParticleColor);
|
||||
var particleBrush = ResolveParticleBrush(ToThemeKind(kind), palette.ParticleColor);
|
||||
var isNightVisual = kind is WeatherVisualKind.ClearNight or WeatherVisualKind.CloudyNight;
|
||||
var conditionSecondary = CreateSolidBrush(palette.SecondaryText, isNightVisual ? (byte)0xF0 : (byte)0xE6);
|
||||
var airQualitySecondary = CreateSolidBrush(palette.SecondaryText, isNightVisual ? (byte)0xDE : (byte)0xCC);
|
||||
var forecastTimeBrush = CreateSolidBrush(palette.SecondaryText, isNightVisual ? (byte)0xEA : (byte)0xB6);
|
||||
var forecastTimeBrush = CreateSolidBrush(palette.TertiaryText, isNightVisual ? (byte)0xEA : (byte)0xB6);
|
||||
var forecastTempBrush = CreateSolidBrush(palette.PrimaryText, isNightVisual ? (byte)0xF8 : (byte)0xE4);
|
||||
HourlyPanelBorder.Background = CreateSolidBrush(isNightVisual ? "#24FFFFFF" : "#1EFFFFFF");
|
||||
LocationIcon.Foreground = primary;
|
||||
@@ -592,7 +565,8 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
|
||||
return cached;
|
||||
}
|
||||
|
||||
if (WeatherBackgroundAssets.TryGetValue(kind, out var uriText))
|
||||
var uriText = HyperOS3WeatherTheme.ResolveBackgroundAsset(ToThemeKind(kind));
|
||||
if (!string.IsNullOrWhiteSpace(uriText))
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -620,104 +594,89 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
|
||||
return gradientBrush;
|
||||
}
|
||||
|
||||
private IBrush ResolveParticleBrush(HyperOS3WeatherVisualKind kind, string fallbackColor)
|
||||
{
|
||||
if (_particleBrushCache.TryGetValue(kind, out var cached))
|
||||
{
|
||||
return cached;
|
||||
}
|
||||
|
||||
var uriText = HyperOS3WeatherTheme.ResolveParticleAsset(kind);
|
||||
if (!string.IsNullOrWhiteSpace(uriText))
|
||||
{
|
||||
try
|
||||
{
|
||||
var uri = new Uri(uriText, UriKind.Absolute);
|
||||
using var stream = AssetLoader.Open(uri);
|
||||
var bitmap = new Bitmap(stream);
|
||||
var imageBrush = new ImageBrush
|
||||
{
|
||||
Source = bitmap,
|
||||
Stretch = Stretch.UniformToFill,
|
||||
AlignmentX = AlignmentX.Center,
|
||||
AlignmentY = AlignmentY.Center
|
||||
};
|
||||
_particleBrushCache[kind] = imageBrush;
|
||||
return imageBrush;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Fall through to solid particle color when the image cannot be loaded.
|
||||
}
|
||||
}
|
||||
|
||||
var solidBrush = CreateSolidBrush(fallbackColor);
|
||||
_particleBrushCache[kind] = solidBrush;
|
||||
return solidBrush;
|
||||
}
|
||||
|
||||
private static WeatherVisualKind ResolveVisualKind(int? weatherCode, bool isNight)
|
||||
{
|
||||
return weatherCode switch
|
||||
return HyperOS3WeatherTheme.ResolveVisualKind(weatherCode, isNight) switch
|
||||
{
|
||||
0 => isNight ? WeatherVisualKind.ClearNight : WeatherVisualKind.ClearDay,
|
||||
1 or 2 => isNight ? WeatherVisualKind.CloudyNight : WeatherVisualKind.CloudyDay,
|
||||
3 or 7 => WeatherVisualKind.RainLight,
|
||||
8 or 9 => WeatherVisualKind.RainHeavy,
|
||||
4 => WeatherVisualKind.Storm,
|
||||
13 or 14 or 15 or 16 => WeatherVisualKind.Snow,
|
||||
18 or 32 => WeatherVisualKind.Fog,
|
||||
_ => isNight ? WeatherVisualKind.CloudyNight : WeatherVisualKind.CloudyDay
|
||||
HyperOS3WeatherVisualKind.ClearDay => WeatherVisualKind.ClearDay,
|
||||
HyperOS3WeatherVisualKind.ClearNight => WeatherVisualKind.ClearNight,
|
||||
HyperOS3WeatherVisualKind.CloudyDay => WeatherVisualKind.CloudyDay,
|
||||
HyperOS3WeatherVisualKind.CloudyNight => WeatherVisualKind.CloudyNight,
|
||||
HyperOS3WeatherVisualKind.RainLight => WeatherVisualKind.RainLight,
|
||||
HyperOS3WeatherVisualKind.RainHeavy => WeatherVisualKind.RainHeavy,
|
||||
HyperOS3WeatherVisualKind.Storm => WeatherVisualKind.Storm,
|
||||
HyperOS3WeatherVisualKind.Snow => WeatherVisualKind.Snow,
|
||||
_ => WeatherVisualKind.Fog
|
||||
};
|
||||
}
|
||||
|
||||
private static WeatherVisualPalette ResolvePalette(WeatherVisualKind kind)
|
||||
{
|
||||
return kind switch
|
||||
{
|
||||
WeatherVisualKind.ClearDay => new WeatherVisualPalette(
|
||||
GradientFrom: "#4F92E8",
|
||||
GradientTo: "#83C5FF",
|
||||
Tint: "#234D87",
|
||||
PrimaryText: "#FFFFFFFF",
|
||||
SecondaryText: "#EEF5FF",
|
||||
ParticleColor: "#00FFFFFF"),
|
||||
WeatherVisualKind.ClearNight => new WeatherVisualPalette(
|
||||
GradientFrom: "#0E2B72",
|
||||
GradientTo: "#193A85",
|
||||
Tint: "#0A1E52",
|
||||
PrimaryText: "#FFFFFFFF",
|
||||
SecondaryText: "#CFE0FF",
|
||||
ParticleColor: "#00FFFFFF"),
|
||||
WeatherVisualKind.CloudyDay => new WeatherVisualPalette(
|
||||
GradientFrom: "#4A72B3",
|
||||
GradientTo: "#6A8EC2",
|
||||
Tint: "#2A487C",
|
||||
PrimaryText: "#FFFFFFFF",
|
||||
SecondaryText: "#EAF2FF",
|
||||
ParticleColor: "#16FFFFFF"),
|
||||
WeatherVisualKind.CloudyNight => new WeatherVisualPalette(
|
||||
GradientFrom: "#102A6B",
|
||||
GradientTo: "#193A80",
|
||||
Tint: "#0B1F51",
|
||||
PrimaryText: "#FFFFFFFF",
|
||||
SecondaryText: "#D5E4FF",
|
||||
ParticleColor: "#24FFFFFF"),
|
||||
WeatherVisualKind.RainLight => new WeatherVisualPalette(
|
||||
GradientFrom: "#32588A",
|
||||
GradientTo: "#4D74A8",
|
||||
Tint: "#1F3454",
|
||||
PrimaryText: "#FFFFFFFF",
|
||||
SecondaryText: "#E6F0FF",
|
||||
ParticleColor: "#88D7E8FF"),
|
||||
WeatherVisualKind.RainHeavy => new WeatherVisualPalette(
|
||||
GradientFrom: "#253F66",
|
||||
GradientTo: "#36567F",
|
||||
Tint: "#17263E",
|
||||
PrimaryText: "#FFFFFFFF",
|
||||
SecondaryText: "#DCE9FF",
|
||||
ParticleColor: "#A2CDE1FF"),
|
||||
WeatherVisualKind.Storm => new WeatherVisualPalette(
|
||||
GradientFrom: "#293A67",
|
||||
GradientTo: "#3A4F78",
|
||||
Tint: "#161E35",
|
||||
PrimaryText: "#FFFFFFFF",
|
||||
SecondaryText: "#DCE4F8",
|
||||
ParticleColor: "#A8C2D6F2"),
|
||||
WeatherVisualKind.Snow => new WeatherVisualPalette(
|
||||
GradientFrom: "#D1E8FF",
|
||||
GradientTo: "#A7D0F4",
|
||||
Tint: "#607C9D",
|
||||
PrimaryText: "#FF10253D",
|
||||
SecondaryText: "#FF2B435E",
|
||||
ParticleColor: "#CCFFFFFF"),
|
||||
_ => new WeatherVisualPalette(
|
||||
GradientFrom: "#445B7A",
|
||||
GradientTo: "#5B738F",
|
||||
Tint: "#2A3E56",
|
||||
PrimaryText: "#FFFFFFFF",
|
||||
SecondaryText: "#E7EDF6",
|
||||
ParticleColor: "#88E4EDF7")
|
||||
};
|
||||
var palette = HyperOS3WeatherTheme.ResolvePalette(ToThemeKind(kind));
|
||||
return new WeatherVisualPalette(
|
||||
palette.GradientFrom,
|
||||
palette.GradientTo,
|
||||
palette.Tint,
|
||||
palette.PrimaryText,
|
||||
palette.SecondaryText,
|
||||
palette.TertiaryText,
|
||||
palette.ParticleColor);
|
||||
}
|
||||
|
||||
private static Symbol ResolveWeatherSymbol(WeatherVisualKind kind)
|
||||
{
|
||||
return HyperOS3WeatherTheme.ResolveWeatherSymbol(ToThemeKind(kind));
|
||||
}
|
||||
|
||||
private static HyperOS3WeatherVisualKind ToThemeKind(WeatherVisualKind kind)
|
||||
{
|
||||
return kind switch
|
||||
{
|
||||
WeatherVisualKind.ClearDay => Symbol.WeatherSunny,
|
||||
WeatherVisualKind.ClearNight => Symbol.WeatherMoon,
|
||||
WeatherVisualKind.CloudyDay => Symbol.WeatherPartlyCloudyDay,
|
||||
WeatherVisualKind.CloudyNight => Symbol.WeatherPartlyCloudyNight,
|
||||
WeatherVisualKind.RainLight => Symbol.WeatherRainShowersDay,
|
||||
WeatherVisualKind.RainHeavy => Symbol.WeatherRain,
|
||||
WeatherVisualKind.Storm => Symbol.WeatherThunderstorm,
|
||||
WeatherVisualKind.Snow => Symbol.WeatherSnow,
|
||||
_ => Symbol.WeatherFog
|
||||
WeatherVisualKind.ClearDay => HyperOS3WeatherVisualKind.ClearDay,
|
||||
WeatherVisualKind.ClearNight => HyperOS3WeatherVisualKind.ClearNight,
|
||||
WeatherVisualKind.CloudyDay => HyperOS3WeatherVisualKind.CloudyDay,
|
||||
WeatherVisualKind.CloudyNight => HyperOS3WeatherVisualKind.CloudyNight,
|
||||
WeatherVisualKind.RainLight => HyperOS3WeatherVisualKind.RainLight,
|
||||
WeatherVisualKind.RainHeavy => HyperOS3WeatherVisualKind.RainHeavy,
|
||||
WeatherVisualKind.Storm => HyperOS3WeatherVisualKind.Storm,
|
||||
WeatherVisualKind.Snow => HyperOS3WeatherVisualKind.Snow,
|
||||
_ => HyperOS3WeatherVisualKind.Fog
|
||||
};
|
||||
}
|
||||
|
||||
@@ -932,18 +891,8 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
|
||||
|
||||
private static string ResolveWeatherIconAccent(Symbol symbol, bool isNightVisual)
|
||||
{
|
||||
return symbol switch
|
||||
{
|
||||
Symbol.WeatherSunny => isNightVisual ? "#FFD978" : "#F7C40A",
|
||||
Symbol.WeatherMoon => "#F3D38C",
|
||||
Symbol.WeatherPartlyCloudyDay => "#75B0FF",
|
||||
Symbol.WeatherPartlyCloudyNight => "#8AB6FF",
|
||||
Symbol.WeatherRainShowersDay => "#9ECBFF",
|
||||
Symbol.WeatherRain => "#8DBDF5",
|
||||
Symbol.WeatherThunderstorm => "#F4D16E",
|
||||
Symbol.WeatherSnow => "#C7E6FF",
|
||||
_ => isNightVisual ? "#D5E2F4" : "#E2ECFA"
|
||||
};
|
||||
var kind = isNightVisual ? HyperOS3WeatherVisualKind.ClearNight : HyperOS3WeatherVisualKind.ClearDay;
|
||||
return HyperOS3WeatherTheme.ResolveIconAccent(kind, symbol);
|
||||
}
|
||||
|
||||
private static string ResolvePreciseDisplayLocation(string? rawName, string languageCode, string fallback)
|
||||
@@ -1092,6 +1041,7 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
|
||||
private void ApplyAdaptiveTypography()
|
||||
{
|
||||
var (layoutWidth, layoutHeight) = ResolveLayoutViewport();
|
||||
var metrics = HyperOS3WeatherTheme.ResolveMetrics(HyperOS3WeatherWidgetKind.MultiDay4x2);
|
||||
var scale = ResolveScale(layoutWidth, layoutHeight);
|
||||
var densityBoost = scale <= 0.55 ? 0.80 : scale <= 0.72 ? 0.88 : scale <= 0.92 ? 0.95 : scale >= 1.45 ? 1.06 : 1.0;
|
||||
var compactness = Math.Clamp((0.88 - scale) / 0.50, 0, 1);
|
||||
@@ -1100,9 +1050,9 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
|
||||
var conditionLength = Math.Max(1, ConditionTextBlock.Text?.Length ?? 2);
|
||||
var conditionCompression = conditionLength >= 12 ? 0.72 : conditionLength >= 8 ? 0.85 : conditionLength >= 6 ? 0.92 : 1.0;
|
||||
|
||||
ContentGrid.RowSpacing = Math.Clamp(layoutHeight * Lerp(0.030, 0.018, compactness), 2, 14);
|
||||
TopRowGrid.ColumnSpacing = Math.Clamp(layoutWidth * 0.014, 3, 14);
|
||||
BottomInfoStack.Spacing = Math.Clamp(layoutHeight * 0.016, 2, 10);
|
||||
ContentGrid.RowSpacing = Math.Clamp(Math.Max(metrics.MainGap * scale, layoutHeight * Lerp(0.030, 0.018, compactness)), 2, 14);
|
||||
TopRowGrid.ColumnSpacing = Math.Clamp(Math.Max(metrics.MainGap * scale, layoutWidth * 0.014), 3, 14);
|
||||
BottomInfoStack.Spacing = Math.Clamp(Math.Max(metrics.SectionGap * scale, layoutHeight * 0.016), 2, 10);
|
||||
BottomInfoStack.Margin = new Thickness(0, 0, 0, Math.Clamp(layoutHeight * 0.018, 0, 10));
|
||||
ConditionIconStack.Spacing = Math.Clamp(layoutWidth * 0.009, 3, 12);
|
||||
RangeTextBlock.Margin = new Thickness(0, 0, 0, Math.Clamp(layoutHeight * 0.020, 0, 12));
|
||||
@@ -1117,12 +1067,12 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
|
||||
var middleBandHeight = Math.Max(24, layoutHeight * 0.30);
|
||||
var bottomBandHeight = Math.Max(22, layoutHeight - topBandHeight - middleBandHeight - (ContentGrid.RowSpacing * 2));
|
||||
|
||||
LocationIcon.FontSize = Math.Min(Math.Clamp(24 * scale * densityBoost, 9, 30), topBandHeight * 0.58);
|
||||
CityTextBlock.FontSize = Math.Min(Math.Clamp(40 * scale * cityCompression * densityBoost, 12, 46), topBandHeight * 0.76);
|
||||
WeatherIconSymbol.FontSize = Math.Min(Math.Clamp(52 * scale * densityBoost, 12, 56), topBandHeight * 0.95);
|
||||
TemperatureTextBlock.FontSize = Math.Min(Math.Clamp(134 * scale * densityBoost, 26, 138), middleBandHeight * 0.92);
|
||||
ConditionTextBlock.FontSize = Math.Min(Math.Clamp(31 * scale * conditionCompression * densityBoost, 9, 40), topBandHeight * 0.70);
|
||||
RangeTextBlock.FontSize = Math.Min(Math.Clamp(34 * scale * densityBoost, 9, 42), middleBandHeight * 0.50);
|
||||
LocationIcon.FontSize = Math.Min(Math.Clamp((metrics.IconFont * 0.6) * scale * densityBoost, 9, 30), topBandHeight * 0.58);
|
||||
CityTextBlock.FontSize = Math.Min(Math.Clamp((metrics.PrimaryTextFont * 1.42) * scale * cityCompression * densityBoost, 12, 46), topBandHeight * 0.76);
|
||||
WeatherIconSymbol.FontSize = Math.Min(Math.Clamp((metrics.IconFont * 1.02) * scale * densityBoost, 12, 56), topBandHeight * 0.95);
|
||||
TemperatureTextBlock.FontSize = Math.Min(Math.Clamp((metrics.PrimaryTemperatureFont * 1.40) * scale * densityBoost, 26, 138), middleBandHeight * 0.92);
|
||||
ConditionTextBlock.FontSize = Math.Min(Math.Clamp((metrics.PrimaryTextFont * 1.10) * scale * conditionCompression * densityBoost, 9, 40), topBandHeight * 0.70);
|
||||
RangeTextBlock.FontSize = Math.Min(Math.Clamp((metrics.SecondaryTextFont * 1.42) * scale * densityBoost, 9, 42), middleBandHeight * 0.50);
|
||||
TemperatureTextBlock.Margin = new Thickness(0, Math.Clamp(layoutHeight * 0.008, 0, 6), 0, Math.Clamp(layoutHeight * 0.012, 0, 8));
|
||||
|
||||
var weightProgress = Math.Clamp((scale - 0.34) / 1.18, 0, 1);
|
||||
@@ -1161,13 +1111,13 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
|
||||
var hourlyIconMaxByHeight = Math.Clamp(forecastLineHeight * 1.05, 8, 28);
|
||||
|
||||
var hourlyTimeSize = Math.Min(
|
||||
Math.Clamp(23 * scale * densityBoost, 8, 30),
|
||||
Math.Clamp((metrics.CaptionFont * 1.15) * scale * densityBoost, 8, 30),
|
||||
Math.Min(hourlyTimeMaxByWidth, hourlyTimeMaxByHeight));
|
||||
var hourlyIconSize = Math.Min(
|
||||
Math.Clamp(30 * scale * densityBoost, 8, 34),
|
||||
Math.Clamp((metrics.IconFont * 0.64) * scale * densityBoost, 8, 34),
|
||||
Math.Min(hourlyIconMaxByWidth, hourlyIconMaxByHeight));
|
||||
var hourlyTempSize = Math.Min(
|
||||
Math.Clamp(30 * scale * densityBoost, 8, 32),
|
||||
Math.Clamp((metrics.SecondaryTextFont * 1.24) * scale * densityBoost, 8, 32),
|
||||
Math.Min(hourlyTempMaxByWidth, hourlyTempMaxByHeight));
|
||||
for (var i = 0; i < _hourlyTimeBlocks.Length; i++)
|
||||
{
|
||||
@@ -1197,81 +1147,25 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
|
||||
|
||||
private WeatherMotionProfile ResolveMotionProfile(WeatherVisualKind kind)
|
||||
{
|
||||
return kind switch
|
||||
{
|
||||
WeatherVisualKind.ClearDay => new WeatherMotionProfile(
|
||||
DriftX: 8.0, DriftY: 4.0, ZoomBase: 1.055, ZoomAmplitude: 0.012,
|
||||
MotionOpacityBase: 0.22, MotionOpacityPulse: 0.05,
|
||||
LightOpacityBase: 0.68, LightOpacityPulse: 0.08,
|
||||
ShadeOpacityBase: 0.72, ShadeOpacityPulse: 0.03,
|
||||
PhaseStep: 0.015, ParticleCount: 0,
|
||||
ParticleSpeedMin: 0, ParticleSpeedMax: 0,
|
||||
ParticleLengthMin: 0, ParticleLengthMax: 0, ParticleDriftPerTick: 0),
|
||||
WeatherVisualKind.ClearNight => new WeatherMotionProfile(
|
||||
DriftX: 10.0, DriftY: 6.0, ZoomBase: 1.060, ZoomAmplitude: 0.014,
|
||||
MotionOpacityBase: 0.28, MotionOpacityPulse: 0.06,
|
||||
LightOpacityBase: 0.58, LightOpacityPulse: 0.07,
|
||||
ShadeOpacityBase: 0.82, ShadeOpacityPulse: 0.04,
|
||||
PhaseStep: 0.018, ParticleCount: 0,
|
||||
ParticleSpeedMin: 0, ParticleSpeedMax: 0,
|
||||
ParticleLengthMin: 0, ParticleLengthMax: 0, ParticleDriftPerTick: 0),
|
||||
WeatherVisualKind.CloudyDay => new WeatherMotionProfile(
|
||||
DriftX: 12.0, DriftY: 7.0, ZoomBase: 1.060, ZoomAmplitude: 0.013,
|
||||
MotionOpacityBase: 0.32, MotionOpacityPulse: 0.06,
|
||||
LightOpacityBase: 0.62, LightOpacityPulse: 0.07,
|
||||
ShadeOpacityBase: 0.80, ShadeOpacityPulse: 0.03,
|
||||
PhaseStep: 0.020, ParticleCount: 6,
|
||||
ParticleSpeedMin: 0.30, ParticleSpeedMax: 0.70,
|
||||
ParticleLengthMin: 14, ParticleLengthMax: 28, ParticleDriftPerTick: 0.10),
|
||||
WeatherVisualKind.CloudyNight => new WeatherMotionProfile(
|
||||
DriftX: 14.0, DriftY: 8.0, ZoomBase: 1.065, ZoomAmplitude: 0.013,
|
||||
MotionOpacityBase: 0.34, MotionOpacityPulse: 0.07,
|
||||
LightOpacityBase: 0.54, LightOpacityPulse: 0.06,
|
||||
ShadeOpacityBase: 0.85, ShadeOpacityPulse: 0.03,
|
||||
PhaseStep: 0.021, ParticleCount: 8,
|
||||
ParticleSpeedMin: 0.35, ParticleSpeedMax: 0.80,
|
||||
ParticleLengthMin: 16, ParticleLengthMax: 30, ParticleDriftPerTick: 0.12),
|
||||
WeatherVisualKind.RainLight => new WeatherMotionProfile(
|
||||
DriftX: 6.0, DriftY: 10.0, ZoomBase: 1.050, ZoomAmplitude: 0.010,
|
||||
MotionOpacityBase: 0.30, MotionOpacityPulse: 0.08,
|
||||
LightOpacityBase: 0.50, LightOpacityPulse: 0.04,
|
||||
ShadeOpacityBase: 0.84, ShadeOpacityPulse: 0.04,
|
||||
PhaseStep: 0.030, ParticleCount: 18,
|
||||
ParticleSpeedMin: 1.80, ParticleSpeedMax: 3.20,
|
||||
ParticleLengthMin: 14, ParticleLengthMax: 26, ParticleDriftPerTick: 0.70),
|
||||
WeatherVisualKind.RainHeavy => new WeatherMotionProfile(
|
||||
DriftX: 5.0, DriftY: 11.0, ZoomBase: 1.045, ZoomAmplitude: 0.010,
|
||||
MotionOpacityBase: 0.34, MotionOpacityPulse: 0.10,
|
||||
LightOpacityBase: 0.42, LightOpacityPulse: 0.03,
|
||||
ShadeOpacityBase: 0.88, ShadeOpacityPulse: 0.05,
|
||||
PhaseStep: 0.036, ParticleCount: 30,
|
||||
ParticleSpeedMin: 2.80, ParticleSpeedMax: 4.80,
|
||||
ParticleLengthMin: 18, ParticleLengthMax: 34, ParticleDriftPerTick: 0.92),
|
||||
WeatherVisualKind.Storm => new WeatherMotionProfile(
|
||||
DriftX: 4.0, DriftY: 12.0, ZoomBase: 1.042, ZoomAmplitude: 0.012,
|
||||
MotionOpacityBase: 0.38, MotionOpacityPulse: 0.12,
|
||||
LightOpacityBase: 0.36, LightOpacityPulse: 0.02,
|
||||
ShadeOpacityBase: 0.91, ShadeOpacityPulse: 0.04,
|
||||
PhaseStep: 0.042, ParticleCount: 34,
|
||||
ParticleSpeedMin: 3.60, ParticleSpeedMax: 5.80,
|
||||
ParticleLengthMin: 20, ParticleLengthMax: 36, ParticleDriftPerTick: 1.08),
|
||||
WeatherVisualKind.Snow => new WeatherMotionProfile(
|
||||
DriftX: 9.0, DriftY: 7.0, ZoomBase: 1.055, ZoomAmplitude: 0.012,
|
||||
MotionOpacityBase: 0.28, MotionOpacityPulse: 0.06,
|
||||
LightOpacityBase: 0.74, LightOpacityPulse: 0.08,
|
||||
ShadeOpacityBase: 0.68, ShadeOpacityPulse: 0.03,
|
||||
PhaseStep: 0.020, ParticleCount: 24,
|
||||
ParticleSpeedMin: 0.60, ParticleSpeedMax: 1.60,
|
||||
ParticleLengthMin: 3.0, ParticleLengthMax: 8.5, ParticleDriftPerTick: 0.24),
|
||||
_ => new WeatherMotionProfile(
|
||||
DriftX: 7.0, DriftY: 5.0, ZoomBase: 1.050, ZoomAmplitude: 0.011,
|
||||
MotionOpacityBase: 0.30, MotionOpacityPulse: 0.05,
|
||||
LightOpacityBase: 0.58, LightOpacityPulse: 0.05,
|
||||
ShadeOpacityBase: 0.86, ShadeOpacityPulse: 0.03,
|
||||
PhaseStep: 0.018, ParticleCount: 10,
|
||||
ParticleSpeedMin: 0.25, ParticleSpeedMax: 0.70,
|
||||
ParticleLengthMin: 16, ParticleLengthMax: 34, ParticleDriftPerTick: 0.12)
|
||||
};
|
||||
var motion = HyperOS3WeatherTheme.ResolveMotion(ToThemeKind(kind));
|
||||
return new WeatherMotionProfile(
|
||||
motion.DriftX,
|
||||
motion.DriftY,
|
||||
motion.ZoomBase,
|
||||
motion.ZoomAmplitude,
|
||||
motion.MotionOpacityBase,
|
||||
motion.MotionOpacityPulse,
|
||||
motion.LightOpacityBase,
|
||||
motion.LightOpacityPulse,
|
||||
motion.ShadeOpacityBase,
|
||||
motion.ShadeOpacityPulse,
|
||||
motion.PhaseStep,
|
||||
motion.ParticleCount,
|
||||
motion.ParticleSpeedMin,
|
||||
motion.ParticleSpeedMax,
|
||||
motion.ParticleLengthMin,
|
||||
motion.ParticleLengthMax,
|
||||
motion.ParticleDriftPerTick);
|
||||
}
|
||||
|
||||
private void ResetAnimationState()
|
||||
|
||||
@@ -55,6 +55,7 @@ public partial class WeatherClockWidget : UserControl, IDesktopComponentWidget,
|
||||
private bool? _isNightModeApplied;
|
||||
private string _languageCode = "zh-CN";
|
||||
private Symbol _activeWeatherSymbol = Symbol.WeatherPartlyCloudyDay;
|
||||
private HyperOS3WeatherVisualKind _activeVisualKind = HyperOS3WeatherVisualKind.CloudyDay;
|
||||
|
||||
public WeatherClockWidget()
|
||||
{
|
||||
@@ -104,6 +105,7 @@ public partial class WeatherClockWidget : UserControl, IDesktopComponentWidget,
|
||||
public void ApplyCellSize(double cellSize)
|
||||
{
|
||||
_currentCellSize = Math.Max(1, cellSize);
|
||||
var metrics = HyperOS3WeatherTheme.ResolveMetrics(HyperOS3WeatherWidgetKind.WeatherClock2x1);
|
||||
var scale = ResolveScale();
|
||||
var targetHeight = Bounds.Height > 1
|
||||
? Math.Clamp(Bounds.Height, 38, 160)
|
||||
@@ -111,9 +113,10 @@ public partial class WeatherClockWidget : UserControl, IDesktopComponentWidget,
|
||||
var targetWidth = Bounds.Width > 1
|
||||
? Math.Clamp(Bounds.Width, 48, 520)
|
||||
: Math.Clamp(_currentCellSize * 2.15, 88, 260);
|
||||
var compactness = Math.Clamp((170 - targetWidth) / 78d, 0, 1);
|
||||
var compactFactor = Lerp(1, 0.72, compactness);
|
||||
var cornerRadius = Math.Clamp(targetHeight * 0.40, 15, 36);
|
||||
var compactness = Math.Clamp((176 - targetWidth) / 86d, 0, 1);
|
||||
var ultraCompact = targetWidth < 126 || targetHeight < 46;
|
||||
var compactFactor = Lerp(1, ultraCompact ? 0.64 : 0.72, compactness);
|
||||
var cornerRadius = Math.Clamp(targetHeight * metrics.CornerRadiusScale, 15, 36);
|
||||
|
||||
var horizontalPadding = Math.Clamp(targetHeight * Lerp(0.18, 0.12, compactness), 5, 30);
|
||||
var verticalPadding = Math.Clamp(targetHeight * Lerp(0.14, 0.10, compactness), 3, 20);
|
||||
@@ -121,31 +124,75 @@ public partial class WeatherClockWidget : UserControl, IDesktopComponentWidget,
|
||||
RootBorder.CornerRadius = new CornerRadius(cornerRadius);
|
||||
RootBorder.Padding = new Thickness(horizontalPadding, verticalPadding, horizontalPadding, verticalPadding);
|
||||
|
||||
var columnSpacing = Math.Clamp(targetHeight * Lerp(0.16, 0.08, compactness), 3, 22);
|
||||
ContentGrid.ColumnSpacing = columnSpacing;
|
||||
var columnSpacing = Math.Clamp(targetHeight * Lerp(0.16, 0.08, compactness), 2, 22);
|
||||
LeftStack.Spacing = Math.Clamp(targetHeight * Lerp(0.06, 0.04, compactness), 1.5, 10);
|
||||
DateWeatherStack.Spacing = Math.Clamp(targetHeight * Lerp(0.10, 0.06, compactness), 3, 14);
|
||||
|
||||
TimeTextBlock.FontSize = Math.Clamp(31 * scale * compactFactor, 14, 62);
|
||||
DateTextBlock.FontSize = Math.Clamp(15.5 * scale * compactFactor, 9, 30);
|
||||
WeatherIconSymbol.FontSize = Math.Clamp(17 * scale * compactFactor, 10, 32);
|
||||
var contentHeight = Math.Max(24, targetHeight - (verticalPadding * 2));
|
||||
var contentWidth = Math.Max(48, targetWidth - (horizontalPadding * 2));
|
||||
var minimumLeftWidth = Math.Clamp(contentWidth * Lerp(0.56, 0.64, compactness), ultraCompact ? 34 : 52, 360);
|
||||
var maxDialByWidth = Math.Max(0, contentWidth - minimumLeftWidth - columnSpacing);
|
||||
var dialByHeight = contentHeight * Lerp(0.94, 0.82, compactness);
|
||||
var dialMinSize = ultraCompact ? 14 : 20;
|
||||
var dialSize = Math.Min(dialByHeight, maxDialByWidth);
|
||||
if (dialSize < dialMinSize && maxDialByWidth >= dialMinSize * 0.8)
|
||||
{
|
||||
dialSize = dialMinSize;
|
||||
}
|
||||
|
||||
dialSize = Math.Clamp(dialSize, 0, 140);
|
||||
var showDial = dialSize >= 12;
|
||||
if (!showDial)
|
||||
{
|
||||
dialSize = 0;
|
||||
columnSpacing = 0;
|
||||
}
|
||||
|
||||
var leftContentWidth = Math.Max(0, contentWidth - (showDial ? dialSize + columnSpacing : 0));
|
||||
if (showDial && leftContentWidth < 26)
|
||||
{
|
||||
var fittedDial = Math.Max(12, Math.Min(dialSize, Math.Max(0, contentWidth - columnSpacing - 26)));
|
||||
dialSize = fittedDial;
|
||||
leftContentWidth = Math.Max(0, contentWidth - dialSize - columnSpacing);
|
||||
if (leftContentWidth < 20)
|
||||
{
|
||||
showDial = false;
|
||||
dialSize = 0;
|
||||
columnSpacing = 0;
|
||||
leftContentWidth = contentWidth;
|
||||
}
|
||||
}
|
||||
|
||||
ContentGrid.ColumnSpacing = showDial ? columnSpacing : 0;
|
||||
if (ContentGrid.ColumnDefinitions.Count >= 2)
|
||||
{
|
||||
ContentGrid.ColumnDefinitions[0].Width = new GridLength(leftContentWidth, GridUnitType.Pixel);
|
||||
ContentGrid.ColumnDefinitions[1].Width = new GridLength(showDial ? dialSize : 0, GridUnitType.Pixel);
|
||||
}
|
||||
|
||||
var leftWidthFactor = Math.Clamp(leftContentWidth / 122d, 0.48, 1.35);
|
||||
TimeTextBlock.FontSize = Math.Clamp((metrics.PrimaryTemperatureFont * 0.74) * scale * compactFactor * leftWidthFactor, 10, 62);
|
||||
DateTextBlock.FontSize = Math.Clamp(metrics.SecondaryTextFont * scale * compactFactor * leftWidthFactor, 8, 30);
|
||||
WeatherIconSymbol.FontSize = Math.Clamp(metrics.IconFont * scale * compactFactor * leftWidthFactor, 9, 32);
|
||||
|
||||
TimeTextBlock.FontWeight = ToVariableWeight(Lerp(620, 760, Math.Clamp((scale - 0.68) / 1.35, 0, 1)));
|
||||
DateTextBlock.FontWeight = ToVariableWeight(Lerp(540, 680, Math.Clamp((scale - 0.68) / 1.35, 0, 1)));
|
||||
|
||||
var contentHeight = Math.Max(24, targetHeight - (verticalPadding * 2));
|
||||
var contentWidth = Math.Max(48, targetWidth - (horizontalPadding * 2));
|
||||
var minimumLeftWidth = Math.Clamp(contentWidth * Lerp(0.56, 0.64, compactness), 52, 360);
|
||||
var maxDialByWidth = Math.Max(18, contentWidth - minimumLeftWidth - columnSpacing);
|
||||
var dialByHeight = contentHeight * Lerp(0.94, 0.84, compactness);
|
||||
var dialSize = Math.Clamp(Math.Min(dialByHeight, maxDialByWidth), 20, 140);
|
||||
var leftContentWidth = Math.Max(26, contentWidth - dialSize - columnSpacing);
|
||||
|
||||
LeftStack.Width = leftContentWidth;
|
||||
LeftStack.MaxWidth = leftContentWidth;
|
||||
DateWeatherStack.MaxWidth = leftContentWidth;
|
||||
TimeTextBlock.MaxWidth = leftContentWidth;
|
||||
DateTextBlock.MaxWidth = Math.Max(18, leftContentWidth - WeatherIconSymbol.FontSize - DateWeatherStack.Spacing);
|
||||
|
||||
var showDateLine = leftContentWidth >= Math.Max(40, TimeTextBlock.FontSize * 1.72);
|
||||
DateWeatherStack.IsVisible = showDateLine;
|
||||
WeatherIconSymbol.IsVisible = showDateLine && leftContentWidth >= Math.Max(56, DateTextBlock.FontSize * 2.4);
|
||||
|
||||
var dateReservedWidth = WeatherIconSymbol.IsVisible
|
||||
? WeatherIconSymbol.FontSize + DateWeatherStack.Spacing
|
||||
: 0;
|
||||
DateTextBlock.MaxWidth = Math.Max(12, leftContentWidth - dateReservedWidth);
|
||||
|
||||
AnalogDialBorder.IsVisible = showDial;
|
||||
AnalogDialBorder.Width = dialSize;
|
||||
AnalogDialBorder.Height = dialSize;
|
||||
AnalogDialBorder.CornerRadius = new CornerRadius(dialSize / 2d);
|
||||
@@ -264,17 +311,19 @@ public partial class WeatherClockWidget : UserControl, IDesktopComponentWidget,
|
||||
private void ApplyWeatherSnapshot(WeatherSnapshot snapshot)
|
||||
{
|
||||
var isNight = ResolveIsNight(snapshot);
|
||||
_activeWeatherSymbol = ResolveWeatherSymbol(snapshot.Current.WeatherCode, isNight);
|
||||
_activeVisualKind = HyperOS3WeatherTheme.ResolveVisualKind(snapshot.Current.WeatherCode, isNight);
|
||||
_activeWeatherSymbol = HyperOS3WeatherTheme.ResolveWeatherSymbol(_activeVisualKind);
|
||||
WeatherIconSymbol.Symbol = _activeWeatherSymbol;
|
||||
WeatherIconSymbol.Foreground = CreateBrush(ResolveWeatherIconColor(_activeWeatherSymbol, isNight));
|
||||
WeatherIconSymbol.Foreground = CreateBrush(HyperOS3WeatherTheme.ResolveIconAccent(_activeVisualKind, _activeWeatherSymbol));
|
||||
}
|
||||
|
||||
private void ApplyDefaultWeatherIcon()
|
||||
{
|
||||
var isNight = IsNightNow();
|
||||
_activeWeatherSymbol = isNight ? Symbol.WeatherMoon : Symbol.WeatherPartlyCloudyDay;
|
||||
_activeVisualKind = isNight ? HyperOS3WeatherVisualKind.ClearNight : HyperOS3WeatherVisualKind.CloudyDay;
|
||||
_activeWeatherSymbol = HyperOS3WeatherTheme.ResolveWeatherSymbol(_activeVisualKind);
|
||||
WeatherIconSymbol.Symbol = _activeWeatherSymbol;
|
||||
WeatherIconSymbol.Foreground = CreateBrush(ResolveWeatherIconColor(_activeWeatherSymbol, isNight));
|
||||
WeatherIconSymbol.Foreground = CreateBrush(HyperOS3WeatherTheme.ResolveIconAccent(_activeVisualKind, _activeWeatherSymbol));
|
||||
}
|
||||
|
||||
private void UpdateClockVisual()
|
||||
@@ -381,7 +430,7 @@ public partial class WeatherClockWidget : UserControl, IDesktopComponentWidget,
|
||||
CenterDotInner.Fill = CreateBrush("#1A74F2");
|
||||
|
||||
BuildTicks(isNightMode);
|
||||
WeatherIconSymbol.Foreground = CreateBrush(ResolveWeatherIconColor(_activeWeatherSymbol, isNightMode));
|
||||
WeatherIconSymbol.Foreground = CreateBrush(HyperOS3WeatherTheme.ResolveIconAccent(_activeVisualKind, _activeWeatherSymbol));
|
||||
}
|
||||
|
||||
private WeatherClockConfig LoadConfig()
|
||||
@@ -442,26 +491,10 @@ public partial class WeatherClockWidget : UserControl, IDesktopComponentWidget,
|
||||
|
||||
private bool ResolveIsNight(WeatherSnapshot snapshot)
|
||||
{
|
||||
if (snapshot.ObservationTime.HasValue)
|
||||
{
|
||||
var observed = snapshot.ObservationTime.Value;
|
||||
try
|
||||
{
|
||||
if (_timeZoneService is not null)
|
||||
{
|
||||
var zoned = TimeZoneInfo.ConvertTime(observed, _timeZoneService.CurrentTimeZone);
|
||||
return zoned.Hour < 6 || zoned.Hour >= 18;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Fall through to local observation.
|
||||
}
|
||||
|
||||
return observed.Hour < 6 || observed.Hour >= 18;
|
||||
}
|
||||
|
||||
return IsNightNow();
|
||||
return HyperOS3WeatherTheme.ResolveIsNightPreferred(
|
||||
snapshot,
|
||||
_timeZoneService?.CurrentTimeZone,
|
||||
_timeZoneService?.GetCurrentTime() ?? DateTime.Now);
|
||||
}
|
||||
|
||||
private bool IsNightNow()
|
||||
@@ -491,37 +524,6 @@ public partial class WeatherClockWidget : UserControl, IDesktopComponentWidget,
|
||||
return false;
|
||||
}
|
||||
|
||||
private static Symbol ResolveWeatherSymbol(int? weatherCode, bool isNight)
|
||||
{
|
||||
return weatherCode switch
|
||||
{
|
||||
0 => isNight ? Symbol.WeatherMoon : Symbol.WeatherSunny,
|
||||
1 or 2 => isNight ? Symbol.WeatherPartlyCloudyNight : Symbol.WeatherPartlyCloudyDay,
|
||||
3 or 7 => Symbol.WeatherRainShowersDay,
|
||||
8 or 9 => Symbol.WeatherRain,
|
||||
4 => Symbol.WeatherThunderstorm,
|
||||
13 or 14 or 15 or 16 => Symbol.WeatherSnow,
|
||||
18 or 32 => Symbol.WeatherFog,
|
||||
_ => isNight ? Symbol.WeatherPartlyCloudyNight : Symbol.WeatherPartlyCloudyDay
|
||||
};
|
||||
}
|
||||
|
||||
private static string ResolveWeatherIconColor(Symbol symbol, bool isNightMode)
|
||||
{
|
||||
return symbol switch
|
||||
{
|
||||
Symbol.WeatherSunny => isNightMode ? "#FFD978" : "#F7B500",
|
||||
Symbol.WeatherMoon => "#F6D98F",
|
||||
Symbol.WeatherPartlyCloudyDay => "#5A9CFF",
|
||||
Symbol.WeatherPartlyCloudyNight => "#8AB6FF",
|
||||
Symbol.WeatherRainShowersDay => "#5F96E8",
|
||||
Symbol.WeatherRain => "#4B84DA",
|
||||
Symbol.WeatherThunderstorm => "#F1C24D",
|
||||
Symbol.WeatherSnow => "#8EBFE5",
|
||||
_ => isNightMode ? "#A9BDD7" : "#93A2B8"
|
||||
};
|
||||
}
|
||||
|
||||
private static void SetHandGeometry(Line hand, double angleDeg, double forwardLength, double backwardLength)
|
||||
{
|
||||
var radians = (angleDeg - 90) * Math.PI / 180d;
|
||||
|
||||
@@ -74,20 +74,6 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, ITime
|
||||
double Latitude,
|
||||
double Longitude);
|
||||
|
||||
private static readonly IReadOnlyDictionary<WeatherVisualKind, string> WeatherBackgroundAssets =
|
||||
new Dictionary<WeatherVisualKind, string>
|
||||
{
|
||||
[WeatherVisualKind.ClearDay] = "avares://LanMontainDesktop/Assets/Weather/clear_day.jpg",
|
||||
[WeatherVisualKind.ClearNight] = "avares://LanMontainDesktop/Assets/Weather/clear_night.jpg",
|
||||
[WeatherVisualKind.CloudyDay] = "avares://LanMontainDesktop/Assets/Weather/cloudy_day.jpg",
|
||||
[WeatherVisualKind.CloudyNight] = "avares://LanMontainDesktop/Assets/Weather/cloudy_night.jpg",
|
||||
[WeatherVisualKind.RainLight] = "avares://LanMontainDesktop/Assets/Weather/rain_light.jpg",
|
||||
[WeatherVisualKind.RainHeavy] = "avares://LanMontainDesktop/Assets/Weather/rain_heavy.jpg",
|
||||
[WeatherVisualKind.Storm] = "avares://LanMontainDesktop/Assets/Weather/storm_dark.jpg",
|
||||
[WeatherVisualKind.Snow] = "avares://LanMontainDesktop/Assets/Weather/snow_soft.jpg",
|
||||
[WeatherVisualKind.Fog] = "avares://LanMontainDesktop/Assets/Weather/fog_haze.jpg"
|
||||
};
|
||||
|
||||
private static readonly IWeatherInfoService DefaultWeatherInfoService = new XiaomiWeatherService();
|
||||
|
||||
private readonly DispatcherTimer _refreshTimer = new()
|
||||
@@ -103,6 +89,7 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, ITime
|
||||
private readonly AppSettingsService _settingsService = new();
|
||||
private readonly LocalizationService _localizationService = new();
|
||||
private readonly Dictionary<WeatherVisualKind, IBrush> _backgroundBrushCache = new();
|
||||
private readonly Dictionary<HyperOS3WeatherVisualKind, IBrush> _particleBrushCache = new();
|
||||
private readonly List<Border> _particleVisuals = new();
|
||||
private readonly List<ParticleState> _particleStates = new();
|
||||
private readonly Random _particleRandom = new();
|
||||
@@ -166,7 +153,10 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, ITime
|
||||
{
|
||||
_currentCellSize = Math.Max(1, cellSize);
|
||||
var scale = ResolveScale();
|
||||
var cornerRadius = Math.Clamp(_currentCellSize * 0.45, 24, 44);
|
||||
var metrics = HyperOS3WeatherTheme.ResolveMetrics(HyperOS3WeatherWidgetKind.Realtime2x2);
|
||||
var cornerRadius = Math.Clamp(_currentCellSize * metrics.CornerRadiusScale, 24, 44);
|
||||
var horizontalPadding = Math.Clamp(_currentCellSize * metrics.HorizontalPaddingScale, 12, 24);
|
||||
var verticalPadding = Math.Clamp(_currentCellSize * metrics.VerticalPaddingScale, 12, 24);
|
||||
|
||||
RootBorder.CornerRadius = new CornerRadius(cornerRadius);
|
||||
BackgroundImageLayer.CornerRadius = new CornerRadius(cornerRadius);
|
||||
@@ -174,7 +164,9 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, ITime
|
||||
BackgroundTintLayer.CornerRadius = new CornerRadius(cornerRadius);
|
||||
BackgroundLightLayer.CornerRadius = new CornerRadius(cornerRadius);
|
||||
BackgroundShadeLayer.CornerRadius = new CornerRadius(cornerRadius);
|
||||
ContentPaddingBorder.Padding = new Thickness(Math.Clamp(18 * scale, 12, 24));
|
||||
ContentPaddingBorder.Padding = new Thickness(
|
||||
Math.Clamp(horizontalPadding * scale, 12, 24),
|
||||
Math.Clamp(verticalPadding * scale, 12, 24));
|
||||
ApplyAdaptiveTypography();
|
||||
ResetParticles();
|
||||
}
|
||||
@@ -257,26 +249,10 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, ITime
|
||||
|
||||
private bool ResolveIsNight(WeatherSnapshot snapshot)
|
||||
{
|
||||
if (snapshot.ObservationTime.HasValue)
|
||||
{
|
||||
var observed = snapshot.ObservationTime.Value;
|
||||
try
|
||||
{
|
||||
if (_timeZoneService is not null)
|
||||
{
|
||||
var zoned = TimeZoneInfo.ConvertTime(observed, _timeZoneService.CurrentTimeZone);
|
||||
return zoned.Hour < 6 || zoned.Hour >= 18;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// fall through to local clock
|
||||
}
|
||||
|
||||
return observed.Hour < 6 || observed.Hour >= 18;
|
||||
}
|
||||
|
||||
return IsNightNow();
|
||||
return HyperOS3WeatherTheme.ResolveIsNightPreferred(
|
||||
snapshot,
|
||||
_timeZoneService?.CurrentTimeZone,
|
||||
_timeZoneService?.GetCurrentTime() ?? DateTime.Now);
|
||||
}
|
||||
|
||||
private bool IsNightNow()
|
||||
@@ -479,7 +455,7 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, ITime
|
||||
|
||||
var primary = CreateSolidBrush(palette.PrimaryText);
|
||||
var secondary = CreateSolidBrush(palette.SecondaryText);
|
||||
var particleBrush = CreateSolidBrush(palette.ParticleColor);
|
||||
var particleBrush = ResolveParticleBrush(ToThemeKind(kind), palette.ParticleColor);
|
||||
LocationIcon.Foreground = primary;
|
||||
CityTextBlock.Foreground = primary;
|
||||
TemperatureTextBlock.Foreground = primary;
|
||||
@@ -503,7 +479,8 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, ITime
|
||||
return cached;
|
||||
}
|
||||
|
||||
if (WeatherBackgroundAssets.TryGetValue(kind, out var uriText))
|
||||
var uriText = HyperOS3WeatherTheme.ResolveBackgroundAsset(ToThemeKind(kind));
|
||||
if (!string.IsNullOrWhiteSpace(uriText))
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -531,104 +508,88 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, ITime
|
||||
return gradientBrush;
|
||||
}
|
||||
|
||||
private IBrush ResolveParticleBrush(HyperOS3WeatherVisualKind kind, string fallbackColor)
|
||||
{
|
||||
if (_particleBrushCache.TryGetValue(kind, out var cached))
|
||||
{
|
||||
return cached;
|
||||
}
|
||||
|
||||
var uriText = HyperOS3WeatherTheme.ResolveParticleAsset(kind);
|
||||
if (!string.IsNullOrWhiteSpace(uriText))
|
||||
{
|
||||
try
|
||||
{
|
||||
var uri = new Uri(uriText, UriKind.Absolute);
|
||||
using var stream = AssetLoader.Open(uri);
|
||||
var bitmap = new Bitmap(stream);
|
||||
var imageBrush = new ImageBrush
|
||||
{
|
||||
Source = bitmap,
|
||||
Stretch = Stretch.UniformToFill,
|
||||
AlignmentX = AlignmentX.Center,
|
||||
AlignmentY = AlignmentY.Center
|
||||
};
|
||||
_particleBrushCache[kind] = imageBrush;
|
||||
return imageBrush;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Fall through to solid particle color when the image cannot be loaded.
|
||||
}
|
||||
}
|
||||
|
||||
var solidBrush = CreateSolidBrush(fallbackColor);
|
||||
_particleBrushCache[kind] = solidBrush;
|
||||
return solidBrush;
|
||||
}
|
||||
|
||||
private static WeatherVisualKind ResolveVisualKind(int? weatherCode, bool isNight)
|
||||
{
|
||||
return weatherCode switch
|
||||
return HyperOS3WeatherTheme.ResolveVisualKind(weatherCode, isNight) switch
|
||||
{
|
||||
0 => isNight ? WeatherVisualKind.ClearNight : WeatherVisualKind.ClearDay,
|
||||
1 or 2 => isNight ? WeatherVisualKind.CloudyNight : WeatherVisualKind.CloudyDay,
|
||||
3 or 7 => WeatherVisualKind.RainLight,
|
||||
8 or 9 => WeatherVisualKind.RainHeavy,
|
||||
4 => WeatherVisualKind.Storm,
|
||||
13 or 14 or 15 or 16 => WeatherVisualKind.Snow,
|
||||
18 or 32 => WeatherVisualKind.Fog,
|
||||
_ => isNight ? WeatherVisualKind.CloudyNight : WeatherVisualKind.CloudyDay
|
||||
HyperOS3WeatherVisualKind.ClearDay => WeatherVisualKind.ClearDay,
|
||||
HyperOS3WeatherVisualKind.ClearNight => WeatherVisualKind.ClearNight,
|
||||
HyperOS3WeatherVisualKind.CloudyDay => WeatherVisualKind.CloudyDay,
|
||||
HyperOS3WeatherVisualKind.CloudyNight => WeatherVisualKind.CloudyNight,
|
||||
HyperOS3WeatherVisualKind.RainLight => WeatherVisualKind.RainLight,
|
||||
HyperOS3WeatherVisualKind.RainHeavy => WeatherVisualKind.RainHeavy,
|
||||
HyperOS3WeatherVisualKind.Storm => WeatherVisualKind.Storm,
|
||||
HyperOS3WeatherVisualKind.Snow => WeatherVisualKind.Snow,
|
||||
_ => WeatherVisualKind.Fog
|
||||
};
|
||||
}
|
||||
|
||||
private static WeatherVisualPalette ResolvePalette(WeatherVisualKind kind)
|
||||
{
|
||||
return kind switch
|
||||
{
|
||||
WeatherVisualKind.ClearDay => new WeatherVisualPalette(
|
||||
GradientFrom: "#4F92E8",
|
||||
GradientTo: "#83C5FF",
|
||||
Tint: "#234D87",
|
||||
PrimaryText: "#FFFFFFFF",
|
||||
SecondaryText: "#EEF5FF",
|
||||
ParticleColor: "#00FFFFFF"),
|
||||
WeatherVisualKind.ClearNight => new WeatherVisualPalette(
|
||||
GradientFrom: "#0E2B72",
|
||||
GradientTo: "#193A85",
|
||||
Tint: "#0A1E52",
|
||||
PrimaryText: "#FFFFFFFF",
|
||||
SecondaryText: "#CFE0FF",
|
||||
ParticleColor: "#00FFFFFF"),
|
||||
WeatherVisualKind.CloudyDay => new WeatherVisualPalette(
|
||||
GradientFrom: "#4A72B3",
|
||||
GradientTo: "#6A8EC2",
|
||||
Tint: "#2A487C",
|
||||
PrimaryText: "#FFFFFFFF",
|
||||
SecondaryText: "#EAF2FF",
|
||||
ParticleColor: "#16FFFFFF"),
|
||||
WeatherVisualKind.CloudyNight => new WeatherVisualPalette(
|
||||
GradientFrom: "#102A6B",
|
||||
GradientTo: "#193A80",
|
||||
Tint: "#0B1F51",
|
||||
PrimaryText: "#FFFFFFFF",
|
||||
SecondaryText: "#D5E4FF",
|
||||
ParticleColor: "#24FFFFFF"),
|
||||
WeatherVisualKind.RainLight => new WeatherVisualPalette(
|
||||
GradientFrom: "#32588A",
|
||||
GradientTo: "#4D74A8",
|
||||
Tint: "#1F3454",
|
||||
PrimaryText: "#FFFFFFFF",
|
||||
SecondaryText: "#E6F0FF",
|
||||
ParticleColor: "#88D7E8FF"),
|
||||
WeatherVisualKind.RainHeavy => new WeatherVisualPalette(
|
||||
GradientFrom: "#253F66",
|
||||
GradientTo: "#36567F",
|
||||
Tint: "#17263E",
|
||||
PrimaryText: "#FFFFFFFF",
|
||||
SecondaryText: "#DCE9FF",
|
||||
ParticleColor: "#A2CDE1FF"),
|
||||
WeatherVisualKind.Storm => new WeatherVisualPalette(
|
||||
GradientFrom: "#293A67",
|
||||
GradientTo: "#3A4F78",
|
||||
Tint: "#161E35",
|
||||
PrimaryText: "#FFFFFFFF",
|
||||
SecondaryText: "#DCE4F8",
|
||||
ParticleColor: "#A8C2D6F2"),
|
||||
WeatherVisualKind.Snow => new WeatherVisualPalette(
|
||||
GradientFrom: "#D1E8FF",
|
||||
GradientTo: "#A7D0F4",
|
||||
Tint: "#607C9D",
|
||||
PrimaryText: "#FF10253D",
|
||||
SecondaryText: "#FF2B435E",
|
||||
ParticleColor: "#CCFFFFFF"),
|
||||
_ => new WeatherVisualPalette(
|
||||
GradientFrom: "#445B7A",
|
||||
GradientTo: "#5B738F",
|
||||
Tint: "#2A3E56",
|
||||
PrimaryText: "#FFFFFFFF",
|
||||
SecondaryText: "#E7EDF6",
|
||||
ParticleColor: "#88E4EDF7")
|
||||
};
|
||||
var palette = HyperOS3WeatherTheme.ResolvePalette(ToThemeKind(kind));
|
||||
return new WeatherVisualPalette(
|
||||
palette.GradientFrom,
|
||||
palette.GradientTo,
|
||||
palette.Tint,
|
||||
palette.PrimaryText,
|
||||
palette.SecondaryText,
|
||||
palette.ParticleColor);
|
||||
}
|
||||
|
||||
private static Symbol ResolveWeatherSymbol(WeatherVisualKind kind)
|
||||
{
|
||||
return HyperOS3WeatherTheme.ResolveWeatherSymbol(ToThemeKind(kind));
|
||||
}
|
||||
|
||||
private static HyperOS3WeatherVisualKind ToThemeKind(WeatherVisualKind kind)
|
||||
{
|
||||
return kind switch
|
||||
{
|
||||
WeatherVisualKind.ClearDay => Symbol.WeatherSunny,
|
||||
WeatherVisualKind.ClearNight => Symbol.WeatherMoon,
|
||||
WeatherVisualKind.CloudyDay => Symbol.WeatherPartlyCloudyDay,
|
||||
WeatherVisualKind.CloudyNight => Symbol.WeatherPartlyCloudyNight,
|
||||
WeatherVisualKind.RainLight => Symbol.WeatherRainShowersDay,
|
||||
WeatherVisualKind.RainHeavy => Symbol.WeatherRain,
|
||||
WeatherVisualKind.Storm => Symbol.WeatherThunderstorm,
|
||||
WeatherVisualKind.Snow => Symbol.WeatherSnow,
|
||||
_ => Symbol.WeatherFog
|
||||
WeatherVisualKind.ClearDay => HyperOS3WeatherVisualKind.ClearDay,
|
||||
WeatherVisualKind.ClearNight => HyperOS3WeatherVisualKind.ClearNight,
|
||||
WeatherVisualKind.CloudyDay => HyperOS3WeatherVisualKind.CloudyDay,
|
||||
WeatherVisualKind.CloudyNight => HyperOS3WeatherVisualKind.CloudyNight,
|
||||
WeatherVisualKind.RainLight => HyperOS3WeatherVisualKind.RainLight,
|
||||
WeatherVisualKind.RainHeavy => HyperOS3WeatherVisualKind.RainHeavy,
|
||||
WeatherVisualKind.Storm => HyperOS3WeatherVisualKind.Storm,
|
||||
WeatherVisualKind.Snow => HyperOS3WeatherVisualKind.Snow,
|
||||
_ => HyperOS3WeatherVisualKind.Fog
|
||||
};
|
||||
}
|
||||
|
||||
@@ -840,23 +801,24 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, ITime
|
||||
private void ApplyAdaptiveTypography()
|
||||
{
|
||||
var scale = ResolveScale();
|
||||
var metrics = HyperOS3WeatherTheme.ResolveMetrics(HyperOS3WeatherWidgetKind.Realtime2x2);
|
||||
var densityBoost = scale <= 0.70 ? 0.88 : scale <= 0.88 ? 0.94 : scale >= 1.45 ? 1.06 : 1.0;
|
||||
var cityLength = Math.Max(1, CityTextBlock.Text?.Length ?? 2);
|
||||
var cityCompression = cityLength >= 10 ? 0.72 : cityLength >= 7 ? 0.83 : cityLength >= 5 ? 0.92 : 1.0;
|
||||
var conditionLength = Math.Max(1, ConditionTextBlock.Text?.Length ?? 2);
|
||||
var conditionCompression = conditionLength >= 9 ? 0.84 : conditionLength >= 6 ? 0.92 : 1.0;
|
||||
|
||||
ContentGrid.RowSpacing = Math.Clamp(8 * scale, 4, 14);
|
||||
TopRowGrid.ColumnSpacing = Math.Clamp(8 * scale, 4, 12);
|
||||
BottomInfoStack.Spacing = Math.Clamp(4 * scale, 2, 8);
|
||||
ContentGrid.RowSpacing = Math.Clamp(metrics.MainGap * scale, 4, 14);
|
||||
TopRowGrid.ColumnSpacing = Math.Clamp(metrics.MainGap * scale, 4, 12);
|
||||
BottomInfoStack.Spacing = Math.Clamp(metrics.SectionGap * scale, 2, 8);
|
||||
BottomInfoStack.Margin = new Thickness(0, 0, 0, Math.Clamp(10 * scale, 4, 16));
|
||||
|
||||
LocationIcon.FontSize = Math.Clamp(20 * scale * densityBoost, 10, 30);
|
||||
CityTextBlock.FontSize = Math.Clamp(30 * scale * cityCompression * densityBoost, 12, 42);
|
||||
WeatherIconSymbol.FontSize = Math.Clamp(40 * scale * densityBoost, 14, 56);
|
||||
TemperatureTextBlock.FontSize = Math.Clamp(108 * scale * densityBoost, 36, 144);
|
||||
ConditionTextBlock.FontSize = Math.Clamp(30 * scale * conditionCompression * densityBoost, 11, 44);
|
||||
RangeTextBlock.FontSize = Math.Clamp(36 * scale * densityBoost, 12, 50);
|
||||
LocationIcon.FontSize = Math.Clamp((metrics.IconFont * 0.50) * scale * densityBoost, 10, 30);
|
||||
CityTextBlock.FontSize = Math.Clamp(metrics.PrimaryTextFont * scale * cityCompression * densityBoost, 12, 42);
|
||||
WeatherIconSymbol.FontSize = Math.Clamp(metrics.IconFont * scale * densityBoost, 14, 56);
|
||||
TemperatureTextBlock.FontSize = Math.Clamp(metrics.PrimaryTemperatureFont * scale * densityBoost, 36, 144);
|
||||
ConditionTextBlock.FontSize = Math.Clamp(metrics.PrimaryTextFont * scale * conditionCompression * densityBoost, 11, 44);
|
||||
RangeTextBlock.FontSize = Math.Clamp(metrics.SecondaryTextFont * scale * densityBoost, 12, 50);
|
||||
TemperatureTextBlock.Margin = new Thickness(0, Math.Clamp(4 * scale, 1, 8), 0, Math.Clamp(10 * scale, 4, 16));
|
||||
|
||||
CityTextBlock.FontWeight = ToVariableWeight(Lerp(560, 700, Math.Clamp((scale - 0.58) / 1.3, 0, 1)));
|
||||
@@ -877,81 +839,25 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, ITime
|
||||
|
||||
private WeatherMotionProfile ResolveMotionProfile(WeatherVisualKind kind)
|
||||
{
|
||||
return kind switch
|
||||
{
|
||||
WeatherVisualKind.ClearDay => new WeatherMotionProfile(
|
||||
DriftX: 8.0, DriftY: 4.0, ZoomBase: 1.055, ZoomAmplitude: 0.012,
|
||||
MotionOpacityBase: 0.22, MotionOpacityPulse: 0.05,
|
||||
LightOpacityBase: 0.68, LightOpacityPulse: 0.08,
|
||||
ShadeOpacityBase: 0.72, ShadeOpacityPulse: 0.03,
|
||||
PhaseStep: 0.015, ParticleCount: 0,
|
||||
ParticleSpeedMin: 0, ParticleSpeedMax: 0,
|
||||
ParticleLengthMin: 0, ParticleLengthMax: 0, ParticleDriftPerTick: 0),
|
||||
WeatherVisualKind.ClearNight => new WeatherMotionProfile(
|
||||
DriftX: 10.0, DriftY: 6.0, ZoomBase: 1.060, ZoomAmplitude: 0.014,
|
||||
MotionOpacityBase: 0.28, MotionOpacityPulse: 0.06,
|
||||
LightOpacityBase: 0.58, LightOpacityPulse: 0.07,
|
||||
ShadeOpacityBase: 0.82, ShadeOpacityPulse: 0.04,
|
||||
PhaseStep: 0.018, ParticleCount: 0,
|
||||
ParticleSpeedMin: 0, ParticleSpeedMax: 0,
|
||||
ParticleLengthMin: 0, ParticleLengthMax: 0, ParticleDriftPerTick: 0),
|
||||
WeatherVisualKind.CloudyDay => new WeatherMotionProfile(
|
||||
DriftX: 12.0, DriftY: 7.0, ZoomBase: 1.060, ZoomAmplitude: 0.013,
|
||||
MotionOpacityBase: 0.32, MotionOpacityPulse: 0.06,
|
||||
LightOpacityBase: 0.62, LightOpacityPulse: 0.07,
|
||||
ShadeOpacityBase: 0.80, ShadeOpacityPulse: 0.03,
|
||||
PhaseStep: 0.020, ParticleCount: 6,
|
||||
ParticleSpeedMin: 0.30, ParticleSpeedMax: 0.70,
|
||||
ParticleLengthMin: 14, ParticleLengthMax: 28, ParticleDriftPerTick: 0.10),
|
||||
WeatherVisualKind.CloudyNight => new WeatherMotionProfile(
|
||||
DriftX: 14.0, DriftY: 8.0, ZoomBase: 1.065, ZoomAmplitude: 0.013,
|
||||
MotionOpacityBase: 0.34, MotionOpacityPulse: 0.07,
|
||||
LightOpacityBase: 0.54, LightOpacityPulse: 0.06,
|
||||
ShadeOpacityBase: 0.85, ShadeOpacityPulse: 0.03,
|
||||
PhaseStep: 0.021, ParticleCount: 8,
|
||||
ParticleSpeedMin: 0.35, ParticleSpeedMax: 0.80,
|
||||
ParticleLengthMin: 16, ParticleLengthMax: 30, ParticleDriftPerTick: 0.12),
|
||||
WeatherVisualKind.RainLight => new WeatherMotionProfile(
|
||||
DriftX: 6.0, DriftY: 10.0, ZoomBase: 1.050, ZoomAmplitude: 0.010,
|
||||
MotionOpacityBase: 0.30, MotionOpacityPulse: 0.08,
|
||||
LightOpacityBase: 0.50, LightOpacityPulse: 0.04,
|
||||
ShadeOpacityBase: 0.84, ShadeOpacityPulse: 0.04,
|
||||
PhaseStep: 0.030, ParticleCount: 18,
|
||||
ParticleSpeedMin: 1.80, ParticleSpeedMax: 3.20,
|
||||
ParticleLengthMin: 14, ParticleLengthMax: 26, ParticleDriftPerTick: 0.70),
|
||||
WeatherVisualKind.RainHeavy => new WeatherMotionProfile(
|
||||
DriftX: 5.0, DriftY: 11.0, ZoomBase: 1.045, ZoomAmplitude: 0.010,
|
||||
MotionOpacityBase: 0.34, MotionOpacityPulse: 0.10,
|
||||
LightOpacityBase: 0.42, LightOpacityPulse: 0.03,
|
||||
ShadeOpacityBase: 0.88, ShadeOpacityPulse: 0.05,
|
||||
PhaseStep: 0.036, ParticleCount: 30,
|
||||
ParticleSpeedMin: 2.80, ParticleSpeedMax: 4.80,
|
||||
ParticleLengthMin: 18, ParticleLengthMax: 34, ParticleDriftPerTick: 0.92),
|
||||
WeatherVisualKind.Storm => new WeatherMotionProfile(
|
||||
DriftX: 4.0, DriftY: 12.0, ZoomBase: 1.042, ZoomAmplitude: 0.012,
|
||||
MotionOpacityBase: 0.38, MotionOpacityPulse: 0.12,
|
||||
LightOpacityBase: 0.36, LightOpacityPulse: 0.02,
|
||||
ShadeOpacityBase: 0.91, ShadeOpacityPulse: 0.04,
|
||||
PhaseStep: 0.042, ParticleCount: 34,
|
||||
ParticleSpeedMin: 3.60, ParticleSpeedMax: 5.80,
|
||||
ParticleLengthMin: 20, ParticleLengthMax: 36, ParticleDriftPerTick: 1.08),
|
||||
WeatherVisualKind.Snow => new WeatherMotionProfile(
|
||||
DriftX: 9.0, DriftY: 7.0, ZoomBase: 1.055, ZoomAmplitude: 0.012,
|
||||
MotionOpacityBase: 0.28, MotionOpacityPulse: 0.06,
|
||||
LightOpacityBase: 0.74, LightOpacityPulse: 0.08,
|
||||
ShadeOpacityBase: 0.68, ShadeOpacityPulse: 0.03,
|
||||
PhaseStep: 0.020, ParticleCount: 24,
|
||||
ParticleSpeedMin: 0.60, ParticleSpeedMax: 1.60,
|
||||
ParticleLengthMin: 3.0, ParticleLengthMax: 8.5, ParticleDriftPerTick: 0.24),
|
||||
_ => new WeatherMotionProfile(
|
||||
DriftX: 7.0, DriftY: 5.0, ZoomBase: 1.050, ZoomAmplitude: 0.011,
|
||||
MotionOpacityBase: 0.30, MotionOpacityPulse: 0.05,
|
||||
LightOpacityBase: 0.58, LightOpacityPulse: 0.05,
|
||||
ShadeOpacityBase: 0.86, ShadeOpacityPulse: 0.03,
|
||||
PhaseStep: 0.018, ParticleCount: 10,
|
||||
ParticleSpeedMin: 0.25, ParticleSpeedMax: 0.70,
|
||||
ParticleLengthMin: 16, ParticleLengthMax: 34, ParticleDriftPerTick: 0.12)
|
||||
};
|
||||
var motion = HyperOS3WeatherTheme.ResolveMotion(ToThemeKind(kind));
|
||||
return new WeatherMotionProfile(
|
||||
motion.DriftX,
|
||||
motion.DriftY,
|
||||
motion.ZoomBase,
|
||||
motion.ZoomAmplitude,
|
||||
motion.MotionOpacityBase,
|
||||
motion.MotionOpacityPulse,
|
||||
motion.LightOpacityBase,
|
||||
motion.LightOpacityPulse,
|
||||
motion.ShadeOpacityBase,
|
||||
motion.ShadeOpacityPulse,
|
||||
motion.PhaseStep,
|
||||
motion.ParticleCount,
|
||||
motion.ParticleSpeedMin,
|
||||
motion.ParticleSpeedMax,
|
||||
motion.ParticleLengthMin,
|
||||
motion.ParticleLengthMax,
|
||||
motion.ParticleDriftPerTick);
|
||||
}
|
||||
|
||||
private void ResetAnimationState()
|
||||
|
||||
@@ -697,6 +697,12 @@ public partial class MainWindow
|
||||
if (placement.ComponentId == BuiltInComponentIds.Date)
|
||||
{
|
||||
OpenDateComponentSettings();
|
||||
return;
|
||||
}
|
||||
|
||||
if (placement.ComponentId == BuiltInComponentIds.DesktopClassSchedule)
|
||||
{
|
||||
OpenClassScheduleComponentSettings();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -716,6 +722,35 @@ public partial class MainWindow
|
||||
ComponentSettingsWindow.Opacity = 1;
|
||||
}
|
||||
|
||||
private void OpenClassScheduleComponentSettings()
|
||||
{
|
||||
if (ComponentSettingsWindow is null || ComponentSettingsContentHost is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var settingsContent = new ClassScheduleSettingsWindow();
|
||||
settingsContent.SettingsChanged += OnClassScheduleSettingsChanged;
|
||||
ComponentSettingsContentHost.Content = settingsContent;
|
||||
|
||||
ComponentSettingsWindow.IsVisible = true;
|
||||
ComponentSettingsWindow.Opacity = 0;
|
||||
ComponentSettingsWindow.Opacity = 1;
|
||||
}
|
||||
|
||||
private void OnClassScheduleSettingsChanged(object? sender, EventArgs e)
|
||||
{
|
||||
if (_selectedDesktopComponentHost is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (TryGetContentHost(_selectedDesktopComponentHost)?.Child is ClassScheduleWidget widget)
|
||||
{
|
||||
widget.RefreshFromSettings();
|
||||
}
|
||||
}
|
||||
|
||||
private void CloseComponentSettingsWindow()
|
||||
{
|
||||
if (ComponentSettingsWindow is null)
|
||||
@@ -723,6 +758,11 @@ public partial class MainWindow
|
||||
return;
|
||||
}
|
||||
|
||||
if (ComponentSettingsContentHost?.Content is ClassScheduleSettingsWindow classScheduleSettingsWindow)
|
||||
{
|
||||
classScheduleSettingsWindow.SettingsChanged -= OnClassScheduleSettingsChanged;
|
||||
}
|
||||
|
||||
ComponentSettingsWindow.Opacity = 0;
|
||||
|
||||
DispatcherTimer.RunOnce(() =>
|
||||
|
||||
@@ -177,6 +177,8 @@ public partial class MainWindow
|
||||
"Choose how weather widgets resolve location.");
|
||||
WeatherLocationModeCityItem.Content = L("settings.weather.mode_city_search", "City Search");
|
||||
WeatherLocationModeCoordinatesItem.Content = L("settings.weather.mode_coordinates", "Coordinates");
|
||||
WeatherLocationModeCityChipItem.Content = L("settings.weather.mode_city_search", "City Search");
|
||||
WeatherLocationModeCoordinatesChipItem.Content = L("settings.weather.mode_coordinates", "Coordinates");
|
||||
WeatherAutoRefreshToggleSwitch.Content = L("settings.weather.auto_refresh", "Auto refresh location on startup");
|
||||
|
||||
WeatherCitySearchSettingsExpander.Header = L("settings.weather.city_search_header", "City Search");
|
||||
@@ -197,11 +199,29 @@ public partial class MainWindow
|
||||
WeatherLocationNameTextBox.Watermark = L("settings.weather.location_name_placeholder", "Display name (optional)");
|
||||
WeatherApplyCoordinatesButton.Content = L("settings.weather.apply_coordinates_button", "Apply Coordinates");
|
||||
|
||||
WeatherPreviewSettingsExpander.Header = L("settings.weather.preview_header", "Connection Test");
|
||||
WeatherPreviewSettingsExpander.Header = L("settings.weather.preview_panel_header", "Weather Preview");
|
||||
WeatherPreviewSettingsExpander.Description = L(
|
||||
"settings.weather.preview_desc",
|
||||
"Send one test request to verify current settings.");
|
||||
WeatherPreviewButton.Content = L("settings.weather.preview_button", "Test Fetch");
|
||||
"settings.weather.preview_panel_desc",
|
||||
"Refresh and verify current weather service status.");
|
||||
WeatherPreviewButton.Content = L("settings.weather.refresh_button", "Refresh");
|
||||
|
||||
WeatherAlertFilterSettingsExpander.Header = L("settings.weather.alert_filter_header", "Excluded Alerts");
|
||||
WeatherAlertFilterSettingsExpander.Description = L(
|
||||
"settings.weather.alert_filter_desc",
|
||||
"Alerts containing these words will not be shown. One rule per line.");
|
||||
WeatherExcludedAlertsTextBox.Watermark = L("settings.weather.alert_filter_placeholder", "One keyword per line");
|
||||
|
||||
WeatherIconPackSettingsExpander.Header = L("settings.weather.icon_style_header", "Weather Icon Style");
|
||||
WeatherIconPackSettingsExpander.Description = L(
|
||||
"settings.weather.icon_style_desc",
|
||||
"Choose Fluent Icon style for weather symbols.");
|
||||
WeatherIconPackFluentRegularItem.Content = L("settings.weather.icon_style_fluent_regular", "Fluent Regular");
|
||||
WeatherIconPackFluentFilledItem.Content = L("settings.weather.icon_style_fluent_filled", "Fluent Filled");
|
||||
|
||||
WeatherNoTlsSettingsExpander.Header = L("settings.weather.no_tls_header", "No TLS Weather Request");
|
||||
WeatherNoTlsSettingsExpander.Description = L(
|
||||
"settings.weather.no_tls_desc",
|
||||
"Not recommended. Enable only for incompatible network environments.");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_weatherSearchKeyword))
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using FluentIcons.Avalonia;
|
||||
using FluentIcons.Common;
|
||||
using LanMontainDesktop.Views.Components;
|
||||
@@ -655,6 +655,9 @@ public partial class MainWindow
|
||||
WeatherLongitude = _weatherLongitude,
|
||||
WeatherAutoRefreshLocation = _weatherAutoRefreshLocation,
|
||||
WeatherLocationQuery = BuildLegacyWeatherLocationQuery(),
|
||||
WeatherExcludedAlerts = _weatherExcludedAlertsRaw,
|
||||
WeatherIconPackId = _weatherIconPackId,
|
||||
WeatherNoTlsRequests = _weatherNoTlsRequests,
|
||||
TopStatusComponentIds = _topStatusComponentIds.ToList(),
|
||||
PinnedTaskbarActions = _pinnedTaskbarActions.Select(action => action.ToString()).ToList(),
|
||||
EnableDynamicTaskbarActions = _enableDynamicTaskbarActions,
|
||||
@@ -698,6 +701,11 @@ public partial class MainWindow
|
||||
_weatherLatitude = NormalizeLatitude(snapshot.WeatherLatitude);
|
||||
_weatherLongitude = NormalizeLongitude(snapshot.WeatherLongitude);
|
||||
_weatherAutoRefreshLocation = snapshot.WeatherAutoRefreshLocation;
|
||||
_weatherExcludedAlertsRaw = snapshot.WeatherExcludedAlerts?.Trim() ?? string.Empty;
|
||||
_weatherIconPackId = string.IsNullOrWhiteSpace(snapshot.WeatherIconPackId)
|
||||
? "FluentRegular"
|
||||
: snapshot.WeatherIconPackId.Trim();
|
||||
_weatherNoTlsRequests = snapshot.WeatherNoTlsRequests;
|
||||
_weatherSearchKeyword = string.Empty;
|
||||
|
||||
var legacyQuery = snapshot.WeatherLocationQuery?.Trim() ?? string.Empty;
|
||||
@@ -717,6 +725,11 @@ public partial class MainWindow
|
||||
WeatherAutoRefreshToggleSwitch.IsChecked = _weatherAutoRefreshLocation;
|
||||
}
|
||||
|
||||
if (WeatherNoTlsToggleSwitch is not null)
|
||||
{
|
||||
WeatherNoTlsToggleSwitch.IsChecked = _weatherNoTlsRequests;
|
||||
}
|
||||
|
||||
if (WeatherCitySearchTextBox is not null)
|
||||
{
|
||||
WeatherCitySearchTextBox.Text = string.Empty;
|
||||
@@ -747,6 +760,13 @@ public partial class MainWindow
|
||||
WeatherLongitudeNumberBox.Value = _weatherLongitude;
|
||||
}
|
||||
|
||||
if (WeatherExcludedAlertsTextBox is not null)
|
||||
{
|
||||
WeatherExcludedAlertsTextBox.Text = _weatherExcludedAlertsRaw;
|
||||
}
|
||||
|
||||
SelectWeatherIconPackInUi(_weatherIconPackId);
|
||||
|
||||
if (WeatherSearchStatusTextBlock is not null)
|
||||
{
|
||||
WeatherSearchStatusTextBlock.Text = L(
|
||||
@@ -766,6 +786,11 @@ public partial class MainWindow
|
||||
"Use test fetch to verify your weather configuration.");
|
||||
}
|
||||
|
||||
UpdateWeatherPreviewSummary(
|
||||
weatherCode: null,
|
||||
temperatureText: "--",
|
||||
updatedAt: null);
|
||||
|
||||
UpdateWeatherLocationModePanels();
|
||||
UpdateWeatherLocationStatusText();
|
||||
}
|
||||
@@ -826,21 +851,61 @@ public partial class MainWindow
|
||||
|
||||
private void SelectWeatherLocationModeInUi(WeatherLocationMode mode)
|
||||
{
|
||||
if (WeatherLocationModeComboBox is null)
|
||||
var targetTag = ToWeatherLocationModeTag(mode);
|
||||
var selected = false;
|
||||
if (WeatherLocationModeComboBox is not null)
|
||||
{
|
||||
foreach (var item in WeatherLocationModeComboBox.Items.OfType<ComboBoxItem>())
|
||||
{
|
||||
if (string.Equals(item.Tag?.ToString(), targetTag, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
WeatherLocationModeComboBox.SelectedItem = item;
|
||||
selected = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!selected)
|
||||
{
|
||||
WeatherLocationModeComboBox.SelectedIndex = mode == WeatherLocationMode.Coordinates ? 1 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (WeatherLocationModeChipListBox is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var item in WeatherLocationModeComboBox.Items.OfType<ComboBoxItem>())
|
||||
foreach (var item in WeatherLocationModeChipListBox.Items.OfType<ListBoxItem>())
|
||||
{
|
||||
if (string.Equals(item.Tag?.ToString(), ToWeatherLocationModeTag(mode), StringComparison.OrdinalIgnoreCase))
|
||||
if (string.Equals(item.Tag?.ToString(), targetTag, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
WeatherLocationModeComboBox.SelectedItem = item;
|
||||
WeatherLocationModeChipListBox.SelectedItem = item;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
WeatherLocationModeComboBox.SelectedIndex = mode == WeatherLocationMode.Coordinates ? 1 : 0;
|
||||
WeatherLocationModeChipListBox.SelectedIndex = mode == WeatherLocationMode.Coordinates ? 1 : 0;
|
||||
}
|
||||
|
||||
private void SelectWeatherIconPackInUi(string iconPackId)
|
||||
{
|
||||
if (WeatherIconPackComboBox is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var item in WeatherIconPackComboBox.Items.OfType<ComboBoxItem>())
|
||||
{
|
||||
if (string.Equals(item.Tag?.ToString(), iconPackId, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
WeatherIconPackComboBox.SelectedItem = item;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
WeatherIconPackComboBox.SelectedIndex = 0;
|
||||
_weatherIconPackId = "FluentRegular";
|
||||
}
|
||||
|
||||
private void UpdateWeatherLocationModePanels()
|
||||
@@ -864,6 +929,38 @@ public partial class MainWindow
|
||||
}
|
||||
|
||||
_weatherLocationMode = ParseWeatherLocationMode(item.Tag?.ToString());
|
||||
_suppressWeatherLocationEvents = true;
|
||||
try
|
||||
{
|
||||
SelectWeatherLocationModeInUi(_weatherLocationMode);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_suppressWeatherLocationEvents = false;
|
||||
}
|
||||
UpdateWeatherLocationModePanels();
|
||||
UpdateWeatherLocationStatusText();
|
||||
PersistSettings();
|
||||
}
|
||||
|
||||
private void OnWeatherLocationModeChipSelectionChanged(object? sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (_suppressWeatherLocationEvents || WeatherLocationModeChipListBox?.SelectedItem is not ListBoxItem item)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_weatherLocationMode = ParseWeatherLocationMode(item.Tag?.ToString());
|
||||
_suppressWeatherLocationEvents = true;
|
||||
try
|
||||
{
|
||||
SelectWeatherLocationModeInUi(_weatherLocationMode);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_suppressWeatherLocationEvents = false;
|
||||
}
|
||||
|
||||
UpdateWeatherLocationModePanels();
|
||||
UpdateWeatherLocationStatusText();
|
||||
PersistSettings();
|
||||
@@ -880,6 +977,51 @@ public partial class MainWindow
|
||||
PersistSettings();
|
||||
}
|
||||
|
||||
private void OnWeatherExcludedAlertsLostFocus(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (WeatherExcludedAlertsTextBox is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_weatherExcludedAlertsRaw = WeatherExcludedAlertsTextBox.Text?.Trim() ?? string.Empty;
|
||||
PersistSettings();
|
||||
}
|
||||
|
||||
private void OnWeatherIconPackSelectionChanged(object? sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (_suppressWeatherLocationEvents || WeatherIconPackComboBox?.SelectedItem is not ComboBoxItem item)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_weatherIconPackId = item.Tag?.ToString() switch
|
||||
{
|
||||
"FluentFilled" => "FluentFilled",
|
||||
_ => "FluentRegular"
|
||||
};
|
||||
|
||||
if (WeatherPreviewIconSymbol is not null)
|
||||
{
|
||||
WeatherPreviewIconSymbol.IconVariant = string.Equals(_weatherIconPackId, "FluentFilled", StringComparison.OrdinalIgnoreCase)
|
||||
? IconVariant.Filled
|
||||
: IconVariant.Regular;
|
||||
}
|
||||
|
||||
PersistSettings();
|
||||
}
|
||||
|
||||
private void OnWeatherNoTlsToggled(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_suppressWeatherLocationEvents || WeatherNoTlsToggleSwitch is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_weatherNoTlsRequests = WeatherNoTlsToggleSwitch.IsChecked == true;
|
||||
PersistSettings();
|
||||
}
|
||||
|
||||
private async void OnSearchWeatherCityClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_isWeatherSearchInProgress || WeatherCitySearchTextBox is null || WeatherCityResultsComboBox is null)
|
||||
@@ -974,7 +1116,7 @@ public partial class MainWindow
|
||||
: $" ({location.Affiliation})";
|
||||
return string.Create(
|
||||
CultureInfo.InvariantCulture,
|
||||
$"{location.Name}{affiliation} · {location.LocationKey}");
|
||||
$"{location.Name}{affiliation} | {location.LocationKey}");
|
||||
}
|
||||
|
||||
private static string BuildWeatherLocationName(WeatherLocation location)
|
||||
@@ -1140,6 +1282,11 @@ public partial class MainWindow
|
||||
"Please apply one weather location before testing.");
|
||||
}
|
||||
|
||||
UpdateWeatherPreviewSummary(
|
||||
weatherCode: null,
|
||||
temperatureText: "--",
|
||||
updatedAt: null);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1168,6 +1315,11 @@ public partial class MainWindow
|
||||
result.ErrorMessage ?? result.ErrorCode ?? "Unknown error");
|
||||
}
|
||||
|
||||
UpdateWeatherPreviewSummary(
|
||||
weatherCode: null,
|
||||
temperatureText: "--",
|
||||
updatedAt: DateTimeOffset.Now);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1178,18 +1330,24 @@ public partial class MainWindow
|
||||
var weather = snapshot.Current.WeatherText ??
|
||||
L("settings.weather.preview_unknown", "Unknown");
|
||||
var temperature = snapshot.Current.TemperatureC.HasValue
|
||||
? string.Create(CultureInfo.InvariantCulture, $"{snapshot.Current.TemperatureC.Value:F1}°C")
|
||||
? string.Create(CultureInfo.InvariantCulture, $"{snapshot.Current.TemperatureC.Value:F1} C")
|
||||
: "--";
|
||||
var updatedAt = snapshot.ObservationTime ?? snapshot.FetchedAt;
|
||||
|
||||
if (WeatherPreviewResultTextBlock is not null)
|
||||
{
|
||||
WeatherPreviewResultTextBlock.Text = Lf(
|
||||
"settings.weather.preview_success_format",
|
||||
"Test success: {0} · {1} · {2}",
|
||||
"Test success: {0} | {1} | {2}",
|
||||
location,
|
||||
weather,
|
||||
temperature);
|
||||
}
|
||||
|
||||
UpdateWeatherPreviewSummary(
|
||||
weatherCode: snapshot.Current.WeatherCode,
|
||||
temperatureText: temperature,
|
||||
updatedAt: updatedAt);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -1200,6 +1358,11 @@ public partial class MainWindow
|
||||
"Test fetch failed: {0}",
|
||||
ex.Message);
|
||||
}
|
||||
|
||||
UpdateWeatherPreviewSummary(
|
||||
weatherCode: null,
|
||||
temperatureText: "--",
|
||||
updatedAt: DateTimeOffset.Now);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -1208,6 +1371,46 @@ public partial class MainWindow
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateWeatherPreviewSummary(int? weatherCode, string temperatureText, DateTimeOffset? updatedAt)
|
||||
{
|
||||
if (WeatherPreviewIconSymbol is not null)
|
||||
{
|
||||
WeatherPreviewIconSymbol.Symbol = ResolveWeatherPreviewSymbol(weatherCode, _isNightMode);
|
||||
WeatherPreviewIconSymbol.IconVariant = string.Equals(_weatherIconPackId, "FluentFilled", StringComparison.OrdinalIgnoreCase)
|
||||
? IconVariant.Filled
|
||||
: IconVariant.Regular;
|
||||
}
|
||||
|
||||
if (WeatherPreviewTemperatureTextBlock is not null)
|
||||
{
|
||||
WeatherPreviewTemperatureTextBlock.Text = string.IsNullOrWhiteSpace(temperatureText) ? "--" : temperatureText;
|
||||
}
|
||||
|
||||
if (WeatherPreviewUpdatedTextBlock is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
WeatherPreviewUpdatedTextBlock.Text = updatedAt.HasValue
|
||||
? Lf("weather.widget.updated_format", "Updated {0:HH:mm}", updatedAt.Value.LocalDateTime)
|
||||
: "-";
|
||||
}
|
||||
|
||||
private static Symbol ResolveWeatherPreviewSymbol(int? weatherCode, bool isNight)
|
||||
{
|
||||
return weatherCode switch
|
||||
{
|
||||
0 => isNight ? Symbol.WeatherMoon : Symbol.WeatherSunny,
|
||||
1 or 2 => isNight ? Symbol.WeatherPartlyCloudyNight : Symbol.WeatherPartlyCloudyDay,
|
||||
3 or 7 => Symbol.WeatherRainShowersDay,
|
||||
8 or 9 => Symbol.WeatherRain,
|
||||
4 => Symbol.WeatherThunderstorm,
|
||||
13 or 14 or 15 or 16 => Symbol.WeatherSnow,
|
||||
18 or 32 => Symbol.WeatherFog,
|
||||
_ => isNight ? Symbol.WeatherPartlyCloudyNight : Symbol.WeatherPartlyCloudyDay
|
||||
};
|
||||
}
|
||||
|
||||
private void SetWeatherSearchBusy(bool isBusy)
|
||||
{
|
||||
if (WeatherSearchButton is not null)
|
||||
@@ -1683,6 +1886,42 @@ public partial class MainWindow
|
||||
};
|
||||
}
|
||||
|
||||
if (WeatherPreviewSettingsExpander is not null)
|
||||
{
|
||||
WeatherPreviewSettingsExpander.IconSource = new FluentIcons.Avalonia.Fluent.SymbolIconSource
|
||||
{
|
||||
Symbol = Symbol.WeatherSunny,
|
||||
IconVariant = variant
|
||||
};
|
||||
}
|
||||
|
||||
if (WeatherAlertFilterSettingsExpander is not null)
|
||||
{
|
||||
WeatherAlertFilterSettingsExpander.IconSource = new FluentIcons.Avalonia.Fluent.SymbolIconSource
|
||||
{
|
||||
Symbol = Symbol.Info,
|
||||
IconVariant = variant
|
||||
};
|
||||
}
|
||||
|
||||
if (WeatherIconPackSettingsExpander is not null)
|
||||
{
|
||||
WeatherIconPackSettingsExpander.IconSource = new FluentIcons.Avalonia.Fluent.SymbolIconSource
|
||||
{
|
||||
Symbol = Symbol.Color,
|
||||
IconVariant = variant
|
||||
};
|
||||
}
|
||||
|
||||
if (WeatherNoTlsSettingsExpander is not null)
|
||||
{
|
||||
WeatherNoTlsSettingsExpander.IconSource = new FluentIcons.Avalonia.Fluent.SymbolIconSource
|
||||
{
|
||||
Symbol = Symbol.Globe,
|
||||
IconVariant = variant
|
||||
};
|
||||
}
|
||||
|
||||
if (LanguageSettingsExpander is not null)
|
||||
{
|
||||
LanguageSettingsExpander.IconSource = new FluentIcons.Avalonia.Fluent.SymbolIconSource
|
||||
@@ -1718,3 +1957,4 @@ public partial class MainWindow
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1076,23 +1076,89 @@
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel x:Name="WeatherSettingsPanel"
|
||||
IsVisible="False"
|
||||
Spacing="16">
|
||||
<ScrollViewer x:Name="WeatherSettingsPanel"
|
||||
IsVisible="False"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel x:Name="WeatherSettingsContentPanel"
|
||||
Margin="0,0,8,0"
|
||||
Spacing="16"
|
||||
Classes="settings-animated-intro">
|
||||
<TextBlock x:Name="WeatherPanelTitleTextBlock"
|
||||
FontSize="24"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
||||
Text="天气" />
|
||||
|
||||
<Border Classes="settings-expander-shell">
|
||||
<ui:SettingsExpander x:Name="WeatherPreviewSettingsExpander"
|
||||
Header="Weather Preview"
|
||||
Description="Refresh and verify current weather service status."
|
||||
IsExpanded="True">
|
||||
<ui:SettingsExpander.Footer>
|
||||
<StackPanel Spacing="10">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto"
|
||||
ColumnSpacing="12">
|
||||
<Border Width="44"
|
||||
Height="44"
|
||||
CornerRadius="{DynamicResource DesignCornerRadiusXs}"
|
||||
BorderThickness="1"
|
||||
BorderBrush="{DynamicResource AdaptiveButtonBorderBrush}"
|
||||
Background="{DynamicResource AdaptiveButtonBackgroundBrush}">
|
||||
<fi:SymbolIcon x:Name="WeatherPreviewIconSymbol"
|
||||
Symbol="WeatherSunny"
|
||||
IconVariant="Regular"
|
||||
FontSize="22"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center" />
|
||||
</Border>
|
||||
<StackPanel Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="2">
|
||||
<TextBlock x:Name="WeatherPreviewTemperatureTextBlock"
|
||||
FontSize="22"
|
||||
FontWeight="SemiBold"
|
||||
Text="--°" />
|
||||
<TextBlock x:Name="WeatherPreviewUpdatedTextBlock"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
|
||||
Text="-" />
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="2"
|
||||
Orientation="Horizontal"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="8">
|
||||
<Button x:Name="WeatherPreviewButton"
|
||||
Padding="12,8"
|
||||
Click="OnTestWeatherRequestClick"
|
||||
Content="Refresh" />
|
||||
<ui:ProgressRing x:Name="WeatherPreviewProgressRing"
|
||||
Width="20"
|
||||
Height="20"
|
||||
IsActive="True"
|
||||
IsVisible="False" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<TextBlock x:Name="WeatherPreviewResultTextBlock"
|
||||
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
|
||||
TextWrapping="Wrap"
|
||||
Text="Use refresh to verify your weather configuration." />
|
||||
</StackPanel>
|
||||
</ui:SettingsExpander.Footer>
|
||||
</ui:SettingsExpander>
|
||||
</Border>
|
||||
|
||||
<Border Classes="settings-expander-shell">
|
||||
<ui:SettingsExpander x:Name="WeatherLocationSettingsExpander"
|
||||
Header="Location Source"
|
||||
Description="Choose how weather widgets resolve location.">
|
||||
Description="Choose how weather widgets resolve location."
|
||||
IsExpanded="True">
|
||||
<ui:SettingsExpander.Footer>
|
||||
<StackPanel Spacing="10">
|
||||
<ComboBox x:Name="WeatherLocationModeComboBox"
|
||||
Width="220"
|
||||
IsVisible="False"
|
||||
SelectionChanged="OnWeatherLocationModeSelectionChanged">
|
||||
<ComboBoxItem x:Name="WeatherLocationModeCityItem"
|
||||
Tag="CitySearch"
|
||||
@@ -1101,6 +1167,24 @@
|
||||
Tag="Coordinates"
|
||||
Content="Coordinates" />
|
||||
</ComboBox>
|
||||
|
||||
<ListBox x:Name="WeatherLocationModeChipListBox"
|
||||
Classes="settings-chip-list"
|
||||
SelectionMode="Single"
|
||||
SelectionChanged="OnWeatherLocationModeChipSelectionChanged">
|
||||
<ListBox.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<WrapPanel Orientation="Horizontal" />
|
||||
</ItemsPanelTemplate>
|
||||
</ListBox.ItemsPanel>
|
||||
<ListBoxItem x:Name="WeatherLocationModeCityChipItem"
|
||||
Tag="CitySearch"
|
||||
Content="City Search" />
|
||||
<ListBoxItem x:Name="WeatherLocationModeCoordinatesChipItem"
|
||||
Tag="Coordinates"
|
||||
Content="Coordinates" />
|
||||
</ListBox>
|
||||
|
||||
<ToggleSwitch x:Name="WeatherAutoRefreshToggleSwitch"
|
||||
Checked="OnWeatherAutoRefreshToggled"
|
||||
Unchecked="OnWeatherAutoRefreshToggled"
|
||||
@@ -1113,7 +1197,8 @@
|
||||
<Border Classes="settings-expander-shell">
|
||||
<ui:SettingsExpander x:Name="WeatherCitySearchSettingsExpander"
|
||||
Header="City Search"
|
||||
Description="Search cities and apply one weather location.">
|
||||
Description="Search cities and apply one weather location."
|
||||
IsExpanded="True">
|
||||
<ui:SettingsExpander.Footer>
|
||||
<StackPanel Spacing="10">
|
||||
<Grid ColumnDefinitions="*,Auto,Auto"
|
||||
@@ -1155,7 +1240,8 @@
|
||||
<ui:SettingsExpander x:Name="WeatherCoordinateSettingsExpander"
|
||||
Header="Coordinates"
|
||||
Description="Set latitude/longitude and optional key/name."
|
||||
IsVisible="False">
|
||||
IsVisible="False"
|
||||
IsExpanded="True">
|
||||
<ui:SettingsExpander.Footer>
|
||||
<StackPanel Spacing="10">
|
||||
<Grid ColumnDefinitions="*,*"
|
||||
@@ -1200,38 +1286,60 @@
|
||||
</Border>
|
||||
|
||||
<Border Classes="settings-expander-shell">
|
||||
<ui:SettingsExpander x:Name="WeatherPreviewSettingsExpander"
|
||||
Header="Connection Test"
|
||||
Description="Send one test request to verify current settings.">
|
||||
<ui:SettingsExpander x:Name="WeatherAlertFilterSettingsExpander"
|
||||
Header="Excluded Alerts"
|
||||
Description="Alerts containing these words will not be shown. One rule per line."
|
||||
IsExpanded="True">
|
||||
<ui:SettingsExpander.Footer>
|
||||
<StackPanel Spacing="10">
|
||||
<StackPanel Orientation="Horizontal"
|
||||
Spacing="8">
|
||||
<Button x:Name="WeatherPreviewButton"
|
||||
Padding="12,8"
|
||||
Click="OnTestWeatherRequestClick"
|
||||
Content="Test Fetch" />
|
||||
<ui:ProgressRing x:Name="WeatherPreviewProgressRing"
|
||||
Width="24"
|
||||
Height="24"
|
||||
IsActive="True"
|
||||
IsVisible="False" />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock x:Name="WeatherPreviewResultTextBlock"
|
||||
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
|
||||
TextWrapping="Wrap"
|
||||
Text="Use test fetch to verify your weather configuration." />
|
||||
</StackPanel>
|
||||
<TextBox x:Name="WeatherExcludedAlertsTextBox"
|
||||
MinHeight="96"
|
||||
MaxHeight="220"
|
||||
Width="360"
|
||||
TextWrapping="Wrap"
|
||||
AcceptsReturn="True"
|
||||
LostFocus="OnWeatherExcludedAlertsLostFocus" />
|
||||
</ui:SettingsExpander.Footer>
|
||||
</ui:SettingsExpander>
|
||||
</Border>
|
||||
|
||||
<TextBlock x:Name="WeatherLocationStatusTextBlock"
|
||||
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
|
||||
Text="No city location is configured." />
|
||||
</StackPanel>
|
||||
<Border Classes="settings-expander-shell">
|
||||
<ui:SettingsExpander x:Name="WeatherIconPackSettingsExpander"
|
||||
Header="Weather Icon Style"
|
||||
Description="Choose Fluent Icon style for weather symbols."
|
||||
IsExpanded="True">
|
||||
<ui:SettingsExpander.Footer>
|
||||
<ComboBox x:Name="WeatherIconPackComboBox"
|
||||
Width="240"
|
||||
SelectionChanged="OnWeatherIconPackSelectionChanged">
|
||||
<ComboBoxItem x:Name="WeatherIconPackFluentRegularItem"
|
||||
Tag="FluentRegular"
|
||||
Content="Fluent Regular" />
|
||||
<ComboBoxItem x:Name="WeatherIconPackFluentFilledItem"
|
||||
Tag="FluentFilled"
|
||||
Content="Fluent Filled" />
|
||||
</ComboBox>
|
||||
</ui:SettingsExpander.Footer>
|
||||
</ui:SettingsExpander>
|
||||
</Border>
|
||||
|
||||
<Border Classes="settings-expander-shell">
|
||||
<ui:SettingsExpander x:Name="WeatherNoTlsSettingsExpander"
|
||||
Header="No TLS Weather Request"
|
||||
Description="Not recommended. Enable only for incompatible network environments."
|
||||
IsExpanded="True">
|
||||
<ui:SettingsExpander.Footer>
|
||||
<ToggleSwitch x:Name="WeatherNoTlsToggleSwitch"
|
||||
Checked="OnWeatherNoTlsToggled"
|
||||
Unchecked="OnWeatherNoTlsToggled" />
|
||||
</ui:SettingsExpander.Footer>
|
||||
</ui:SettingsExpander>
|
||||
</Border>
|
||||
|
||||
<TextBlock x:Name="WeatherLocationStatusTextBlock"
|
||||
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
|
||||
Text="No city location is configured." />
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
<StackPanel x:Name="RegionSettingsPanel"
|
||||
IsVisible="False"
|
||||
Spacing="16">
|
||||
@@ -1499,3 +1607,4 @@
|
||||
|
||||
</Window>
|
||||
|
||||
|
||||
|
||||
@@ -147,6 +147,9 @@ public partial class MainWindow : Window
|
||||
private double _weatherLatitude = 39.9042;
|
||||
private double _weatherLongitude = 116.4074;
|
||||
private bool _weatherAutoRefreshLocation;
|
||||
private string _weatherExcludedAlertsRaw = string.Empty;
|
||||
private string _weatherIconPackId = "FluentRegular";
|
||||
private bool _weatherNoTlsRequests;
|
||||
private string _weatherSearchKeyword = string.Empty;
|
||||
private bool _isWeatherSearchInProgress;
|
||||
private bool _isWeatherPreviewInProgress;
|
||||
|
||||
Reference in New Issue
Block a user