Files
LlamaCasty/Services/NotificationService.cs
T
gramps ec01a7deb8 design: the last VM-side colours come from tokens (ThemeBrush)
The state colours the view-models hand to bindings — top bar, REC/ON-AIR dots, health banner,
meter bands, toast tints — were hex literals in C#. They now resolve from Themes/Tokens.xaml
through `Helpers/ThemeBrush`, so the values live in one place. Without an Application (unit tests)
the helper returns a neutral grey: visibly wrong rather than silently plausible.

Four tokens the code needed and the document lacked, added to DESIGN.md (so the doc-is-the-contract
guard demanded their brushes): info-container #3a3b52, success-container #1d5c38,
warning-container #b8860b, test-container #9c6f1c (the test-stream gold bar).

Also fixed by this: the top bar still painted the RETIRED brand red #e94560 when live — the
standard's #ff5451 now, and `OnAirSignTests` (which pinned the old value) says so. BarOn/BarOff
became properties rather than static fields, because a static field would resolve tokens before
the Application exists.

Verified: DesignTokensTests, GameMeterHonestyTests, NotificationAreaIntegrationTests,
OnAirSignTests, SessionTeardownTests — 38 tests green.
2026-10-30 09:58:52 -07:00

67 lines
2.5 KiB
C#

using System.Windows;
using System.Windows.Threading;
using Notification.Core;
using Notification.Wpf;
using Notification.Wpf.Constants;
using ytLive.Helpers;
namespace ytLive.Services;
/// <summary>
/// Toast notifications rendered inside the main window's <c>ToastArea</c>
/// (bottom-right, above the footer). Info/success auto-dismiss after ~4s,
/// warnings ~8s, errors stay until dismissed. Calls from any thread are
/// marshalled to the captured dispatcher.
/// </summary>
public sealed class NotificationService : INotificationService
{
public const string AreaName = "ToastArea";
internal static readonly IReadOnlyDictionary<AppNotificationSeverity, (string BackgroundHex, double ExpirationSeconds)> Cards
= new Dictionary<AppNotificationSeverity, (string, double)>
{
[AppNotificationSeverity.Info] = (ThemeBrush.Hex("Brush.InfoContainer"), 4),
[AppNotificationSeverity.Success] = (ThemeBrush.Hex("Brush.SuccessContainer"), 4),
[AppNotificationSeverity.Warning] = (ThemeBrush.Hex("Brush.WarningContainer"), 8),
[AppNotificationSeverity.Error] = (ThemeBrush.Hex("Brush.ErrorContainer"), 0),
};
private readonly NotificationManager _manager = new();
private readonly Dispatcher _dispatcher;
public NotificationService()
: this(Application.Current?.Dispatcher)
{
}
internal NotificationService(Dispatcher? dispatcher)
{
_dispatcher = dispatcher ?? Dispatcher.CurrentDispatcher;
NotificationConstants.NotificationCornerRadius = new CornerRadius(8);
NotificationConstants.KeepNotificationVisibleOnMouseOver = true;
}
public void Show(string title, string message, AppNotificationSeverity severity)
{
var request = BuildRequest(title, message, severity);
_dispatcher.Invoke(() => _manager.Show(request));
}
internal static NotificationRequest BuildRequest(string title, string message, AppNotificationSeverity severity)
{
var (backgroundHex, expirationSeconds) = Cards[severity];
var builder = NotificationBuilder
.Create(title, message)
.InArea(AreaName)
.WithBackground(NotificationColor.FromHex(backgroundHex))
.WithForeground(NotificationColor.FromHex(ThemeBrush.Hex("Brush.OnSurface")));
if (severity == AppNotificationSeverity.Error)
builder.NeverExpires();
else
builder.ExpiresInSeconds(expirationSeconds);
return builder.Build();
}
}