feat(chat-overlay): add ChatBox config UI + schema persistence

- Elements panel config UI for ChatBox source (font size slider,
  font/member/super-chat/background color inputs, max messages slider,
  timestamp/badge toggles)
- Schema migration: 8 new ChatBox columns on Source table (ChatFontSize,
  ChatFontColor, ChatBackgroundColor, ChatMaxMessages, ChatShowTimestamps,
  ChatShowBadges, ChatMemberColor, ChatSuperChatColor)
- Load/save: Source SELECT and INSERT now include all ChatBox fields
- IsElementsPanelEmpty excludes ChatBox so hint doesn't cover config
- ChatBoxRenderer.cs created: WPF FormattedText + RenderTargetBitmap
  renderer for on-stream chat overlay
- Slider bindings set to TwoWay for live propagation
This commit is contained in:
2026-08-19 12:42:32 -07:00
parent 93987506a7
commit 65641d84e5
4 changed files with 346 additions and 4 deletions
+27 -1
View File
@@ -126,6 +126,7 @@ public class MainViewModel : ViewModelBase
private readonly IFullScreenDetector _fullScreenDetector;
private readonly ScreenCaptureManager _screenCaptureManager;
private readonly ScreenCaptureSourceFactory _screenCaptureFactory;
private readonly ChatBoxRenderer _chatBoxRenderer;
private int? _lastForegroundFullScreenMonitor;
private CancellationTokenSource? _deactivateCts;
private ImageSource? _backdropImage;
@@ -270,7 +271,7 @@ public class MainViewModel : ViewModelBase
/// <summary>True when the Elements panel should show the "No configurable elements" hint.</summary>
public bool IsElementsPanelEmpty => SelectedElement is { } el
&& !el.IsDraggable && !el.IsWebcam
&& el is not Source { Type: SourceType.Countdown or SourceType.WebSource };
&& el is not Source { Type: SourceType.Countdown or SourceType.WebSource or SourceType.ChatBox };
/// <summary>
/// The reason the webcam feed is down (device in use, offline, locked, no frames),
@@ -1267,6 +1268,8 @@ public class MainViewModel : ViewModelBase
_screenCaptureManager.CaptureFailed += (key, message) =>
AppLog.Write($"ScreenCaptureManager: capture '{key}' failed: {message}");
_chatBoxRenderer = new ChatBoxRenderer();
_rtmpUrlProvider = () => _reusableStreamUrl; // TASK 5: the reusable stream's ingest URL
_framePump = new FramePump(
sceneProvider: () => ActiveScene,
@@ -1761,6 +1764,7 @@ public class MainViewModel : ViewModelBase
SourceType.TextOverlay => "Text",
SourceType.Countdown => "Countdown",
SourceType.WebSource => "Web",
SourceType.ChatBox => "Chat",
_ => "Source",
};
@@ -2414,11 +2418,33 @@ public class MainViewModel : ViewModelBase
{
WebcamSceneConfig webcam => _cameraManager.GetLatestFrame(webcam.WebcamId),
Source { IsLiveCapture: true, CaptureKey: not null } live => _screenCaptureManager.GetLatestFrame(live.CaptureKey),
Source { Type: SourceType.ChatBox } chatBox => RenderChatBox(chatBox),
Source { AssetId: not null } image => StaticPixelCache.Get(image.AssetId),
_ => null,
};
}
private VideoFrame? RenderChatBox(Source chatBox)
{
if (ChatMessages.Count == 0) return null;
var width = (int)Math.Max(1, chatBox.Width);
var height = (int)Math.Max(1, chatBox.Height);
return _chatBoxRenderer.Render(
ChatMessages,
width,
height,
chatBox.ChatFontSize,
chatBox.ChatFontColor,
chatBox.ChatBackgroundColor,
chatBox.ChatMaxMessages,
chatBox.ChatShowTimestamps,
chatBox.ChatShowBadges,
chatBox.ChatMemberColor,
chatBox.ChatSuperChatColor);
}
// The tier's crop rect over the 1920x1080 master, integer-aligned (the VM's
// OutputRect* are doubles — the vertical 607.5 half-pixel crop rounds to 608).
private CompositorOptions BuildCompositorOptions()