using Microsoft.UI.Text; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Automation; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls.Primitives; using Microsoft.UI.Xaml.Media; using YMhut.Box.Core.Settings; using YMhut.Box.WinUI.Services; namespace YMhut.Box.WinUI.Controls; public sealed class WeatherCapsuleControl : UserControl { private readonly ITitleWeatherService _weatherService; private readonly ISettingsService _settingsService = AppServices.GetRequiredService(); private readonly Button _button; private readonly AnimatedWeatherIconControl _weatherIcon; private readonly ProgressRing _loadingRing; private readonly TextBlock _locationText; private readonly TextBlock _tempText; private readonly TextBlock _conditionText; private readonly Flyout _flyout; private TitleWeatherSnapshot _snapshot = TitleWeatherSnapshot.Loading; private TitleWeatherSnapshot? _lastAvailableSnapshot; private int _loadVersion; public WeatherCapsuleControl(ITitleWeatherService weatherService) { _weatherService = weatherService; HorizontalAlignment = HorizontalAlignment.Right; VerticalAlignment = VerticalAlignment.Center; _weatherIcon = new AnimatedWeatherIconControl(20) { HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center }; _loadingRing = new ProgressRing { Width = 18, Height = 18, IsActive = true, Visibility = Visibility.Collapsed }; _locationText = ModernUi.Text(_snapshot.Location, 12, FontWeights.SemiBold, ModernUi.TextSecondary, maxLines: 1); _conditionText = ModernUi.Text(_snapshot.Condition, 10.5, foreground: ModernUi.TextSecondary, maxLines: 1); _tempText = ModernUi.Text(_snapshot.TemperatureText, 15, FontWeights.SemiBold, ModernUi.TextPrimary, maxLines: 1); ConfigureCompactLine(_locationText, 14); ConfigureCompactLine(_conditionText, 13); ConfigureCompactLine(_tempText, 18); var visual = new Grid { ColumnSpacing = 9, VerticalAlignment = VerticalAlignment.Center, ColumnDefinitions = { new ColumnDefinition { Width = GridLength.Auto }, new ColumnDefinition(), new ColumnDefinition { Width = GridLength.Auto } } }; var iconHost = new Grid { Width = 22, Height = 22, Children = { _weatherIcon, _loadingRing } }; visual.Children.Add(iconHost); var text = new Grid { MinWidth = 76, MaxWidth = 130, VerticalAlignment = VerticalAlignment.Center, RowDefinitions = { new RowDefinition { Height = GridLength.Auto }, new RowDefinition { Height = GridLength.Auto } } }; text.Children.Add(_locationText); Grid.SetRow(_conditionText, 1); text.Children.Add(_conditionText); Grid.SetColumn(text, 1); visual.Children.Add(text); Grid.SetColumn(_tempText, 2); visual.Children.Add(_tempText); _flyout = new Flyout { Placement = FlyoutPlacementMode.BottomEdgeAlignedRight }; _flyout.Opening += (_, _) => _flyout.Content = BuildFlyoutContent(); _button = new Button { Height = 38, MinWidth = 168, MaxWidth = 242, Margin = new Thickness(0, 4, 0, 4), Padding = new Thickness(12, 4, 12, 4), CornerRadius = new CornerRadius(19), Background = ModernUi.Surface, BorderBrush = ModernUi.Stroke, BorderThickness = new Thickness(1), Content = visual, Flyout = _flyout }; AutomationProperties.SetName(_button, AppLocalizer.T("天气", "Weather")); ToolTipService.SetToolTip(_button, AppLocalizer.T("查看天气详情", "Show weather details")); Content = _button; ApplySnapshot(_snapshot); } public async Task LoadAsync(CancellationToken cancellationToken = default) { await RefreshAsync(cancellationToken).ConfigureAwait(false); } public async Task RefreshAsync(CancellationToken cancellationToken = default) { var loadVersion = Interlocked.Increment(ref _loadVersion); DispatcherQueue.TryEnqueue(() => ApplySnapshot(TitleWeatherSnapshot.Loading, loadingOverlay: true)); var snapshot = await _weatherService.GetCurrentAsync(cancellationToken).ConfigureAwait(false); if (loadVersion != _loadVersion) { return; } DispatcherQueue.TryEnqueue(() => ApplySnapshot(snapshot)); } public void RefreshLanguage() { ApplySnapshot(_snapshot); } private void ApplySnapshot(TitleWeatherSnapshot snapshot, bool loadingOverlay = false) { var displaySnapshot = loadingOverlay && _lastAvailableSnapshot is not null ? _lastAvailableSnapshot : snapshot; _snapshot = displaySnapshot; if (displaySnapshot.IsAvailable) { _lastAvailableSnapshot = displaySnapshot; } var loading = loadingOverlay || ReferenceEquals(snapshot, TitleWeatherSnapshot.Loading); _loadingRing.IsActive = loading; _loadingRing.Visibility = loading ? Visibility.Visible : Visibility.Collapsed; _loadingRing.Opacity = loading ? 0.78 : 0; _weatherIcon.Visibility = Visibility.Visible; _weatherIcon.Opacity = loading ? 0.62 : 1; _weatherIcon.Update(displaySnapshot, _settingsService.Current.AnimationsEnabled); _locationText.Text = displaySnapshot.Location; _conditionText.Text = displaySnapshot.Condition; _tempText.Text = displaySnapshot.TemperatureText; _button.Background = displaySnapshot.IsAvailable ? ModernUi.Surface : ModernUi.SurfaceAlt; _button.BorderBrush = displaySnapshot.IsAvailable ? ModernUi.Stroke : ModernUi.StrokeStrong; ToolTipService.SetToolTip(_button, loading ? AppLocalizer.T("天气正在刷新", "Weather is refreshing") : BuildTooltip(displaySnapshot)); AutomationProperties.SetName(_button, loading ? AppLocalizer.T("天气正在刷新", "Weather is refreshing") : BuildTooltip(displaySnapshot)); } private string BuildTooltip(TitleWeatherSnapshot snapshot) { return snapshot.IsAvailable ? $"{snapshot.Location} {snapshot.Condition} {snapshot.TemperatureText}" : AppLocalizer.T("天气暂不可用,点击重试", "Weather unavailable. Click to retry."); } private UIElement BuildFlyoutContent() { var snapshot = _snapshot; var refresh = ModernUi.PillButton(AppLocalizer.T("刷新", "Refresh"), "\uE72C", async () => await RefreshAsync(), primary: true); refresh.HorizontalAlignment = HorizontalAlignment.Right; Grid.SetColumn(refresh, 2); return new StackPanel { Width = 280, Padding = new Thickness(4), Spacing = 12, Children = { new Grid { ColumnSpacing = 12, ColumnDefinitions = { new ColumnDefinition { Width = GridLength.Auto }, new ColumnDefinition(), new ColumnDefinition { Width = GridLength.Auto } }, Children = { BuildFlyoutIcon(snapshot), BuildTitleBlock(snapshot), refresh } }, BuildDetailLine(AppLocalizer.T("体感", "Feels like"), snapshot.FeelsLikeText, "\uE706"), BuildDetailLine(AppLocalizer.T("湿度", "Humidity"), snapshot.HumidityText, "\uE81F"), BuildDetailLine(AppLocalizer.T("风速", "Wind"), snapshot.WindText, "\uE9CA"), BuildDetailLine(AppLocalizer.T("今日温度", "Today"), snapshot.RangeText, "\uE787"), BuildDetailLine(AppLocalizer.T("更新时间", "Updated"), snapshot.UpdatedText, "\uE823"), snapshot.ErrorMessage is null ? new Border { Height = 0 } : ModernUi.Card( ModernUi.Text(snapshot.ErrorMessage, 12, foreground: ModernUi.TextSecondary, maxLines: 3), new Thickness(10), radius: 8, background: ModernUi.SurfaceAlt) } }; } private static StackPanel BuildTitleBlock(TitleWeatherSnapshot snapshot) { var panel = new StackPanel { Spacing = 1, VerticalAlignment = VerticalAlignment.Center, Children = { ModernUi.Text(snapshot.Location, 17, FontWeights.SemiBold, maxLines: 1), ModernUi.Text($"{snapshot.Condition} · {snapshot.TemperatureText} · {snapshot.QueryLevel}", 13, foreground: ModernUi.TextSecondary, maxLines: 1) } }; Grid.SetColumn(panel, 1); return panel; } private UIElement BuildFlyoutIcon(TitleWeatherSnapshot snapshot) { var icon = new AnimatedWeatherIconControl(44); icon.Update(snapshot, _settingsService.Current.AnimationsEnabled); return new Border { Width = 52, Height = 52, CornerRadius = new CornerRadius(8), Background = ModernUi.AccentSoft, BorderBrush = ModernUi.Stroke, BorderThickness = new Thickness(1), Child = icon }; } private static void ConfigureCompactLine(TextBlock text, double lineHeight) { text.TextWrapping = TextWrapping.NoWrap; text.TextTrimming = TextTrimming.CharacterEllipsis; text.LineHeight = lineHeight; text.VerticalAlignment = VerticalAlignment.Center; } private static UIElement BuildDetailLine(string label, string value, string glyph) { var grid = new Grid { ColumnSpacing = 10 }; grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); grid.ColumnDefinitions.Add(new ColumnDefinition()); grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); grid.Children.Add(ModernUi.IconTile(glyph, 30, ModernUi.SurfaceAlt, ModernUi.TextSecondary, 13)); var title = ModernUi.Text(label, 13, FontWeights.SemiBold, ModernUi.TextSecondary, maxLines: 1); Grid.SetColumn(title, 1); grid.Children.Add(title); var text = ModernUi.Text(value, 13, FontWeights.SemiBold, ModernUi.TextPrimary, maxLines: 1); Grid.SetColumn(text, 2); grid.Children.Add(text); return grid; } }