Add WinUI and core source
build-winui / winui (push) Has been cancelled
build-winui / winui (push) Has been cancelled
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user