Files
LanMountainDesktop/LanMontainDesktop/Views/Components/TimerWidget.axaml.cs

274 lines
8.5 KiB
C#
Raw Normal View History

2026-03-02 22:46:10 +08:00
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;
2026-03-03 04:56:04 +08:00
public partial class TimerWidget : UserControl, IDesktopComponentWidget
2026-03-02 22:46:10 +08:00
{
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;
}
}