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 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 ChatMessages { get; } = new(); /// The Add → YouTube Chat menu item: enabled only while no chat layer exists /// anywhere — one chat overlay per layout. public bool CanAddYouTubeChat => StagedScene != null && !Scenes.Any(s => s.Elements.OfType().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().Where(s => s.Type == SourceType.ChatBox)) UpdateChatBoxPreview(chatBox); }); } private void ResetChatFadeTimer() { _chatFadeTimer?.Stop(); var chatBoxes = Scenes.SelectMany(s => s.Elements).OfType() .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() .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); } /// /// Renders the ChatBox preview: real messages when connected, mock when not. /// private void UpdateChatBoxPreview(Source chatBox, IReadOnlyList? messages = null) { try { messages ??= ChatMessages.Count > 0 ? (IReadOnlyList)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() .Where(s => s.Type == SourceType.ChatBox && s.ChatPreviewEnabled)) _ = RunMockChatPreviewAsync(chatBox, ct); } private void StopMockChatPreview() { _mockChatCts?.Cancel(); _mockChatCts?.Dispose(); _mockChatCts = null; } /// /// 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. /// private void UpdateActiveChatBoxPreviews() { foreach (var scene in Scenes) foreach (var chatBox in scene.Elements.OfType().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); } } }