课表组件、天气组件全面升级。
This commit is contained in:
lincube
2026-03-03 15:09:49 +08:00
parent 2d09c1aca2
commit 478ed115a1
47 changed files with 4876 additions and 771 deletions

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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="&#22825;&#27668;" />
<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>

View File

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