Add WinUI and core source
build-winui / winui (push) Has been cancelled

This commit is contained in:
QWQLwToo
2026-06-26 13:27:13 +08:00
parent f59190251d
commit 7ecc6a8923
262 changed files with 137492 additions and 0 deletions
@@ -0,0 +1,382 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Animation;
using Microsoft.UI.Xaml.Shapes;
using Windows.Foundation;
using Windows.UI.ViewManagement;
using YMhut.Box.WinUI.Services;
namespace YMhut.Box.WinUI.Controls;
public sealed class AnimatedWeatherIconControl : UserControl
{
private readonly Canvas _canvas = new();
private readonly AccessibilitySettings _accessibility = new();
private Storyboard? _storyboard;
private WeatherVisualKind _kind = WeatherVisualKind.Unknown;
private WeatherIntensity _intensity = WeatherIntensity.None;
private bool _animationEnabled = true;
public AnimatedWeatherIconControl(double size = 20)
{
Width = size;
Height = size;
MinWidth = size;
MinHeight = size;
_canvas.Width = size;
_canvas.Height = size;
_canvas.IsHitTestVisible = false;
Content = _canvas;
Loaded += (_, _) => Render();
SizeChanged += (_, _) => Render();
Unloaded += (_, _) => StopStoryboard();
}
public void Update(TitleWeatherSnapshot snapshot, bool animationEnabled)
{
_kind = snapshot.VisualKind;
_intensity = snapshot.Intensity;
_animationEnabled = animationEnabled;
Render();
}
private void Render()
{
StopStoryboard();
_canvas.Children.Clear();
var size = Math.Max(16, ActualWidth > 0 ? ActualWidth : Width > 0 ? Width : 20);
_canvas.Width = size;
_canvas.Height = size;
_canvas.Clip = new RectangleGeometry { Rect = new Rect(0, 0, size, size) };
_storyboard = new Storyboard();
switch (_kind)
{
case WeatherVisualKind.Clear:
AddSun(size, 0.5, 0.5, 0.23, animate: true);
break;
case WeatherVisualKind.PartlyCloudy:
AddSun(size, 0.34, 0.35, 0.18, animate: true, opacity: 0.9);
AddCloud(size, 0.20, 0.38, 0.68, 0.38, opacity: 0.95, animate: true);
break;
case WeatherVisualKind.Cloudy:
AddCloud(size, 0.13, 0.34, 0.74, 0.42, opacity: 1, animate: true);
break;
case WeatherVisualKind.Fog:
AddCloud(size, 0.16, 0.25, 0.70, 0.34, opacity: 0.9, animate: false);
AddFog(size);
break;
case WeatherVisualKind.Drizzle:
case WeatherVisualKind.Rain:
case WeatherVisualKind.FreezingRain:
case WeatherVisualKind.Showers:
AddCloud(size, 0.14, 0.20, 0.72, 0.36, opacity: 1, animate: false);
AddRain(size, DropsFor(_intensity), _kind == WeatherVisualKind.Drizzle);
break;
case WeatherVisualKind.Snow:
case WeatherVisualKind.SnowGrains:
case WeatherVisualKind.SnowShowers:
AddCloud(size, 0.14, 0.20, 0.72, 0.36, opacity: 1, animate: false);
AddSnow(size, DropsFor(_intensity));
break;
case WeatherVisualKind.Thunderstorm:
AddCloud(size, 0.14, 0.18, 0.72, 0.36, opacity: 1, animate: false);
AddRain(size, Math.Max(3, DropsFor(_intensity)), drizzle: false);
AddBolt(size);
break;
default:
AddCloud(size, 0.18, 0.30, 0.64, 0.38, opacity: 0.72, animate: true);
AddUnknownMark(size);
break;
}
if (CanAnimate() && _storyboard.Children.Count > 0)
{
_storyboard.Begin();
}
}
private void AddSun(double size, double cx, double cy, double radius, bool animate, double opacity = 1)
{
var centerX = size * cx;
var centerY = size * cy;
var r = size * radius;
var sun = AddEllipse(centerX - r, centerY - r, r * 2, r * 2, ModernUi.Bronze, opacity);
for (var i = 0; i < 8; i++)
{
var angle = i * Math.PI / 4;
var inner = r * 1.35;
var outer = r * 1.75;
AddLine(
centerX + Math.Cos(angle) * inner,
centerY + Math.Sin(angle) * inner,
centerX + Math.Cos(angle) * outer,
centerY + Math.Sin(angle) * outer,
ModernUi.Bronze,
Math.Max(1.1, size * 0.055),
opacity * 0.72);
}
if (animate && CanAnimate())
{
AnimatePulse(sun, 0.76, 1, 1800);
}
}
private void AddCloud(double size, double left, double top, double width, double height, double opacity, bool animate)
{
var x = size * left;
var y = size * top;
var w = size * width;
var h = size * height;
var fill = CloudBrush();
var parts = new UIElement[]
{
AddEllipse(x + w * 0.05, y + h * 0.35, w * 0.32, h * 0.46, fill, opacity),
AddEllipse(x + w * 0.24, y + h * 0.10, w * 0.36, h * 0.62, fill, opacity),
AddEllipse(x + w * 0.48, y + h * 0.24, w * 0.34, h * 0.52, fill, opacity),
AddRoundedRect(x + w * 0.10, y + h * 0.48, w * 0.74, h * 0.34, fill, opacity)
};
if (animate && CanAnimate())
{
foreach (var part in parts)
{
var transform = new TranslateTransform();
part.RenderTransform = transform;
AnimateDrift(transform, size * 0.035, 2300, 0);
}
}
}
private void AddFog(double size)
{
for (var index = 0; index < 3; index++)
{
var band = AddRoundedRect(
size * (0.18 + index * 0.04),
size * (0.58 + index * 0.12),
size * (0.62 - index * 0.03),
Math.Max(1.5, size * 0.065),
ModernUi.TextSecondary,
0.62);
if (CanAnimate())
{
var transform = new TranslateTransform();
band.RenderTransform = transform;
AnimateDrift(transform, size * 0.06, 1800 + index * 180, index * 90);
}
}
}
private void AddRain(double size, int count, bool drizzle)
{
var duration = _intensity == WeatherIntensity.Heavy ? 520 : _intensity == WeatherIntensity.Moderate ? 720 : 940;
for (var index = 0; index < count; index++)
{
var drop = AddRoundedRect(
size * (0.28 + index * 0.12),
size * (0.58 + index % 2 * 0.04),
Math.Max(1.1, size * 0.055),
size * (drizzle ? 0.14 : 0.22),
ModernUi.Accent,
0.82);
RotateElement(drop, -15);
if (CanAnimate())
{
var transform = new TranslateTransform();
drop.RenderTransform = transform;
AnimateDrop(transform, drop, size * 0.16, duration, index * 120);
}
}
}
private void AddSnow(double size, int count)
{
var duration = _intensity == WeatherIntensity.Heavy ? 780 : _intensity == WeatherIntensity.Moderate ? 980 : 1250;
for (var index = 0; index < count; index++)
{
var flake = AddEllipse(
size * (0.25 + index * 0.13),
size * (0.60 + index % 2 * 0.06),
Math.Max(2.2, size * 0.10),
Math.Max(2.2, size * 0.10),
ModernUi.Accent,
0.78);
if (CanAnimate())
{
var transform = new TranslateTransform();
flake.RenderTransform = transform;
AnimateDrop(transform, flake, size * 0.14, duration, index * 150);
}
}
}
private void AddBolt(double size)
{
var bolt = new Polyline
{
Stroke = ModernUi.Bronze,
StrokeThickness = Math.Max(1.7, size * 0.08),
StrokeLineJoin = PenLineJoin.Round,
Points =
{
new Point(size * 0.52, size * 0.48),
new Point(size * 0.42, size * 0.70),
new Point(size * 0.55, size * 0.68),
new Point(size * 0.48, size * 0.90)
},
Opacity = 0.95
};
_canvas.Children.Add(bolt);
if (CanAnimate())
{
AnimatePulse(bolt, 0.28, 1, 620);
}
}
private void AddUnknownMark(double size)
{
AddLine(size * 0.40, size * 0.44, size * 0.60, size * 0.64, ModernUi.TextSecondary, Math.Max(1.4, size * 0.07), 0.72);
AddLine(size * 0.60, size * 0.44, size * 0.40, size * 0.64, ModernUi.TextSecondary, Math.Max(1.4, size * 0.07), 0.72);
}
private Ellipse AddEllipse(double left, double top, double width, double height, Brush fill, double opacity)
{
var shape = new Ellipse
{
Width = width,
Height = height,
Fill = fill,
Opacity = opacity,
RenderTransformOrigin = new Point(0.5, 0.5)
};
Canvas.SetLeft(shape, left);
Canvas.SetTop(shape, top);
_canvas.Children.Add(shape);
return shape;
}
private Border AddRoundedRect(double left, double top, double width, double height, Brush fill, double opacity)
{
var border = new Border
{
Width = width,
Height = height,
CornerRadius = new CornerRadius(Math.Max(2, height / 2)),
Background = fill,
Opacity = opacity,
RenderTransformOrigin = new Point(0.5, 0.5)
};
Canvas.SetLeft(border, left);
Canvas.SetTop(border, top);
_canvas.Children.Add(border);
return border;
}
private Line AddLine(double x1, double y1, double x2, double y2, Brush stroke, double thickness, double opacity)
{
var line = new Line
{
X1 = x1,
Y1 = y1,
X2 = x2,
Y2 = y2,
Stroke = stroke,
StrokeThickness = thickness,
StrokeStartLineCap = PenLineCap.Round,
StrokeEndLineCap = PenLineCap.Round,
Opacity = opacity
};
_canvas.Children.Add(line);
return line;
}
private void RotateElement(UIElement element, double angle)
{
element.RenderTransformOrigin = new Point(0.5, 0.5);
element.RenderTransform = new RotateTransform { Angle = angle };
}
private bool CanAnimate()
=> _animationEnabled && !_accessibility.HighContrast;
private void AnimatePulse(UIElement target, double from, double to, double durationMs)
{
var animation = new DoubleAnimation
{
From = from,
To = to,
AutoReverse = true,
Duration = new Duration(TimeSpan.FromMilliseconds(durationMs)),
RepeatBehavior = RepeatBehavior.Forever
};
Storyboard.SetTarget(animation, target);
Storyboard.SetTargetProperty(animation, "Opacity");
_storyboard?.Children.Add(animation);
}
private void AnimateDrop(TranslateTransform transform, UIElement target, double distance, double durationMs, double delayMs)
{
var move = new DoubleAnimation
{
From = -distance * 0.35,
To = distance,
BeginTime = TimeSpan.FromMilliseconds(delayMs),
Duration = new Duration(TimeSpan.FromMilliseconds(durationMs)),
RepeatBehavior = RepeatBehavior.Forever
};
Storyboard.SetTarget(move, transform);
Storyboard.SetTargetProperty(move, "Y");
_storyboard?.Children.Add(move);
var fade = new DoubleAnimation
{
From = 0.2,
To = 0.9,
AutoReverse = true,
BeginTime = TimeSpan.FromMilliseconds(delayMs),
Duration = new Duration(TimeSpan.FromMilliseconds(durationMs * 0.55)),
RepeatBehavior = RepeatBehavior.Forever
};
Storyboard.SetTarget(fade, target);
Storyboard.SetTargetProperty(fade, "Opacity");
_storyboard?.Children.Add(fade);
}
private void AnimateDrift(TranslateTransform transform, double distance, double durationMs, double delayMs)
{
var move = new DoubleAnimation
{
From = -distance,
To = distance,
AutoReverse = true,
BeginTime = TimeSpan.FromMilliseconds(delayMs),
Duration = new Duration(TimeSpan.FromMilliseconds(durationMs)),
RepeatBehavior = RepeatBehavior.Forever
};
Storyboard.SetTarget(move, transform);
Storyboard.SetTargetProperty(move, "X");
_storyboard?.Children.Add(move);
}
private void StopStoryboard()
{
_storyboard?.Stop();
_storyboard = null;
}
private static int DropsFor(WeatherIntensity intensity) => intensity switch
{
WeatherIntensity.Heavy => 5,
WeatherIntensity.Moderate => 4,
WeatherIntensity.Light => 3,
_ => 2
};
private static Brush CloudBrush()
=> ModernUi.TextSecondary;
}
@@ -0,0 +1,204 @@
using Microsoft.UI.Text;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Shapes;
using Windows.Foundation;
namespace YMhut.Box.WinUI.Controls;
public sealed class MetricChartControl : UserControl
{
private const int MaxSamples = 90;
private readonly TextBlock _valueText;
private readonly TextBlock _detailText;
private readonly ProgressBar _bar;
private readonly Canvas _canvas = new() { MinHeight = 142, Height = 152 };
private readonly Polyline _line = new()
{
Stroke = ModernUi.Accent,
StrokeThickness = 2.2
};
private readonly Queue<double> _samples = new();
public MetricChartControl(string title, string subtitle, string glyph)
{
_valueText = ModernUi.Text("--", 24, FontWeights.SemiBold, maxLines: 1);
_valueText.TextAlignment = TextAlignment.Right;
_detailText = ModernUi.Text(subtitle, 12, foreground: ModernUi.TextSecondary, maxLines: 2);
_bar = new ProgressBar
{
Minimum = 0,
Maximum = 100,
Height = 5,
Foreground = ModernUi.Accent,
Background = ModernUi.SurfaceAlt
};
_canvas.SizeChanged += (_, _) => Redraw();
var header = new Grid { ColumnSpacing = 12 };
header.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
header.ColumnDefinitions.Add(new ColumnDefinition());
header.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
header.Children.Add(ModernUi.IconTile(glyph, 38, ModernUi.SurfaceAlt, ModernUi.Accent, 17));
var titleStack = new StackPanel
{
Spacing = 2,
Children =
{
ModernUi.Text(title, 17, FontWeights.SemiBold, maxLines: 1),
_detailText
}
};
Grid.SetColumn(titleStack, 1);
header.Children.Add(titleStack);
Grid.SetColumn(_valueText, 2);
header.Children.Add(_valueText);
Content = ModernUi.Card(new StackPanel
{
Spacing = 12,
Children =
{
header,
_bar,
_canvas
}
}, new Thickness(16), radius: 8);
}
public void Update(double? percent, string valueText, string detailText)
{
_valueText.Text = valueText;
_detailText.Text = detailText;
if (percent is double value)
{
var clamped = Math.Clamp(value, 0, 100);
_bar.IsIndeterminate = false;
_bar.Value = clamped;
_samples.Enqueue(clamped);
while (_samples.Count > MaxSamples)
{
_samples.Dequeue();
}
}
else
{
_bar.Value = 0;
}
Redraw();
}
private void Redraw()
{
var width = _canvas.ActualWidth > 0 ? _canvas.ActualWidth : 320;
var height = _canvas.ActualHeight > 0 ? _canvas.ActualHeight : _canvas.Height;
if (double.IsNaN(height) || height <= 0)
{
height = 152;
}
_canvas.Children.Clear();
_canvas.Children.Add(new Border
{
Width = width,
Height = height,
Background = ChartBackground(),
CornerRadius = new CornerRadius(8)
});
var left = 34d;
var right = 10d;
var top = 10d;
var bottom = 20d;
var plotWidth = Math.Max(12, width - left - right);
var plotHeight = Math.Max(12, height - top - bottom);
DrawGrid(left, top, plotWidth, plotHeight);
var values = _samples.ToArray();
var points = new PointCollection();
if (values.Length == 0 || width <= 0 || height <= 0)
{
_line.Points = points;
_canvas.Children.Add(_line);
return;
}
for (var index = 0; index < values.Length; index++)
{
var x = left + (values.Length == 1 ? plotWidth : index * plotWidth / (values.Length - 1));
var y = top + (100 - values[index]) / 100 * plotHeight;
points.Add(new Point(x, Math.Clamp(y, top, top + plotHeight)));
}
_line.Points = points;
_line.Stroke = ModernUi.Accent;
_canvas.Children.Add(_line);
}
private void DrawGrid(double left, double top, double width, double height)
{
var gridBrush = GridBrush();
var labelBrush = LabelBrush();
foreach (var value in new[] { 100, 75, 50, 25, 0 })
{
var y = top + (100 - value) / 100d * height;
var line = new Line
{
X1 = left,
X2 = left + width,
Y1 = y,
Y2 = y,
Stroke = gridBrush,
StrokeThickness = 1,
Opacity = value is 0 or 100 ? 0.42 : 0.28
};
_canvas.Children.Add(line);
var label = new TextBlock
{
Text = value.ToString(),
FontSize = 10.5,
Foreground = labelBrush,
Opacity = 0.78
};
Canvas.SetLeft(label, 6);
Canvas.SetTop(label, Math.Clamp(y - 8, 2, top + height - 12));
_canvas.Children.Add(label);
}
for (var index = 0; index <= 5; index++)
{
var x = left + index / 5d * width;
_canvas.Children.Add(new Line
{
X1 = x,
X2 = x,
Y1 = top,
Y2 = top + height,
Stroke = gridBrush,
StrokeThickness = 1,
Opacity = index is 0 or 5 ? 0.32 : 0.18
});
}
}
private static SolidColorBrush ChartBackground()
=> IsDarkTheme() ? ModernUi.Brush("#121820") : ModernUi.Brush("#202833");
private static SolidColorBrush GridBrush()
=> IsDarkTheme() ? ModernUi.Brush("#7EA6C7") : ModernUi.Brush("#9FB4C8");
private static SolidColorBrush LabelBrush()
=> IsDarkTheme() ? ModernUi.Brush("#C9D8E6") : ModernUi.Brush("#D7E4F0");
private static bool IsDarkTheme()
{
var color = ModernUi.AppBackground.Color;
return color.R + color.G + color.B < 384;
}
}
@@ -0,0 +1,160 @@
using Microsoft.UI.Text;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Automation;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Shapes;
using Windows.Foundation;
namespace YMhut.Box.WinUI.Controls;
public sealed class MetricStripControl : UserControl
{
private const int MaxSamples = 30;
private readonly TextBlock _valueText;
private readonly ProgressBar _bar;
private readonly Canvas _trendCanvas = new() { Width = 58, Height = 18 };
private readonly Polyline _trendLine = new()
{
Stroke = ModernUi.Accent,
StrokeThickness = 1.5
};
private readonly Queue<double> _samples = new();
public MetricStripControl(string title, string glyph)
{
var icon = ModernUi.IconTile(glyph, 28, ModernUi.SurfaceAlt, ModernUi.TextSecondary, 12);
_valueText = ModernUi.Text("--", 13, FontWeights.SemiBold, maxLines: 1);
_valueText.TextAlignment = TextAlignment.Right;
_bar = new ProgressBar
{
Minimum = 0,
Maximum = 100,
Height = 4,
IsIndeterminate = false,
Foreground = ModernUi.Accent,
Background = ModernUi.SurfaceAlt
};
_trendCanvas.SizeChanged += (_, _) => RedrawTrend();
var header = new Grid { ColumnSpacing = 8 };
header.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
header.ColumnDefinitions.Add(new ColumnDefinition());
header.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
header.Children.Add(icon);
var titleText = ModernUi.Text(title, 12, FontWeights.SemiBold, ModernUi.TextSecondary, maxLines: 1);
Grid.SetColumn(titleText, 1);
header.Children.Add(titleText);
Grid.SetColumn(_valueText, 2);
header.Children.Add(_valueText);
var bottom = new Grid { ColumnSpacing = 8 };
bottom.ColumnDefinitions.Add(new ColumnDefinition());
bottom.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
bottom.Children.Add(_bar);
Grid.SetColumn(_trendCanvas, 1);
bottom.Children.Add(_trendCanvas);
Content = new StackPanel
{
Spacing = 5,
Children =
{
header,
bottom
}
};
AutomationProperties.SetName(this, title);
}
public void Update(double? percent, string valueText)
{
_valueText.Text = valueText;
if (percent is double value)
{
var clamped = Math.Clamp(value, 0, 100);
_bar.IsIndeterminate = false;
_bar.Value = clamped;
_samples.Enqueue(clamped);
while (_samples.Count > MaxSamples)
{
_samples.Dequeue();
}
}
else
{
_bar.Value = 0;
}
RedrawTrend();
}
private void RedrawTrend()
{
var width = _trendCanvas.ActualWidth > 0 ? _trendCanvas.ActualWidth : _trendCanvas.Width;
var height = _trendCanvas.ActualHeight > 0 ? _trendCanvas.ActualHeight : _trendCanvas.Height;
_trendCanvas.Children.Clear();
DrawTrendGrid(width, height);
var values = _samples.ToArray();
var points = new PointCollection();
if (values.Length == 0 || width <= 0 || height <= 0)
{
_trendLine.Points = points;
_trendCanvas.Children.Add(_trendLine);
return;
}
for (var index = 0; index < values.Length; index++)
{
var x = values.Length == 1 ? width : index * width / (values.Length - 1);
var y = height - (values[index] / 100 * height);
points.Add(new Point(x, Math.Clamp(y, 1, height - 1)));
}
_trendLine.Points = points;
_trendLine.Stroke = ModernUi.Accent;
_trendCanvas.Children.Add(_trendLine);
}
private void DrawTrendGrid(double width, double height)
{
if (width <= 0 || height <= 0)
{
return;
}
var gridBrush = ModernUi.TextSecondary;
foreach (var fraction in new[] { 0.25, 0.5, 0.75 })
{
var y = height * fraction;
_trendCanvas.Children.Add(new Line
{
X1 = 0,
X2 = width,
Y1 = y,
Y2 = y,
Stroke = gridBrush,
StrokeThickness = 1,
Opacity = 0.18
});
}
foreach (var fraction in new[] { 0.33, 0.66 })
{
var x = width * fraction;
_trendCanvas.Children.Add(new Line
{
X1 = x,
X2 = x,
Y1 = 0,
Y2 = height,
Stroke = gridBrush,
StrokeThickness = 1,
Opacity = 0.12
});
}
}
}
@@ -0,0 +1,268 @@
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<ISettingsService>();
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;
}
}