AvaloniaCountdownTimer.cs 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. using Avalonia;
  2. using Avalonia.Controls;
  3. using Avalonia.Media;
  4. using Avalonia.Threading;
  5. using System;
  6. using System.IO;
  7. using System.Net;
  8. using System.Windows.Input;
  9. using Avalonia.Data;
  10. using Avalonia.Svg.Skia;
  11. using InABox.Core;
  12. using SkiaSharp;
  13. using Svg.Skia;
  14. namespace InABox.Avalonia.Components;
  15. public class CircularCountdownTimer : Control
  16. {
  17. private DispatcherTimer _timer;
  18. private DateTime? _startTime = null;
  19. public static readonly StyledProperty<IImage?> ImageProperty =
  20. AvaloniaProperty.Register<CircularCountdownTimer, IImage?>(nameof(Image));
  21. public IImage? Image
  22. {
  23. get => GetValue(ImageProperty);
  24. set => SetValue(ImageProperty, value);
  25. }
  26. public static readonly StyledProperty<bool> IsActiveProperty =
  27. AvaloniaProperty.Register<CircularCountdownTimer, bool>(nameof(IsActive));
  28. public bool IsActive
  29. {
  30. get => GetValue(IsActiveProperty);
  31. set => SetValue(IsActiveProperty, value);
  32. }
  33. public static readonly StyledProperty<ICommand> StartedProperty =
  34. AvaloniaProperty.Register<CircularCountdownTimer, ICommand>(nameof(Started));
  35. public ICommand Started
  36. {
  37. get => GetValue(StartedProperty);
  38. set => SetValue(StartedProperty, value);
  39. }
  40. public static readonly StyledProperty<ICommand> StoppedProperty =
  41. AvaloniaProperty.Register<CircularCountdownTimer, ICommand>(nameof(Stopped));
  42. public ICommand Stopped
  43. {
  44. get => GetValue(StoppedProperty);
  45. set => SetValue(StoppedProperty, value);
  46. }
  47. public static readonly StyledProperty<IBrush> BackgroundProperty =
  48. AvaloniaProperty.Register<CircularCountdownTimer, IBrush>(nameof(Background), Brushes.Transparent);
  49. public IBrush Background
  50. {
  51. get => GetValue(BackgroundProperty);
  52. set => SetValue(BackgroundProperty, value);
  53. }
  54. public static readonly StyledProperty<double> StrokeThicknessProperty =
  55. AvaloniaProperty.Register<CircularCountdownTimer, double>(nameof(StrokeThickness), 10.0);
  56. public double StrokeThickness
  57. {
  58. get => GetValue(StrokeThicknessProperty);
  59. set => SetValue(StrokeThicknessProperty, value);
  60. }
  61. public static readonly StyledProperty<IBrush> ProgressBackgroundProperty =
  62. AvaloniaProperty.Register<CircularCountdownTimer, IBrush>(nameof(ProgressBackground), Brushes.Gray);
  63. public IBrush ProgressBackground
  64. {
  65. get => GetValue(ProgressBackgroundProperty);
  66. set => SetValue(ProgressBackgroundProperty, value);
  67. }
  68. public static readonly StyledProperty<IBrush> ProgressForegroundProperty =
  69. AvaloniaProperty.Register<CircularCountdownTimer, IBrush>(nameof(ProgressForeground), Brushes.WhiteSmoke);
  70. public IBrush ProgressForeground
  71. {
  72. get => GetValue(ProgressForegroundProperty);
  73. set => SetValue(ProgressForegroundProperty, value);
  74. }
  75. protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
  76. {
  77. base.OnPropertyChanged(change);
  78. if (change.Property == IsActiveProperty)
  79. {
  80. if (Equals(change.NewValue,true))
  81. Start();
  82. else
  83. Stop();
  84. }
  85. }
  86. public static readonly StyledProperty<double> DurationProperty =
  87. AvaloniaProperty.Register<CircularCountdownTimer, double>(nameof(Duration), 15.0);
  88. public double Duration
  89. {
  90. get => GetValue(DurationProperty);
  91. set
  92. {
  93. SetValue(DurationProperty, value);
  94. InvalidateVisual();
  95. }
  96. }
  97. public CircularCountdownTimer()
  98. {
  99. _timer = new DispatcherTimer
  100. {
  101. Interval = TimeSpan.FromMilliseconds(100) // Update every 100ms
  102. };
  103. _timer.Tick += TimerTick;
  104. }
  105. private void Start()
  106. {
  107. _startTime = DateTime.Now;
  108. _timer.Start();
  109. Started?.Execute(DataContext);
  110. InvalidateVisual();
  111. }
  112. private void Stop()
  113. {
  114. _timer.Stop();
  115. _startTime = null;
  116. Stopped?.Execute(DataContext);
  117. InvalidateVisual();
  118. }
  119. private TimeSpan RemainingTime()
  120. {
  121. if (!_startTime.HasValue)
  122. return TimeSpan.Zero;
  123. var _nowTime = DateTime.Now;
  124. var _endTime = _startTime.Value.AddSeconds(Duration);
  125. return _endTime > _nowTime
  126. ? _endTime - _nowTime
  127. : TimeSpan.Zero;
  128. }
  129. private void TimerTick(object? sender, EventArgs e)
  130. {
  131. if (RemainingTime() <= TimeSpan.Zero)
  132. IsActive = false;
  133. else
  134. InvalidateVisual();
  135. }
  136. public override void Render(DrawingContext context)
  137. {
  138. var boundsRect = new Rect(Bounds.Left+Margin.Left, Bounds.Top+Margin.Top, Bounds.Width-(Margin.Left+Margin.Right), Bounds.Height-(Margin.Top+Margin.Bottom));
  139. double centerX = boundsRect.Width / 2;
  140. double centerY = boundsRect.Height / 2;
  141. double radius = Math.Min(centerX, centerY) - StrokeThickness;
  142. var backgroundPen = new Pen(ProgressBackground, StrokeThickness);
  143. var progressPen = new Pen(ProgressForeground, StrokeThickness)
  144. {
  145. LineCap = PenLineCap.Flat // Smooth arc edges
  146. };
  147. context.FillRectangle(Background, boundsRect);
  148. // Draw background circle
  149. context.DrawEllipse(null, backgroundPen, new Point(centerX, centerY), radius, radius);
  150. if (Image != null)
  151. {
  152. var boxwidth = Math.Sqrt(2) * radius;
  153. Rect rect = new Rect(centerX - (boxwidth/2), centerY - (boxwidth/2), boxwidth, boxwidth);
  154. context.DrawImage(Image, rect);
  155. }
  156. double _remainingTime = RemainingTime().TotalSeconds;
  157. if (_remainingTime.IsEffectivelyGreaterThan(0.0))
  158. {
  159. // Calculate the sweep angle
  160. double sweepAngle = (_remainingTime / Duration) * 360;
  161. double startAngle = -90;
  162. double endAngle = startAngle + sweepAngle;
  163. // Convert angles to radians
  164. double startRad = Math.PI * startAngle / 180.0;
  165. double endRad = Math.PI * endAngle / 180.0;
  166. // Calculate points on the circle
  167. var startPoint = new Point(
  168. centerX + radius * Math.Cos(startRad),
  169. centerY + radius * Math.Sin(startRad)
  170. );
  171. var endPoint = new Point(
  172. centerX + radius * Math.Cos(endRad),
  173. centerY + radius * Math.Sin(endRad)
  174. );
  175. // Create an arc geometry
  176. var geometry = new StreamGeometry();
  177. using (var ctx = geometry.Open())
  178. {
  179. ctx.BeginFigure(startPoint, false);
  180. ctx.ArcTo(endPoint, new Size(radius, radius), 0, sweepAngle > 180, SweepDirection.Clockwise);
  181. }
  182. // Draw the progress arc
  183. context.DrawGeometry(null, progressPen, geometry);
  184. }
  185. }
  186. }