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:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user