Files
LlamaCasty/ViewModels/MainViewModel.Chat.cs
T
gramps 3b77a132be refactor: split Chat out of MainViewModel — Commit 10/12
MainViewModel.Chat.cs (194 lines) takes the YouTube chat overlay: ChatMessages
(+500 cap, clear-on-fade), OnChatMessageReceived (dispatcher marshal,
mock→real preview switch), chat fade timer (ResetChatFadeTimer), the ChatBox
render paths (RenderChatBox for the live compositor, UpdateChatBoxPreview for
the editor preview), the mock preview loop (MockChatMessages/StartMockChatPreview/
StopMockChatPreview/RunMockChatPreviewAsync/_mockChatCts), UpdateActiveChatBoxPreviews,
CanAddYouTubeChat (one-per-layout gate) and ShowChatInactiveMessage. Core drops
1119 -> 940 lines. Ctor wiring (MessageReceived hookup, ChatBoxRenderer ctor,
LoadLayout preview start, dispose) stays in core; ResolveOutputFrame (core)
still calls RenderChatBox cross-partial.

Zero behavior change; build 0 warnings; 246 pass, only the 2 known failures.
Docs: index.md Chat row added + core row trimmed in the same commit.
2026-08-30 08:48:35 -07:00

195 lines
7.7 KiB
C#

using System.Collections.ObjectModel;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Threading;
using ytLive.Helpers;
using ytLive.Models;
using ytLive.Services;
namespace ytLive.ViewModels;
public partial class MainViewModel : ViewModelBase
{
private readonly ChatBoxRenderer _chatBoxRenderer;
// Mock chat messages for the ChatBox preview — branded placeholders so the
// creator sees where the overlay lands before going live.
private static readonly IReadOnlyList<ChatMessage> MockChatMessages = new[]
{
new ChatMessage { AuthorName = "LlamaCasty", Message = "welcome to the stream!", Timestamp = DateTime.Now.AddMinutes(-4), IsMember = true },
new ChatMessage { AuthorName = "LlamaCastyFan", Message = "$10 — LlamaCasty is the best!", Timestamp = DateTime.Now.AddMinutes(-3), IsSuperChat = true },
new ChatMessage { AuthorName = "GamerDude", Message = "what streaming software is this?", Timestamp = DateTime.Now.AddMinutes(-2) },
new ChatMessage { AuthorName = "LlamaCasty", Message = "thanks for watching! llamacasty.com", Timestamp = DateTime.Now.AddMinutes(-1), IsMember = true },
new ChatMessage { AuthorName = "LlamaDog", Message = "Subscribe so they can buy the subscription at llamacasty.com!", Timestamp = DateTime.Now, IsMember = true },
};
// Chat fade timer: clears chat overlay after N seconds of inactivity.
private System.Windows.Threading.DispatcherTimer? _chatFadeTimer;
public ObservableCollection<ChatMessage> ChatMessages { get; } = new();
/// <summary>The Add → YouTube Chat menu item: enabled only while no chat layer exists
/// anywhere — one chat overlay per layout.</summary>
public bool CanAddYouTubeChat
=> StagedScene != null
&& !Scenes.Any(s => s.Elements.OfType<Source>().Any(e => e.Type == SourceType.ChatBox));
public bool ShowChatInactiveMessage => !IsLive;
private void OnChatMessageReceived(ChatMessage message)
{
System.Windows.Application.Current.Dispatcher.Invoke(() =>
{
ChatMessages.Add(message);
if (ChatMessages.Count > 500)
ChatMessages.RemoveAt(0);
// Reset chat fade timer on every message
ResetChatFadeTimer();
StopMockChatPreview();
// Switch ChatBox previews from mock to real messages
if (StagedScene != null)
foreach (var chatBox in StagedScene.Elements.OfType<Source>().Where(s => s.Type == SourceType.ChatBox))
UpdateChatBoxPreview(chatBox);
});
}
private void ResetChatFadeTimer()
{
_chatFadeTimer?.Stop();
var chatBoxes = Scenes.SelectMany(s => s.Elements).OfType<Source>()
.Where(s => s.Type == SourceType.ChatBox && s.ChatFadeSeconds > 0).ToList();
if (chatBoxes.Count == 0) return;
var delay = chatBoxes.Min(s => s.ChatFadeSeconds);
_chatFadeTimer = new System.Windows.Threading.DispatcherTimer
{ Interval = TimeSpan.FromSeconds(delay) };
_chatFadeTimer.Tick += (_, _) =>
{
_chatFadeTimer?.Stop();
_chatFadeTimer = null;
ChatMessages.Clear();
foreach (var chatBox in Scenes.SelectMany(s => s.Elements).OfType<Source>()
.Where(s => s.Type == SourceType.ChatBox))
UpdateChatBoxPreview(chatBox);
StartMockChatPreview();
};
_chatFadeTimer.Start();
}
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.ChatFontFamily,
chatBox.ChatFontColor,
chatBox.ChatBackgroundColor,
chatBox.ChatMaxMessages,
chatBox.ChatShowTimestamps,
chatBox.ChatShowBadges,
chatBox.ChatMemberColor,
chatBox.ChatSuperChatColor);
}
/// <summary>
/// Renders the ChatBox preview: real messages when connected, mock when not.
/// </summary>
private void UpdateChatBoxPreview(Source chatBox, IReadOnlyList<ChatMessage>? messages = null)
{
try
{
messages ??= ChatMessages.Count > 0 ? (IReadOnlyList<ChatMessage>)ChatMessages : MockChatMessages;
var width = (int)Math.Max(32, chatBox.Width);
var height = (int)Math.Max(32, chatBox.Height);
var frame = _chatBoxRenderer.Render(
messages,
width,
height,
chatBox.ChatFontSize,
chatBox.ChatFontFamily,
chatBox.ChatFontColor,
chatBox.ChatBackgroundColor,
chatBox.ChatMaxMessages,
chatBox.ChatShowTimestamps,
chatBox.ChatShowBadges,
chatBox.ChatMemberColor,
chatBox.ChatSuperChatColor);
var bitmap = new WriteableBitmap(width, height, 96, 96, System.Windows.Media.PixelFormats.Bgra32, null);
bitmap.WritePixels(new System.Windows.Int32Rect(0, 0, width, height), frame.BgraPixels, frame.Stride, 0);
chatBox.VideoImageSource = bitmap;
}
catch (Exception ex) { AppLog.Write($"UpdateChatBoxPreview failed: {ex.Message}"); }
}
private CancellationTokenSource? _mockChatCts;
private async Task RunMockChatPreviewAsync(Source chatBox, CancellationToken ct)
{
var n = 0;
while (!ct.IsCancellationRequested)
{
UpdateChatBoxPreview(chatBox, MockChatMessages.Take(n + 1).ToList());
await Task.Delay(1000, ct);
n++;
if (n == MockChatMessages.Count)
{
await Task.Delay(3000, ct);
n = 0;
}
}
}
private void StartMockChatPreview()
{
StopMockChatPreview();
_mockChatCts = new CancellationTokenSource();
var ct = _mockChatCts.Token;
foreach (var chatBox in Scenes.SelectMany(s => s.Elements).OfType<Source>()
.Where(s => s.Type == SourceType.ChatBox && s.ChatPreviewEnabled))
_ = RunMockChatPreviewAsync(chatBox, ct);
}
private void StopMockChatPreview()
{
_mockChatCts?.Cancel();
_mockChatCts?.Dispose();
_mockChatCts = null;
}
/// <summary>
/// Render previews + wire property-change subscriptions for all ChatBox
/// sources across every scene. Called after layout load so existing ChatBox
/// sources show mock content and respond to config changes.
/// </summary>
private void UpdateActiveChatBoxPreviews()
{
foreach (var scene in Scenes)
foreach (var chatBox in scene.Elements.OfType<Source>().Where(s => s.Type == SourceType.ChatBox))
{
chatBox.PropertyChanged += (_, e) =>
{
if (e.PropertyName is nameof(Source.ChatFontSize) or nameof(Source.ChatFontColor)
or nameof(Source.ChatBackgroundColor) or nameof(Source.ChatBackgroundOpacity)
or nameof(Source.ChatMaxMessages) or nameof(Source.ChatShowTimestamps)
or nameof(Source.ChatShowBadges) or nameof(Source.ChatMemberColor)
or nameof(Source.ChatSuperChatColor) or nameof(Source.ChatFontFamily)
or nameof(Source.ChatFadeSeconds) or nameof(Source.Width) or nameof(Source.Height))
UpdateChatBoxPreview(chatBox);
};
UpdateChatBoxPreview(chatBox);
}
}
}