Toasts: non-blocking notifications replace MessageBoxes + log-only failures (TASK 24)
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
namespace ytLive.Services;
|
||||
|
||||
/// <summary>How urgent a user-facing notification is. Drives the toast's tint and lifetime.</summary>
|
||||
public enum AppNotificationSeverity
|
||||
{
|
||||
Info,
|
||||
Success,
|
||||
Warning,
|
||||
Error,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Non-blocking user-facing messages (toasts). VMs and services raise events through
|
||||
/// this seam so failures surface without MessageBox calls blocking the UI thread.
|
||||
/// </summary>
|
||||
public interface INotificationService
|
||||
{
|
||||
void Show(string title, string message, AppNotificationSeverity severity);
|
||||
}
|
||||
|
||||
public static class NotificationServiceExtensions
|
||||
{
|
||||
public static void Info(this INotificationService service, string title, string message = "")
|
||||
=> service.Show(title, message, AppNotificationSeverity.Info);
|
||||
|
||||
public static void Success(this INotificationService service, string title, string message = "")
|
||||
=> service.Show(title, message, AppNotificationSeverity.Success);
|
||||
|
||||
public static void Warning(this INotificationService service, string title, string message = "")
|
||||
=> service.Show(title, message, AppNotificationSeverity.Warning);
|
||||
|
||||
public static void Error(this INotificationService service, string title, string message = "")
|
||||
=> service.Show(title, message, AppNotificationSeverity.Error);
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
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] = ("#3a3b52", 4),
|
||||
[AppNotificationSeverity.Success] = ("#1d5c38", 4),
|
||||
[AppNotificationSeverity.Warning] = ("#b8860b", 8),
|
||||
[AppNotificationSeverity.Error] = ("#8f1f1f", 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("#FFFFFF"));
|
||||
|
||||
if (severity == AppNotificationSeverity.Error)
|
||||
builder.NeverExpires();
|
||||
else
|
||||
builder.ExpiresInSeconds(expirationSeconds);
|
||||
|
||||
return builder.Build();
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,8 @@ External-facing logic: YouTube API, persistence. See
|
||||
| `Encoder/FfmpegEncoderPicker.cs` | Pure H.264 encoder picker from `-encoders` output: NVENC → QSV → AMF → OpenH264; **never returns libx264** (GPL) |
|
||||
| `Encoder/FramePump.cs` | **TASK 4 ship step 5**: the live frame producer — while live it snapshots the active scene each tick, resolves every element to its latest frame (`Func<SceneElement, VideoFrame?>` resolver), composites it into the tier's output frame, and paces frames into the encoder at the tier's FPS. All collaborators constructor-injected seams; free of WPF and the capture managers. `StartAsync` never throws (failures log + `Failed`); no RTMP URL = encoder skipped; `StopAsync` stops the encoder before awaiting the loop (backpressure deadlock); `ProcessFailed` self-stops. Since the bar bug-fix branch it takes an optional `socialBar: Func<(VideoFrame? Frame, SocialBarPosition Position)>?` seam — a pre-rendered bar strip composited last (above the flash) at the top edge or `SourceRectHeight − bar height`. See `ai.md` "Live frame pipeline" |
|
||||
| `Encoder/FfmpegLocator.cs` | The default locator: PATH probe wins, then the cache, then pull+extract (`ffmpeg.exe` **+ the `libav*.dll` family** via a staging dir, failures via `AppLog`). Pinned to BtbN `autobuild-2026-08-09-13-03` **lgpl-shared** (dynamic linking = LGPL compliance without static-relink §6 material; no GPL libx264; NVENC/QSV/AMF + libopenh264 + native AAC — see `ai.md` → Licensing). Search dirs / tools dir / downloader constructor-injected for hermetic tests. Wired into the encoder since ship step 5 |
|
||||
| `INotificationService.cs` | **Toast seam (TASK 24)**: `Show(title, message, AppNotificationSeverity)` + `AppNotificationSeverity { Info, Success, Warning, Error }` + `Info/Success/Warning/Error` extension methods — non-blocking user-facing messages; VMs/services raise through this so failures surface without MessageBox blocking the UI thread |
|
||||
| `NotificationService.cs` | The default seam impl (Notification.Wpf 11.0.0, MIT): wraps a library `NotificationManager`; routes to the in-window `NotificationArea` named `ToastArea` (MainWindow bottom-right above the footer); lifetimes via the pure `BuildRequest` mapping + `Cards` tint table — Info/Success ~4s, Warning ~8s auto-dismiss, Error sticky (`NeverExpires()` = `TimeSpan.MaxValue`, never null); dark card tints matching the health-banner language; all Show calls marshal to the captured Dispatcher. See [`../ai.md`](../ai.md) "Toast notifications" |
|
||||
| `Audio/IAudioSource.cs` | **TASK 4 ship step 4 seam**: live capture source — `Start`/`Stop`/`Started`/`SampleReady(AudioSample)`/`Failed(Exception)`, `IDisposable`. Capture now runs for the app's lifetime (started at startup) so the footer meters preview live. The app consumes this seam; tests inject hermetic fakes |
|
||||
| `Audio/AudioSample.cs` | One captured chunk: interleaved PCM float (-1..1) + sample rate + channels |
|
||||
| `Audio/WasapiLoopbackAudioSource.cs` | Desktop/game capture: NAudio `WasapiLoopbackCapture` on the default render device — automatic at unity; the game bar's meter consumes it |
|
||||
|
||||
Reference in New Issue
Block a user