feat(ui): constrain decimal input and add stacked timed toasts with uniform actions
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
namespace Duempelkas.App.Services;
|
||||
|
||||
public enum ToastKind { Info, Success, Error }
|
||||
public sealed record ToastMessage(Guid Id, string Message, ToastKind Kind, string? Details,
|
||||
string? ActionLabel, Func<Task>? Action);
|
||||
|
||||
public sealed class ToastService : IDisposable
|
||||
{
|
||||
private readonly object sync = new();
|
||||
private readonly Dictionary<Guid, (ToastMessage Message, Timer? Timer, TimeSpan? Remaining, DateTimeOffset Started)> items = [];
|
||||
public event Action? Changed;
|
||||
public IReadOnlyList<ToastMessage> Messages { get { lock (sync) return items.Values.Select(x => x.Message).ToArray(); } }
|
||||
|
||||
public Guid Show(string message, ToastKind kind = ToastKind.Info, string? details = null,
|
||||
string? actionLabel = null, Func<Task>? action = null, TimeSpan? duration = null, bool persistent = false)
|
||||
{
|
||||
var id = Guid.NewGuid();
|
||||
var lifetime = persistent ? (TimeSpan?)null : duration ?? TimeSpan.FromSeconds(kind == ToastKind.Error ? 15 : 9);
|
||||
var toast = new ToastMessage(id, message, kind, details, actionLabel, action);
|
||||
lock (sync)
|
||||
{
|
||||
Timer? timer = lifetime.HasValue ? new Timer(_ => Dismiss(id), null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan) : null;
|
||||
items[id] = (toast, timer, lifetime, DateTimeOffset.UtcNow);
|
||||
if (lifetime.HasValue) timer!.Change(lifetime.Value, Timeout.InfiniteTimeSpan);
|
||||
}
|
||||
Changed?.Invoke();
|
||||
return id;
|
||||
}
|
||||
|
||||
public void Pause(Guid id)
|
||||
{
|
||||
lock (sync)
|
||||
{
|
||||
if (!items.TryGetValue(id, out var item) || item.Remaining == null || item.Started == default) return;
|
||||
item.Timer?.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
|
||||
var left = item.Remaining.Value - (DateTimeOffset.UtcNow - item.Started);
|
||||
items[id] = (item.Message, item.Timer, left > TimeSpan.Zero ? left : TimeSpan.FromMilliseconds(1), default);
|
||||
}
|
||||
}
|
||||
|
||||
public void Resume(Guid id)
|
||||
{
|
||||
lock (sync)
|
||||
{
|
||||
if (!items.TryGetValue(id, out var item) || item.Remaining == null || item.Started != default) return;
|
||||
items[id] = (item.Message, item.Timer, item.Remaining, DateTimeOffset.UtcNow);
|
||||
item.Timer?.Change(item.Remaining.Value, Timeout.InfiniteTimeSpan);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dismiss(Guid id)
|
||||
{
|
||||
lock (sync)
|
||||
{
|
||||
if (!items.Remove(id, out var item)) return;
|
||||
item.Timer?.Dispose();
|
||||
}
|
||||
Changed?.Invoke();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
lock (sync) { foreach (var item in items.Values) item.Timer?.Dispose(); items.Clear(); }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user