mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-22 00:54:26 +08:00
0.2.2
时钟组件的完善。
This commit is contained in:
78
LanMontainDesktop/Views/Components/AnalogClockWidget.axaml
Normal file
78
LanMontainDesktop/Views/Components/AnalogClockWidget.axaml
Normal file
@@ -0,0 +1,78 @@
|
||||
<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"
|
||||
mc:Ignorable="d"
|
||||
d:DesignWidth="320"
|
||||
d:DesignHeight="320"
|
||||
x:Class="LanMontainDesktop.Views.Components.AnalogClockWidget">
|
||||
|
||||
<Border x:Name="RootBorder"
|
||||
CornerRadius="42"
|
||||
ClipToBounds="True"
|
||||
Padding="14">
|
||||
<Border.Background>
|
||||
<LinearGradientBrush StartPoint="0,0"
|
||||
EndPoint="1,1">
|
||||
<GradientStop Color="#1F2C4B"
|
||||
Offset="0" />
|
||||
<GradientStop Color="#131B33"
|
||||
Offset="1" />
|
||||
</LinearGradientBrush>
|
||||
</Border.Background>
|
||||
|
||||
<Viewbox Stretch="Uniform">
|
||||
<Grid x:Name="LayoutRoot"
|
||||
Width="300"
|
||||
Height="300">
|
||||
<Grid Width="258"
|
||||
Height="258"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center">
|
||||
<Border x:Name="DialBorder"
|
||||
CornerRadius="129"
|
||||
Background="#F4F4F4"
|
||||
BorderBrush="#E5E5E5"
|
||||
BorderThickness="1" />
|
||||
|
||||
<Canvas x:Name="TickCanvas"
|
||||
Width="258"
|
||||
Height="258"
|
||||
IsHitTestVisible="False" />
|
||||
|
||||
<Canvas x:Name="NumberCanvas"
|
||||
Width="258"
|
||||
Height="258"
|
||||
IsHitTestVisible="False" />
|
||||
|
||||
<Canvas x:Name="HandsCanvas"
|
||||
Width="258"
|
||||
Height="258"
|
||||
IsHitTestVisible="False" />
|
||||
|
||||
<TextBlock x:Name="CityTextBlock"
|
||||
Text="北京"
|
||||
FontSize="21"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="#757575"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,76,0,0" />
|
||||
|
||||
<Ellipse x:Name="CenterDotOuter"
|
||||
Width="18"
|
||||
Height="18"
|
||||
Fill="#1E3C6A"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center" />
|
||||
|
||||
<Ellipse x:Name="CenterDotInner"
|
||||
Width="8"
|
||||
Height="8"
|
||||
Fill="#1A74F2"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Viewbox>
|
||||
</Border>
|
||||
</UserControl>
|
||||
330
LanMontainDesktop/Views/Components/AnalogClockWidget.axaml.cs
Normal file
330
LanMontainDesktop/Views/Components/AnalogClockWidget.axaml.cs
Normal file
@@ -0,0 +1,330 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Shapes;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Styling;
|
||||
using Avalonia.Threading;
|
||||
using LanMontainDesktop.Services;
|
||||
|
||||
namespace LanMontainDesktop.Views.Components;
|
||||
|
||||
public partial class AnalogClockWidget : UserControl
|
||||
{
|
||||
private readonly DispatcherTimer _timer = new()
|
||||
{
|
||||
Interval = TimeSpan.FromSeconds(1)
|
||||
};
|
||||
|
||||
private const double DialSize = 258;
|
||||
private const double Center = DialSize / 2;
|
||||
|
||||
private TimeZoneService? _timeZoneService;
|
||||
private double _currentCellSize = 48;
|
||||
private bool _dialInitialized;
|
||||
private bool _handsInitialized;
|
||||
private bool? _isNightModeApplied;
|
||||
private readonly Line _hourHandLine = CreateHandLine("#1A2A46", 12);
|
||||
private readonly Line _minuteHandLine = CreateHandLine("#29406B", 8);
|
||||
private readonly Line _secondHandLine = CreateHandLine("#1A74F2", 4);
|
||||
|
||||
public AnalogClockWidget()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
_timer.Tick += OnTimerTick;
|
||||
AttachedToVisualTree += OnAttachedToVisualTree;
|
||||
DetachedFromVisualTree += OnDetachedFromVisualTree;
|
||||
SizeChanged += OnSizeChanged;
|
||||
|
||||
InitializeDialIfNeeded();
|
||||
InitializeHandsIfNeeded();
|
||||
UpdateClock();
|
||||
}
|
||||
|
||||
public void SetTimeZoneService(TimeZoneService timeZoneService)
|
||||
{
|
||||
if (_timeZoneService is not null)
|
||||
{
|
||||
_timeZoneService.TimeZoneChanged -= OnTimeZoneChanged;
|
||||
}
|
||||
|
||||
_timeZoneService = timeZoneService;
|
||||
_timeZoneService.TimeZoneChanged += OnTimeZoneChanged;
|
||||
UpdateClock();
|
||||
}
|
||||
|
||||
private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
InitializeDialIfNeeded();
|
||||
InitializeHandsIfNeeded();
|
||||
UpdateClock();
|
||||
_timer.Start();
|
||||
}
|
||||
|
||||
private void OnDetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
_timer.Stop();
|
||||
}
|
||||
|
||||
private void OnSizeChanged(object? sender, SizeChangedEventArgs e)
|
||||
{
|
||||
ApplyCellSize(_currentCellSize);
|
||||
}
|
||||
|
||||
private void OnTimerTick(object? sender, EventArgs e)
|
||||
{
|
||||
UpdateClock();
|
||||
}
|
||||
|
||||
private void OnTimeZoneChanged(object? sender, EventArgs e)
|
||||
{
|
||||
UpdateClock();
|
||||
}
|
||||
|
||||
private void InitializeDialIfNeeded()
|
||||
{
|
||||
if (_dialInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
BuildTicks(isNightMode: true);
|
||||
BuildNumbers(isNightMode: true);
|
||||
_dialInitialized = true;
|
||||
}
|
||||
|
||||
private void InitializeHandsIfNeeded()
|
||||
{
|
||||
if (_handsInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
HandsCanvas.Children.Clear();
|
||||
HandsCanvas.Children.Add(_hourHandLine);
|
||||
HandsCanvas.Children.Add(_minuteHandLine);
|
||||
HandsCanvas.Children.Add(_secondHandLine);
|
||||
_handsInitialized = true;
|
||||
}
|
||||
|
||||
private void BuildTicks(bool isNightMode)
|
||||
{
|
||||
TickCanvas.Children.Clear();
|
||||
var majorBrush = CreateBrush(isNightMode ? "#1A1A1A" : "#1E2430");
|
||||
var minorBrush = CreateBrush(isNightMode ? "#D0D0D0" : "#D7DCE5");
|
||||
var majorThickness = isNightMode ? 3.0 : 2.8;
|
||||
var minorThickness = isNightMode ? 1.4 : 1.2;
|
||||
|
||||
for (var i = 0; i < 60; i++)
|
||||
{
|
||||
var angle = (i * 6 - 90) * Math.PI / 180d;
|
||||
var isHourTick = i % 5 == 0;
|
||||
var outerRadius = Center - 7;
|
||||
var innerRadius = outerRadius - (isHourTick ? 16 : 8);
|
||||
|
||||
var x1 = Center + Math.Cos(angle) * innerRadius;
|
||||
var y1 = Center + Math.Sin(angle) * innerRadius;
|
||||
var x2 = Center + Math.Cos(angle) * outerRadius;
|
||||
var y2 = Center + Math.Sin(angle) * outerRadius;
|
||||
|
||||
var tick = new Line
|
||||
{
|
||||
StartPoint = new Point(x1, y1),
|
||||
EndPoint = new Point(x2, y2),
|
||||
Stroke = isHourTick ? majorBrush : minorBrush,
|
||||
StrokeThickness = isHourTick ? majorThickness : minorThickness,
|
||||
StrokeLineCap = PenLineCap.Round
|
||||
};
|
||||
|
||||
TickCanvas.Children.Add(tick);
|
||||
}
|
||||
}
|
||||
|
||||
private void BuildNumbers(bool isNightMode)
|
||||
{
|
||||
NumberCanvas.Children.Clear();
|
||||
var foreground = CreateBrush(isNightMode ? "#101010" : "#0F131A");
|
||||
var fontWeight = isNightMode ? FontWeight.Bold : FontWeight.SemiBold;
|
||||
|
||||
for (var number = 1; number <= 12; number++)
|
||||
{
|
||||
var angle = (number * 30 - 90) * Math.PI / 180d;
|
||||
var radius = 88;
|
||||
var x = Center + Math.Cos(angle) * radius;
|
||||
var y = Center + Math.Sin(angle) * radius;
|
||||
var isDoubleDigit = number >= 10;
|
||||
var width = isDoubleDigit ? 44 : 28;
|
||||
var height = 34;
|
||||
|
||||
var text = new TextBlock
|
||||
{
|
||||
Text = number.ToString(CultureInfo.InvariantCulture),
|
||||
Width = width,
|
||||
Height = height,
|
||||
TextAlignment = TextAlignment.Center,
|
||||
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center,
|
||||
FontSize = 18,
|
||||
FontWeight = fontWeight,
|
||||
Foreground = foreground
|
||||
};
|
||||
|
||||
Canvas.SetLeft(text, x - width / 2d);
|
||||
Canvas.SetTop(text, y - height / 2d);
|
||||
NumberCanvas.Children.Add(text);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateClock()
|
||||
{
|
||||
ApplyModeVisualIfNeeded();
|
||||
|
||||
var now = _timeZoneService?.GetCurrentTime() ?? DateTime.Now;
|
||||
var hourAngle = (now.Hour % 12 + now.Minute / 60d + now.Second / 3600d) * 30d;
|
||||
var minuteAngle = (now.Minute + now.Second / 60d) * 6d;
|
||||
var secondAngle = (now.Second + now.Millisecond / 1000d) * 6d;
|
||||
|
||||
SetHandGeometry(_hourHandLine, hourAngle, forwardLength: 52, backwardLength: 6);
|
||||
SetHandGeometry(_minuteHandLine, minuteAngle, forwardLength: 76, backwardLength: 8);
|
||||
SetHandGeometry(_secondHandLine, secondAngle, forwardLength: 94, backwardLength: 18);
|
||||
|
||||
var isZh = CultureInfo.CurrentCulture.TwoLetterISOLanguageName.Equals("zh", StringComparison.OrdinalIgnoreCase);
|
||||
CityTextBlock.Text = isZh ? "\u5317\u4eac" : "Beijing";
|
||||
}
|
||||
|
||||
private void ApplyModeVisualIfNeeded()
|
||||
{
|
||||
var isNightMode = ResolveIsNightMode();
|
||||
if (_isNightModeApplied.HasValue && _isNightModeApplied.Value == isNightMode)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_isNightModeApplied = isNightMode;
|
||||
ApplyModeVisual(isNightMode);
|
||||
}
|
||||
|
||||
private void ApplyModeVisual(bool isNightMode)
|
||||
{
|
||||
RootBorder.Background = isNightMode
|
||||
? CreateLinearGradientBrush("#1F2C4B", "#131B33")
|
||||
: CreateLinearGradientBrush("#EEF2FA", "#E7ECF6");
|
||||
|
||||
DialBorder.Background = CreateBrush(isNightMode ? "#F4F4F4" : "#FEFEFF");
|
||||
DialBorder.BorderBrush = CreateBrush(isNightMode ? "#E5E5E5" : "#DCE2EB");
|
||||
|
||||
CityTextBlock.Foreground = CreateBrush(isNightMode ? "#757575" : "#7E8593");
|
||||
CenterDotOuter.Fill = CreateBrush(isNightMode ? "#1E3C6A" : "#30486E");
|
||||
CenterDotInner.Fill = CreateBrush("#1A74F2");
|
||||
|
||||
_hourHandLine.Stroke = CreateBrush(isNightMode ? "#1A2A46" : "#2E3F5F");
|
||||
_minuteHandLine.Stroke = CreateBrush(isNightMode ? "#29406B" : "#3E557E");
|
||||
_secondHandLine.Stroke = CreateBrush("#1A74F2");
|
||||
|
||||
BuildTicks(isNightMode);
|
||||
BuildNumbers(isNightMode);
|
||||
}
|
||||
|
||||
private static void SetHandGeometry(Line hand, double angleDeg, double forwardLength, double backwardLength)
|
||||
{
|
||||
var radians = (angleDeg - 90) * Math.PI / 180d;
|
||||
var cos = Math.Cos(radians);
|
||||
var sin = Math.Sin(radians);
|
||||
|
||||
var start = new Point(
|
||||
Center - cos * backwardLength,
|
||||
Center - sin * backwardLength);
|
||||
var end = new Point(
|
||||
Center + cos * forwardLength,
|
||||
Center + sin * forwardLength);
|
||||
|
||||
hand.StartPoint = start;
|
||||
hand.EndPoint = end;
|
||||
}
|
||||
|
||||
public void ApplyCellSize(double cellSize)
|
||||
{
|
||||
_currentCellSize = Math.Max(1, cellSize);
|
||||
var scale = ResolveScale();
|
||||
|
||||
RootBorder.CornerRadius = new CornerRadius(Math.Clamp(42 * scale, 16, 56));
|
||||
RootBorder.Padding = new Thickness(Math.Clamp(14 * scale, 8, 26));
|
||||
ApplyModeVisualIfNeeded();
|
||||
}
|
||||
|
||||
private double ResolveScale()
|
||||
{
|
||||
var cellScale = Math.Clamp(_currentCellSize / 44d, 0.60, 1.90);
|
||||
var heightScale = Bounds.Height > 1 ? Math.Clamp(Bounds.Height / 300d, 0.58, 2.0) : 1;
|
||||
var widthScale = Bounds.Width > 1 ? Math.Clamp(Bounds.Width / 300d, 0.58, 2.0) : 1;
|
||||
return Math.Clamp(Math.Min(cellScale, Math.Min(heightScale, widthScale) * 1.05), 0.58, 1.95);
|
||||
}
|
||||
|
||||
private static IBrush CreateBrush(string colorHex)
|
||||
{
|
||||
return new SolidColorBrush(Color.Parse(colorHex));
|
||||
}
|
||||
|
||||
private static IBrush CreateLinearGradientBrush(string fromColorHex, string toColorHex)
|
||||
{
|
||||
return new LinearGradientBrush
|
||||
{
|
||||
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
|
||||
EndPoint = new RelativePoint(1, 1, RelativeUnit.Relative),
|
||||
GradientStops = new GradientStops
|
||||
{
|
||||
new GradientStop(Color.Parse(fromColorHex), 0),
|
||||
new GradientStop(Color.Parse(toColorHex), 1)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static Line CreateHandLine(string strokeHex, double thickness)
|
||||
{
|
||||
return new Line
|
||||
{
|
||||
StartPoint = new Point(Center, Center),
|
||||
EndPoint = new Point(Center, Center - 40),
|
||||
Stroke = CreateBrush(strokeHex),
|
||||
StrokeThickness = thickness,
|
||||
StrokeLineCap = PenLineCap.Round
|
||||
};
|
||||
}
|
||||
|
||||
private bool ResolveIsNightMode()
|
||||
{
|
||||
if (ActualThemeVariant == ThemeVariant.Dark)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ActualThemeVariant == ThemeVariant.Light)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.TryFindResource("AdaptiveSurfaceBaseBrush", out var value) &&
|
||||
value is ISolidColorBrush solidBrush)
|
||||
{
|
||||
return CalculateRelativeLuminance(solidBrush.Color) < 0.45;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<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"
|
||||
mc:Ignorable="d"
|
||||
d:DesignWidth="300"
|
||||
d:DesignHeight="300"
|
||||
x:Class="LanMontainDesktop.Views.Components.HolidayCalendarWidget">
|
||||
|
||||
<Border x:Name="RootBorder"
|
||||
Background="#DCE7FA"
|
||||
CornerRadius="34"
|
||||
ClipToBounds="True"
|
||||
Padding="14">
|
||||
<Viewbox Stretch="Uniform">
|
||||
<Grid x:Name="LayoutRoot"
|
||||
Width="300"
|
||||
Height="300"
|
||||
RowDefinitions="Auto,Auto,Auto,Auto,Auto"
|
||||
RowSpacing="8">
|
||||
<TextBlock x:Name="TitleTextBlock"
|
||||
Grid.Row="0"
|
||||
Text="Holiday countdown"
|
||||
FontSize="24"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="#61697C"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,10,0,0" />
|
||||
|
||||
<TextBlock x:Name="CountTextBlock"
|
||||
Grid.Row="1"
|
||||
Text="0"
|
||||
FontFeatures="tnum"
|
||||
FontSize="120"
|
||||
FontWeight="Bold"
|
||||
Foreground="#0A0A0A"
|
||||
HorizontalAlignment="Center" />
|
||||
|
||||
<Canvas Grid.Row="2"
|
||||
Width="260"
|
||||
Height="40"
|
||||
HorizontalAlignment="Center">
|
||||
<Path Data="M 10,16 C 68,11 192,11 250,16"
|
||||
Stroke="#1A73F0"
|
||||
StrokeThickness="12"
|
||||
Opacity="0.97"
|
||||
StrokeLineCap="Round" />
|
||||
<Path Data="M 30,19 C 96,22 164,22 230,19"
|
||||
Stroke="#5C9AF7"
|
||||
StrokeThickness="3.2"
|
||||
Opacity="0.55"
|
||||
StrokeLineCap="Round" />
|
||||
<Path Data="M 104,28 C 118,23 142,23 156,28 C 146,32 114,33 104,28 Z"
|
||||
Fill="#1A73F0"
|
||||
Opacity="0.92" />
|
||||
<Ellipse Width="58"
|
||||
Height="4.5"
|
||||
Fill="#5C9AF7"
|
||||
Canvas.Left="101"
|
||||
Canvas.Top="27.5"
|
||||
Opacity="0.35" />
|
||||
</Canvas>
|
||||
|
||||
<Grid Grid.Row="3"
|
||||
ColumnDefinitions="*,Auto,*"
|
||||
Margin="8,0,8,0"
|
||||
VerticalAlignment="Center">
|
||||
<Border Grid.Column="0"
|
||||
Height="2"
|
||||
Margin="0,0,10,0"
|
||||
VerticalAlignment="Center"
|
||||
Background="#B0B9CB" />
|
||||
<TextBlock x:Name="DayUnitTextBlock"
|
||||
Grid.Column="1"
|
||||
Text="Days"
|
||||
FontSize="56"
|
||||
Foreground="#7D869A"
|
||||
FontWeight="Medium"
|
||||
VerticalAlignment="Center" />
|
||||
<Border Grid.Column="2"
|
||||
Height="2"
|
||||
Margin="10,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
Background="#B0B9CB" />
|
||||
</Grid>
|
||||
|
||||
<TextBlock x:Name="DateTextBlock"
|
||||
Grid.Row="4"
|
||||
Text="2024-10-01"
|
||||
FontSize="34"
|
||||
Foreground="#596177"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,2,0,8" />
|
||||
</Grid>
|
||||
</Viewbox>
|
||||
</Border>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,215 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Threading;
|
||||
using LanMontainDesktop.Services;
|
||||
|
||||
namespace LanMontainDesktop.Views.Components;
|
||||
|
||||
public partial class HolidayCalendarWidget : UserControl
|
||||
{
|
||||
private readonly DispatcherTimer _timer = new()
|
||||
{
|
||||
Interval = TimeSpan.FromMinutes(15)
|
||||
};
|
||||
|
||||
private static readonly HolidayCalendarService HolidayService = new();
|
||||
|
||||
private TimeZoneService? _timeZoneService;
|
||||
private double _currentCellSize = 48;
|
||||
private CancellationTokenSource? _refreshCts;
|
||||
private long _refreshVersion;
|
||||
|
||||
public HolidayCalendarWidget()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
_timer.Tick += OnTimerTick;
|
||||
AttachedToVisualTree += OnAttachedToVisualTree;
|
||||
DetachedFromVisualTree += OnDetachedFromVisualTree;
|
||||
SizeChanged += OnSizeChanged;
|
||||
|
||||
TriggerContentRefresh();
|
||||
}
|
||||
|
||||
public void SetTimeZoneService(TimeZoneService timeZoneService)
|
||||
{
|
||||
if (_timeZoneService is not null)
|
||||
{
|
||||
_timeZoneService.TimeZoneChanged -= OnTimeZoneChanged;
|
||||
}
|
||||
|
||||
_timeZoneService = timeZoneService;
|
||||
_timeZoneService.TimeZoneChanged += OnTimeZoneChanged;
|
||||
TriggerContentRefresh();
|
||||
}
|
||||
|
||||
private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
TriggerContentRefresh();
|
||||
_timer.Start();
|
||||
}
|
||||
|
||||
private void OnDetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
_timer.Stop();
|
||||
_refreshCts?.Cancel();
|
||||
_refreshCts?.Dispose();
|
||||
_refreshCts = null;
|
||||
}
|
||||
|
||||
private void OnSizeChanged(object? sender, SizeChangedEventArgs e)
|
||||
{
|
||||
ApplyCellSize(_currentCellSize);
|
||||
}
|
||||
|
||||
private void OnTimerTick(object? sender, EventArgs e)
|
||||
{
|
||||
TriggerContentRefresh();
|
||||
}
|
||||
|
||||
private void OnTimeZoneChanged(object? sender, EventArgs e)
|
||||
{
|
||||
TriggerContentRefresh();
|
||||
}
|
||||
|
||||
private void TriggerContentRefresh()
|
||||
{
|
||||
_refreshCts?.Cancel();
|
||||
_refreshCts?.Dispose();
|
||||
_refreshCts = new CancellationTokenSource();
|
||||
|
||||
var now = _timeZoneService?.GetCurrentTime() ?? DateTime.Now;
|
||||
var isZh = CultureInfo.CurrentCulture.TwoLetterISOLanguageName.Equals("zh", StringComparison.OrdinalIgnoreCase);
|
||||
var version = Interlocked.Increment(ref _refreshVersion);
|
||||
_ = UpdateContentAsync(now, isZh, version, _refreshCts.Token);
|
||||
}
|
||||
|
||||
private async Task UpdateContentAsync(DateTime now, bool isZh, long refreshVersion, CancellationToken cancellationToken)
|
||||
{
|
||||
HolidayDisplayInfo displayInfo;
|
||||
try
|
||||
{
|
||||
displayInfo = await HolidayService.GetDisplayInfoAsync(now, cancellationToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
catch
|
||||
{
|
||||
var fallbackDayType = now.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday
|
||||
? HolidayDayType.Weekend
|
||||
: HolidayDayType.Workday;
|
||||
|
||||
displayInfo = new HolidayDisplayInfo(
|
||||
NextHoliday: HolidayService.GetNextHoliday(now),
|
||||
TodayStatus: new HolidayDayStatus(
|
||||
Date: DateOnly.FromDateTime(now.Date),
|
||||
DayType: fallbackDayType,
|
||||
TypeNameZh: fallbackDayType == HolidayDayType.Weekend ? "\u5468\u672b" : "\u5de5\u4f5c\u65e5",
|
||||
IsHoliday: false,
|
||||
IsAdjustedWorkday: false,
|
||||
NameZh: null,
|
||||
NameEn: null,
|
||||
TargetHolidayZh: null),
|
||||
UsesOnlineData: false);
|
||||
}
|
||||
|
||||
if (cancellationToken.IsCancellationRequested ||
|
||||
refreshVersion != Volatile.Read(ref _refreshVersion))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var holiday = displayInfo.NextHoliday;
|
||||
|
||||
if (holiday is null)
|
||||
{
|
||||
TitleTextBlock.Text = isZh
|
||||
? "\u6682\u65e0\u8282\u5047\u65e5\u6570\u636e"
|
||||
: "No holiday data";
|
||||
CountTextBlock.Text = "--";
|
||||
DayUnitTextBlock.Text = isZh ? "\u5929" : "Days";
|
||||
DateTextBlock.Text = "--";
|
||||
return;
|
||||
}
|
||||
|
||||
var today = DateOnly.FromDateTime(now.Date);
|
||||
var remainDays = Math.Max(0, holiday.Date.DayNumber - today.DayNumber);
|
||||
CountTextBlock.Text = remainDays.ToString(CultureInfo.InvariantCulture);
|
||||
|
||||
if (isZh)
|
||||
{
|
||||
if (remainDays == 0)
|
||||
{
|
||||
TitleTextBlock.Text = $"{holiday.NameZh}\u4eca\u5929";
|
||||
}
|
||||
else
|
||||
{
|
||||
var adjustPrefix = displayInfo.TodayStatus.IsAdjustedWorkday
|
||||
? string.IsNullOrWhiteSpace(displayInfo.TodayStatus.NameZh)
|
||||
? "\u4eca\u65e5\u8c03\u4f11\u8865\u73ed\uff0c"
|
||||
: string.Create(CultureInfo.InvariantCulture, $"\u4eca\u65e5{displayInfo.TodayStatus.NameZh}\uff0c")
|
||||
: string.Empty;
|
||||
TitleTextBlock.Text = string.Create(
|
||||
CultureInfo.InvariantCulture,
|
||||
$"{adjustPrefix}\u8ddd{holiday.NameZh}\u8fd8\u6709");
|
||||
}
|
||||
|
||||
DayUnitTextBlock.Text = "\u5929";
|
||||
|
||||
var holidayDateText = HolidayCalendarService.FormatDate(holiday.Date, isZh: true);
|
||||
DateTextBlock.Text = displayInfo.TodayStatus.IsAdjustedWorkday && remainDays > 0
|
||||
? string.Create(CultureInfo.InvariantCulture, $"{holidayDateText} \u00b7 \u4eca\u65e5\u8865\u73ed")
|
||||
: holidayDateText;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (remainDays == 0)
|
||||
{
|
||||
TitleTextBlock.Text = $"{holiday.NameEn} is today";
|
||||
}
|
||||
else
|
||||
{
|
||||
var adjustPrefix = displayInfo.TodayStatus.IsAdjustedWorkday
|
||||
? "Make-up workday today, "
|
||||
: string.Empty;
|
||||
TitleTextBlock.Text = $"{adjustPrefix}Days to {holiday.NameEn}";
|
||||
}
|
||||
|
||||
DayUnitTextBlock.Text = "Days";
|
||||
|
||||
var holidayDateText = HolidayCalendarService.FormatDate(holiday.Date, isZh: false);
|
||||
DateTextBlock.Text = displayInfo.TodayStatus.IsAdjustedWorkday && remainDays > 0
|
||||
? $"{holidayDateText} - make-up workday"
|
||||
: holidayDateText;
|
||||
}
|
||||
}
|
||||
|
||||
public void ApplyCellSize(double cellSize)
|
||||
{
|
||||
_currentCellSize = Math.Max(1, cellSize);
|
||||
var scale = ResolveScale();
|
||||
|
||||
RootBorder.CornerRadius = new CornerRadius(Math.Clamp(34 * scale, 15, 50));
|
||||
RootBorder.Padding = new Thickness(Math.Clamp(14 * scale, 7, 22));
|
||||
LayoutRoot.RowSpacing = Math.Clamp(8 * scale, 4, 14);
|
||||
|
||||
TitleTextBlock.FontSize = Math.Clamp(24 * scale, 11, 36);
|
||||
CountTextBlock.FontSize = Math.Clamp(120 * scale, 36, 160);
|
||||
DayUnitTextBlock.FontSize = Math.Clamp(56 * scale, 16, 78);
|
||||
DateTextBlock.FontSize = Math.Clamp(34 * scale, 12, 50);
|
||||
}
|
||||
|
||||
private double ResolveScale()
|
||||
{
|
||||
var cellScale = Math.Clamp(_currentCellSize / 44d, 0.60, 1.95);
|
||||
var heightScale = Bounds.Height > 1 ? Math.Clamp(Bounds.Height / 300d, 0.58, 2.0) : 1;
|
||||
var widthScale = Bounds.Width > 1 ? Math.Clamp(Bounds.Width / 300d, 0.58, 2.0) : 1;
|
||||
return Math.Clamp(Math.Min(cellScale, Math.Min(heightScale, widthScale) * 1.05), 0.58, 1.95);
|
||||
}
|
||||
}
|
||||
160
LanMontainDesktop/Views/Components/TimerWidget.axaml
Normal file
160
LanMontainDesktop/Views/Components/TimerWidget.axaml
Normal file
@@ -0,0 +1,160 @@
|
||||
<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"
|
||||
mc:Ignorable="d"
|
||||
d:DesignWidth="320"
|
||||
d:DesignHeight="320"
|
||||
x:Class="LanMontainDesktop.Views.Components.TimerWidget">
|
||||
|
||||
<Border x:Name="RootBorder"
|
||||
CornerRadius="34"
|
||||
ClipToBounds="True"
|
||||
Padding="14"
|
||||
Background="#E8EAEE">
|
||||
<Viewbox Stretch="Uniform">
|
||||
<Grid x:Name="LayoutRoot"
|
||||
Width="300"
|
||||
Height="300">
|
||||
<Border x:Name="TimerPanelBorder"
|
||||
Width="224"
|
||||
Height="224"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
CornerRadius="32"
|
||||
BorderThickness="1">
|
||||
<Grid ColumnDefinitions="96,2,*">
|
||||
<Grid Grid.Column="0"
|
||||
Margin="18,18,8,18"
|
||||
RowDefinitions="Auto,Auto,Auto,Auto,*">
|
||||
<TextBlock x:Name="TopNumberTextBlock"
|
||||
Grid.Row="0"
|
||||
Text="60"
|
||||
FontSize="38"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="#AEB4C1"
|
||||
Margin="0,0,0,10" />
|
||||
<TextBlock x:Name="MainNumberTextBlock"
|
||||
Grid.Row="1"
|
||||
Text="0"
|
||||
FontSize="64"
|
||||
FontWeight="Bold"
|
||||
Foreground="#0F141C" />
|
||||
<TextBlock x:Name="NextNumberTextBlock"
|
||||
Grid.Row="2"
|
||||
Text="1"
|
||||
FontSize="34"
|
||||
FontWeight="Medium"
|
||||
Foreground="#B2B8C4"
|
||||
Margin="0,8,0,0" />
|
||||
<TextBlock x:Name="NextNextNumberTextBlock"
|
||||
Grid.Row="3"
|
||||
Text="2"
|
||||
FontSize="26"
|
||||
FontWeight="Medium"
|
||||
Foreground="#C8CDD7"
|
||||
Margin="0,2,0,0" />
|
||||
</Grid>
|
||||
|
||||
<Border x:Name="CenterDivider"
|
||||
Grid.Column="1"
|
||||
Width="2"
|
||||
Margin="0,12"
|
||||
Background="#D5DAE3" />
|
||||
|
||||
<Grid Grid.Column="2"
|
||||
Margin="10,18,16,16"
|
||||
RowDefinitions="Auto,Auto,Auto,Auto,*,Auto">
|
||||
<Border x:Name="ScaleMark1"
|
||||
Grid.Row="0"
|
||||
Height="3"
|
||||
Width="18"
|
||||
CornerRadius="2"
|
||||
HorizontalAlignment="Left"
|
||||
Background="#D0D6E1" />
|
||||
<Border x:Name="ScaleMark2"
|
||||
Grid.Row="1"
|
||||
Height="3"
|
||||
Width="16"
|
||||
CornerRadius="2"
|
||||
Margin="0,18,0,0"
|
||||
HorizontalAlignment="Left"
|
||||
Background="#D0D6E1" />
|
||||
<Border x:Name="ScaleMark3"
|
||||
Grid.Row="2"
|
||||
Height="3"
|
||||
Width="14"
|
||||
CornerRadius="2"
|
||||
Margin="0,18,0,0"
|
||||
HorizontalAlignment="Left"
|
||||
Background="#D0D6E1" />
|
||||
<Border x:Name="ScaleMark4"
|
||||
Grid.Row="3"
|
||||
Height="3"
|
||||
Width="12"
|
||||
CornerRadius="2"
|
||||
Margin="0,18,0,0"
|
||||
HorizontalAlignment="Left"
|
||||
Background="#D0D6E1" />
|
||||
|
||||
<Border x:Name="PlayButtonBorder"
|
||||
Grid.Row="5"
|
||||
Width="42"
|
||||
Height="42"
|
||||
CornerRadius="21"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Bottom"
|
||||
Background="#00000000"
|
||||
BorderBrush="#D3D9E4"
|
||||
BorderThickness="2"
|
||||
Cursor="Hand">
|
||||
<Viewbox Width="16"
|
||||
Height="16"
|
||||
Stretch="Uniform">
|
||||
<Path x:Name="PlayIconPath"
|
||||
Data="M 0,0 L 0,14 L 11,7 Z"
|
||||
Fill="#98A2B8"
|
||||
Stretch="Uniform" />
|
||||
</Viewbox>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<Canvas Width="224"
|
||||
Height="224"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
IsHitTestVisible="False">
|
||||
<Line x:Name="HandGlowLine"
|
||||
StartPoint="112,112"
|
||||
EndPoint="186,112"
|
||||
Stroke="#FF7A78"
|
||||
StrokeThickness="9"
|
||||
Opacity="0.20"
|
||||
StrokeLineCap="Round" />
|
||||
<Line x:Name="HandLine"
|
||||
StartPoint="112,112"
|
||||
EndPoint="180,112"
|
||||
Stroke="#FF4D63"
|
||||
StrokeThickness="6"
|
||||
StrokeLineCap="Round" />
|
||||
<Ellipse x:Name="CenterDotRing"
|
||||
Width="22"
|
||||
Height="22"
|
||||
Fill="#FDFEFF"
|
||||
Stroke="#E3E8F0"
|
||||
StrokeThickness="2"
|
||||
Canvas.Left="101"
|
||||
Canvas.Top="101" />
|
||||
<Ellipse x:Name="CenterDotCore"
|
||||
Width="8"
|
||||
Height="8"
|
||||
Fill="#FF4D63"
|
||||
Canvas.Left="108"
|
||||
Canvas.Top="108" />
|
||||
</Canvas>
|
||||
</Grid>
|
||||
</Viewbox>
|
||||
</Border>
|
||||
</UserControl>
|
||||
273
LanMontainDesktop/Views/Components/TimerWidget.axaml.cs
Normal file
273
LanMontainDesktop/Views/Components/TimerWidget.axaml.cs
Normal file
@@ -0,0 +1,273 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Shapes;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Styling;
|
||||
using Avalonia.Threading;
|
||||
|
||||
namespace LanMontainDesktop.Views.Components;
|
||||
|
||||
public partial class TimerWidget : UserControl
|
||||
{
|
||||
private const int MaxTimerSeconds = 60;
|
||||
private const double DialSize = 224;
|
||||
private const double Center = DialSize / 2;
|
||||
|
||||
private readonly DispatcherTimer _timer = new()
|
||||
{
|
||||
Interval = TimeSpan.FromSeconds(1)
|
||||
};
|
||||
|
||||
private double _currentCellSize = 48;
|
||||
private bool _isRunning;
|
||||
private int _remainingSeconds;
|
||||
private bool? _isNightModeApplied;
|
||||
|
||||
public TimerWidget()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
_timer.Tick += OnTimerTick;
|
||||
AttachedToVisualTree += OnAttachedToVisualTree;
|
||||
DetachedFromVisualTree += OnDetachedFromVisualTree;
|
||||
SizeChanged += OnSizeChanged;
|
||||
PlayButtonBorder.PointerPressed += OnPlayButtonPointerPressed;
|
||||
|
||||
UpdateVisual();
|
||||
}
|
||||
|
||||
private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
UpdateVisual();
|
||||
_timer.Start();
|
||||
}
|
||||
|
||||
private void OnDetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
_timer.Stop();
|
||||
}
|
||||
|
||||
private void OnSizeChanged(object? sender, SizeChangedEventArgs e)
|
||||
{
|
||||
ApplyCellSize(_currentCellSize);
|
||||
}
|
||||
|
||||
private void OnTimerTick(object? sender, EventArgs e)
|
||||
{
|
||||
ApplyModeVisualIfNeeded();
|
||||
|
||||
if (!_isRunning)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_remainingSeconds > 0)
|
||||
{
|
||||
_remainingSeconds--;
|
||||
}
|
||||
|
||||
if (_remainingSeconds <= 0)
|
||||
{
|
||||
_remainingSeconds = 0;
|
||||
_isRunning = false;
|
||||
}
|
||||
|
||||
UpdateVisual();
|
||||
}
|
||||
|
||||
private void OnPlayButtonPointerPressed(object? sender, PointerPressedEventArgs e)
|
||||
{
|
||||
if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_isRunning)
|
||||
{
|
||||
_isRunning = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_remainingSeconds <= 0)
|
||||
{
|
||||
_remainingSeconds = MaxTimerSeconds;
|
||||
}
|
||||
|
||||
_isRunning = true;
|
||||
}
|
||||
|
||||
UpdateVisual();
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void UpdateVisual()
|
||||
{
|
||||
ApplyModeVisualIfNeeded();
|
||||
UpdateNumberVisual();
|
||||
UpdateHandGeometry();
|
||||
UpdatePlayButtonVisual();
|
||||
}
|
||||
|
||||
private void UpdateNumberVisual()
|
||||
{
|
||||
var current = Math.Clamp(_remainingSeconds, 0, MaxTimerSeconds);
|
||||
var top = current == 0 ? MaxTimerSeconds : current - 1;
|
||||
var next = (current + 1) % (MaxTimerSeconds + 1);
|
||||
var nextNext = (current + 2) % (MaxTimerSeconds + 1);
|
||||
|
||||
TopNumberTextBlock.Text = top.ToString(CultureInfo.InvariantCulture);
|
||||
MainNumberTextBlock.Text = current.ToString(CultureInfo.InvariantCulture);
|
||||
NextNumberTextBlock.Text = next.ToString(CultureInfo.InvariantCulture);
|
||||
NextNextNumberTextBlock.Text = nextNext.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
private void UpdateHandGeometry()
|
||||
{
|
||||
var angleDeg = (_remainingSeconds % (MaxTimerSeconds + 1)) / 60d * 360d;
|
||||
var radians = angleDeg * Math.PI / 180d;
|
||||
var cos = Math.Cos(radians);
|
||||
var sin = Math.Sin(radians);
|
||||
|
||||
var start = new Point(Center - cos * 2, Center - sin * 2);
|
||||
var end = new Point(Center + cos * 68, Center + sin * 68);
|
||||
var glowEnd = new Point(Center + cos * 74, Center + sin * 74);
|
||||
|
||||
HandLine.StartPoint = start;
|
||||
HandLine.EndPoint = end;
|
||||
HandGlowLine.StartPoint = start;
|
||||
HandGlowLine.EndPoint = glowEnd;
|
||||
}
|
||||
|
||||
private void UpdatePlayButtonVisual()
|
||||
{
|
||||
PlayIconPath.Data = Geometry.Parse(_isRunning
|
||||
? "M 0,0 L 4,0 L 4,14 L 0,14 Z M 8,0 L 12,0 L 12,14 L 8,14 Z"
|
||||
: "M 0,0 L 0,14 L 11,7 Z");
|
||||
}
|
||||
|
||||
private void ApplyModeVisualIfNeeded()
|
||||
{
|
||||
var isNightMode = ResolveIsNightMode();
|
||||
if (_isNightModeApplied.HasValue && _isNightModeApplied.Value == isNightMode)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_isNightModeApplied = isNightMode;
|
||||
ApplyModeVisual(isNightMode);
|
||||
}
|
||||
|
||||
private void ApplyModeVisual(bool isNightMode)
|
||||
{
|
||||
RootBorder.Background = CreateBrush(isNightMode ? "#313540" : "#E8EAEE");
|
||||
TimerPanelBorder.Background = isNightMode
|
||||
? CreateLinearGradientBrush("#2F3441", "#202632")
|
||||
: CreateLinearGradientBrush("#FBFCFE", "#F3F5F9");
|
||||
TimerPanelBorder.BorderBrush = CreateBrush(isNightMode ? "#3B4353" : "#E2E7F0");
|
||||
|
||||
CenterDivider.Background = CreateBrush(isNightMode ? "#434B5C" : "#D5DAE3");
|
||||
|
||||
TopNumberTextBlock.Foreground = CreateBrush(isNightMode ? "#7A8397" : "#AEB4C1");
|
||||
MainNumberTextBlock.Foreground = CreateBrush(isNightMode ? "#F3F6FE" : "#0F141C");
|
||||
NextNumberTextBlock.Foreground = CreateBrush(isNightMode ? "#8089A0" : "#B2B8C4");
|
||||
NextNextNumberTextBlock.Foreground = CreateBrush(isNightMode ? "#6A7388" : "#C8CDD7");
|
||||
|
||||
var markBrush = CreateBrush(isNightMode ? "#5A657D" : "#D0D6E1");
|
||||
ScaleMark1.Background = markBrush;
|
||||
ScaleMark2.Background = markBrush;
|
||||
ScaleMark3.Background = markBrush;
|
||||
ScaleMark4.Background = markBrush;
|
||||
|
||||
PlayButtonBorder.BorderBrush = CreateBrush(isNightMode ? "#4A5367" : "#D3D9E4");
|
||||
PlayIconPath.Fill = CreateBrush(isNightMode ? "#8E98AF" : "#98A2B8");
|
||||
|
||||
CenterDotRing.Fill = CreateBrush(isNightMode ? "#EAF0FF" : "#FDFEFF");
|
||||
CenterDotRing.Stroke = CreateBrush(isNightMode ? "#A9B8D5" : "#E3E8F0");
|
||||
CenterDotCore.Fill = CreateBrush("#FF4D63");
|
||||
HandLine.Stroke = CreateBrush("#FF4D63");
|
||||
HandGlowLine.Stroke = CreateBrush(isNightMode ? "#FF6A6E" : "#FF7A78");
|
||||
HandGlowLine.Opacity = isNightMode ? 0.28 : 0.20;
|
||||
}
|
||||
|
||||
public void ApplyCellSize(double cellSize)
|
||||
{
|
||||
_currentCellSize = Math.Max(1, cellSize);
|
||||
var scale = ResolveScale();
|
||||
|
||||
RootBorder.CornerRadius = new CornerRadius(Math.Clamp(34 * scale, 12, 48));
|
||||
RootBorder.Padding = new Thickness(Math.Clamp(14 * scale, 7, 22));
|
||||
TimerPanelBorder.CornerRadius = new CornerRadius(Math.Clamp(32 * scale, 12, 42));
|
||||
|
||||
PlayButtonBorder.Width = Math.Clamp(42 * scale, 28, 58);
|
||||
PlayButtonBorder.Height = Math.Clamp(42 * scale, 28, 58);
|
||||
PlayButtonBorder.CornerRadius = new CornerRadius(PlayButtonBorder.Width / 2d);
|
||||
|
||||
ApplyModeVisualIfNeeded();
|
||||
}
|
||||
|
||||
private double ResolveScale()
|
||||
{
|
||||
var cellScale = Math.Clamp(_currentCellSize / 44d, 0.60, 1.90);
|
||||
var heightScale = Bounds.Height > 1 ? Math.Clamp(Bounds.Height / 300d, 0.58, 2.0) : 1;
|
||||
var widthScale = Bounds.Width > 1 ? Math.Clamp(Bounds.Width / 300d, 0.58, 2.0) : 1;
|
||||
return Math.Clamp(Math.Min(cellScale, Math.Min(heightScale, widthScale) * 1.05), 0.58, 1.95);
|
||||
}
|
||||
|
||||
private bool ResolveIsNightMode()
|
||||
{
|
||||
if (ActualThemeVariant == ThemeVariant.Dark)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ActualThemeVariant == ThemeVariant.Light)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.TryFindResource("AdaptiveSurfaceBaseBrush", out var value) &&
|
||||
value is ISolidColorBrush solidBrush)
|
||||
{
|
||||
return CalculateRelativeLuminance(solidBrush.Color) < 0.45;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static IBrush CreateBrush(string colorHex)
|
||||
{
|
||||
return new SolidColorBrush(Color.Parse(colorHex));
|
||||
}
|
||||
|
||||
private static IBrush CreateLinearGradientBrush(string fromColorHex, string toColorHex)
|
||||
{
|
||||
return new LinearGradientBrush
|
||||
{
|
||||
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
|
||||
EndPoint = new RelativePoint(1, 1, RelativeUnit.Relative),
|
||||
GradientStops = new GradientStops
|
||||
{
|
||||
new GradientStop(Color.Parse(fromColorHex), 0),
|
||||
new GradientStop(Color.Parse(toColorHex), 1)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user