| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220 | using Avalonia;using Avalonia.Controls;using Avalonia.Media;using Avalonia.Threading;using System.Windows.Input;using InABox.Core;namespace InABox.Avalonia.Components;public class CircularCountdownTimer : Control{    private readonly DispatcherTimer _timer;    private DateTime? _startTime;        public static readonly StyledProperty<IImage?> ImageProperty =        AvaloniaProperty.Register<CircularCountdownTimer, IImage?>(nameof(Image));    public IImage? Image    {        get => GetValue(ImageProperty);        set => SetValue(ImageProperty, value);    }        public static readonly StyledProperty<bool> IsActiveProperty =        AvaloniaProperty.Register<CircularCountdownTimer, bool>(nameof(IsActive));    public bool IsActive    {        get => GetValue(IsActiveProperty);        set => SetValue(IsActiveProperty, value);    }        public static readonly StyledProperty<ICommand?> StartedProperty =        AvaloniaProperty.Register<CircularCountdownTimer, ICommand?>(nameof(Started));    public ICommand? Started    {        get => GetValue(StartedProperty);        set => SetValue(StartedProperty, value);    }        public static readonly StyledProperty<ICommand?> StoppedProperty =        AvaloniaProperty.Register<CircularCountdownTimer, ICommand?>(nameof(Stopped));    public ICommand? Stopped    {        get => GetValue(StoppedProperty);        set => SetValue(StoppedProperty, value);    }                public static readonly StyledProperty<IBrush> BackgroundProperty =        AvaloniaProperty.Register<CircularCountdownTimer, IBrush>(nameof(Background), Brushes.Transparent);    public IBrush Background    {        get => GetValue(BackgroundProperty);        set => SetValue(BackgroundProperty, value);    }        public static readonly StyledProperty<double> StrokeThicknessProperty =        AvaloniaProperty.Register<CircularCountdownTimer, double>(nameof(StrokeThickness), 10.0);    public double StrokeThickness    {        get => GetValue(StrokeThicknessProperty);        set => SetValue(StrokeThicknessProperty, value);    }        public static readonly StyledProperty<IBrush> ProgressBackgroundProperty =        AvaloniaProperty.Register<CircularCountdownTimer, IBrush>(nameof(ProgressBackground), Brushes.Gray);    public IBrush ProgressBackground    {        get => GetValue(ProgressBackgroundProperty);        set => SetValue(ProgressBackgroundProperty, value);    }        public static readonly StyledProperty<IBrush> ProgressForegroundProperty =        AvaloniaProperty.Register<CircularCountdownTimer, IBrush>(nameof(ProgressForeground), Brushes.WhiteSmoke);    public IBrush ProgressForeground    {        get => GetValue(ProgressForegroundProperty);        set => SetValue(ProgressForegroundProperty, value);    }        protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)    {        base.OnPropertyChanged(change);        if (change.Property == IsActiveProperty)        {            if (Equals(change.NewValue,true))                Start();            else                Stop();        }    }    public static readonly StyledProperty<double> DurationProperty =        AvaloniaProperty.Register<CircularCountdownTimer, double>(nameof(Duration), 15.0);    public double Duration    {        get => GetValue(DurationProperty);        set        {            SetValue(DurationProperty, value);            InvalidateVisual();        }    }        public CircularCountdownTimer()    {        _timer = new DispatcherTimer(DispatcherPriority.MaxValue)        {            Interval = TimeSpan.FromMilliseconds(200),        };        _timer.Tick += TimerTick;    }    private void Start()    {        _startTime = DateTime.Now;        _timer.Start();        Started?.Execute(DataContext);        InvalidateVisual();    }        private void Stop()    {        _timer.Stop();        _startTime = null;        Stopped?.Execute(DataContext);        InvalidateVisual();    }        private TimeSpan RemainingTime()    {        if (!_startTime.HasValue)            return TimeSpan.Zero;                var _nowTime = DateTime.Now;        var _endTime = _startTime.Value.AddSeconds(Duration);        return _endTime > _nowTime             ? _endTime - _nowTime             : TimeSpan.Zero;    }        private void TimerTick(object? sender, EventArgs e)    {        if (RemainingTime() <= TimeSpan.Zero)            IsActive = false;        else            InvalidateVisual();    }        public override void Render(DrawingContext context)    {        var boundsRect = new Rect(Bounds.Left+Margin.Left, Bounds.Top+Margin.Top, Bounds.Width-(Margin.Left+Margin.Right), Bounds.Height-(Margin.Top+Margin.Bottom));                double centerX = boundsRect.Width / 2;        double centerY = boundsRect.Height / 2;        double radius = Math.Min(centerX, centerY) - StrokeThickness;        var backgroundPen = new Pen(ProgressBackground, StrokeThickness);        var progressPen = new Pen(ProgressForeground, StrokeThickness)        {            LineCap = PenLineCap.Flat // Smooth arc edges        };        context.FillRectangle(Background, boundsRect);        // Draw background circle        context.DrawEllipse(null, backgroundPen, new Point(centerX, centerY), radius, radius);        if (Image != null)        {            var boxwidth = Math.Sqrt(2) * radius;            Rect rect = new Rect(centerX - (boxwidth/2), centerY - (boxwidth/2), boxwidth, boxwidth);            context.DrawImage(Image, rect);        }        double _remainingTime = RemainingTime().TotalSeconds;        if (_remainingTime.IsEffectivelyGreaterThan(0.0))        {            // Calculate the sweep angle            double sweepAngle = (_remainingTime / Duration) * 360;            double startAngle = -90;            double endAngle = startAngle + sweepAngle;            // Convert angles to radians            double startRad = Math.PI * startAngle / 180.0;            double endRad = Math.PI * endAngle / 180.0;            // Calculate points on the circle            var startPoint = new Point(                centerX + radius * Math.Cos(startRad),                centerY + radius * Math.Sin(startRad)            );            var endPoint = new Point(                centerX + radius * Math.Cos(endRad),                centerY + radius * Math.Sin(endRad)            );            // Create an arc geometry            var geometry = new StreamGeometry();            using (var ctx = geometry.Open())            {                ctx.BeginFigure(startPoint, false);                ctx.ArcTo(endPoint, new Size(radius, radius), 0, sweepAngle > 180, SweepDirection.Clockwise);            }            // Draw the progress arc            context.DrawGeometry(null, progressPen, geometry);        }    }}
 |