refactor: extract ChatOverlayLayer — real component decomposition, Commit G
The first true decomposition of the 4105-line god-object, not another partial shuffle. MainViewModel.Chat.cs 194 -> 44 lines of thin delegation; all chat behavior now lives in a self-contained Services/ChatOverlayLayer.cs (199): - Owns the message buffer (Messages), ChatBoxRenderer, fade + mock-preview timers, per-source preview renders, and the live-output RenderFrame path. - The VM keeps only the binding surface: ChatMessages delegates to _chatLayer.Messages (so XAML ItemsSource + LeftPanel CollectionChanged hold), CanAddYouTubeChat / ShowChatInactiveMessage stay computed on the VM (OnPropertyChanged raised from VM setters + XAML-bound), and thin forwards. - Scenes handed in as args (no Func seam), so the layer owns no scene graph. An AI reading ChatOverlayLayer.cs now sees the entire chat feature in one self-contained unit. Zero behavior change; build 0 warnings; 246 pass, only the 2 known failures. Docs: ViewModels/index.md tracker updated in same commit.
This commit is contained in:
@@ -0,0 +1,199 @@
|
|||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using System.Windows.Media.Imaging;
|
||||||
|
using System.Windows.Threading;
|
||||||
|
using ytLive.Helpers;
|
||||||
|
using ytLive.Models;
|
||||||
|
|
||||||
|
namespace ytLive.Services;
|
||||||
|
|
||||||
|
/// <summary>Owns the YouTube-chat overlay behavior: the message buffer, the
|
||||||
|
/// renderer, and the fade/mock-preview timers. The MainViewModel keeps only the
|
||||||
|
/// thin binding surface (the shared <see cref="Messages"/> collection + two
|
||||||
|
/// computed gate props) and hands the scene list in at each call — this class
|
||||||
|
/// owns all chat logic so an AI can read one self-contained unit.</summary>
|
||||||
|
public sealed class ChatOverlayLayer
|
||||||
|
{
|
||||||
|
private readonly ChatBoxRenderer _renderer;
|
||||||
|
|
||||||
|
// 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 DispatcherTimer? _fadeTimer;
|
||||||
|
private CancellationTokenSource? _mockCts;
|
||||||
|
|
||||||
|
/// <summary>The live chat message buffer. Shared with the ViewModel (which
|
||||||
|
/// binds it to the panel) via <c>ChatMessages => _chatLayer.Messages</c>.</summary>
|
||||||
|
public ObservableCollection<ChatMessage> Messages { get; } = new();
|
||||||
|
|
||||||
|
public ChatOverlayLayer(ChatBoxRenderer renderer) => _renderer = renderer;
|
||||||
|
|
||||||
|
/// <summary>Ingest a message from the poller. UI-thread marshalled: appended
|
||||||
|
/// to the buffer (capped at 500), mock preview stopped, swipe to real
|
||||||
|
/// messages, and the fade timer reset. Scenes are handed in so the layer can
|
||||||
|
/// refresh every ChatBox preview without owning the scene graph.</summary>
|
||||||
|
public void OnMessageReceived(ChatMessage message, IEnumerable<Scene> scenes)
|
||||||
|
{
|
||||||
|
System.Windows.Application.Current.Dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
Messages.Add(message);
|
||||||
|
if (Messages.Count > 500)
|
||||||
|
Messages.RemoveAt(0);
|
||||||
|
|
||||||
|
// Reset chat fade timer on every message
|
||||||
|
ResetChatFadeTimer(scenes);
|
||||||
|
|
||||||
|
StopMockPreview();
|
||||||
|
|
||||||
|
// Switch ChatBox previews from mock to real messages
|
||||||
|
foreach (var chatBox in scenes.SelectMany(s => s.Elements).OfType<Source>()
|
||||||
|
.Where(s => s.Type == SourceType.ChatBox))
|
||||||
|
UpdatePreview(chatBox);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ResetChatFadeTimer(IEnumerable<Scene> scenes)
|
||||||
|
{
|
||||||
|
_fadeTimer?.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);
|
||||||
|
_fadeTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(delay) };
|
||||||
|
_fadeTimer.Tick += (_, _) =>
|
||||||
|
{
|
||||||
|
_fadeTimer?.Stop();
|
||||||
|
_fadeTimer = null;
|
||||||
|
Messages.Clear();
|
||||||
|
foreach (var chatBox in scenes.SelectMany(s => s.Elements).OfType<Source>()
|
||||||
|
.Where(s => s.Type == SourceType.ChatBox))
|
||||||
|
UpdatePreview(chatBox);
|
||||||
|
StartMockPreview(scenes);
|
||||||
|
};
|
||||||
|
_fadeTimer.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Renders the live-output frame for one ChatBox element (the
|
||||||
|
/// compositor path), or null when no messages are buffered yet. Mirrors the
|
||||||
|
/// preview render but without writing onto the source's VideoImageSource.</summary>
|
||||||
|
public VideoFrame? RenderFrame(Source chatBox)
|
||||||
|
{
|
||||||
|
if (Messages.Count == 0) return null;
|
||||||
|
|
||||||
|
var width = (int)Math.Max(1, chatBox.Width);
|
||||||
|
var height = (int)Math.Max(1, chatBox.Height);
|
||||||
|
|
||||||
|
return _renderer.Render(
|
||||||
|
Messages,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
chatBox.ChatFontSize,
|
||||||
|
chatBox.ChatFontFamily,
|
||||||
|
chatBox.ChatFontColor,
|
||||||
|
chatBox.ChatBackgroundColor,
|
||||||
|
chatBox.ChatMaxMessages,
|
||||||
|
chatBox.ChatShowTimestamps,
|
||||||
|
chatBox.ChatShowBadges,
|
||||||
|
chatBox.ChatMemberColor,
|
||||||
|
chatBox.ChatSuperChatColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Renders one ChatBox preview: real messages when connected, mock
|
||||||
|
/// when not. Writes the resulting bitmap onto the source's
|
||||||
|
/// <c>VideoImageSource</c>.</summary>
|
||||||
|
public void UpdatePreview(Source chatBox, IReadOnlyList<ChatMessage>? messages = null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
messages ??= Messages.Count > 0 ? (IReadOnlyList<ChatMessage>)Messages : MockChatMessages;
|
||||||
|
var width = (int)Math.Max(32, chatBox.Width);
|
||||||
|
var height = (int)Math.Max(32, chatBox.Height);
|
||||||
|
|
||||||
|
var frame = _renderer.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, PixelFormats.Bgra32, null);
|
||||||
|
bitmap.WritePixels(new Int32Rect(0, 0, width, height), frame.BgraPixels, frame.Stride, 0);
|
||||||
|
chatBox.VideoImageSource = bitmap;
|
||||||
|
}
|
||||||
|
catch (Exception ex) { AppLog.Write($"UpdateChatBoxPreview failed: {ex.Message}"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RunMockPreviewAsync(Source chatBox, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var n = 0;
|
||||||
|
while (!ct.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
UpdatePreview(chatBox, MockChatMessages.Take(n + 1).ToList());
|
||||||
|
await Task.Delay(1000, ct);
|
||||||
|
n++;
|
||||||
|
if (n == MockChatMessages.Count)
|
||||||
|
{
|
||||||
|
await Task.Delay(3000, ct);
|
||||||
|
n = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StartMockPreview(IEnumerable<Scene> scenes)
|
||||||
|
{
|
||||||
|
StopMockPreview();
|
||||||
|
_mockCts = new CancellationTokenSource();
|
||||||
|
var ct = _mockCts.Token;
|
||||||
|
foreach (var chatBox in scenes.SelectMany(s => s.Elements).OfType<Source>()
|
||||||
|
.Where(s => s.Type == SourceType.ChatBox && s.ChatPreviewEnabled))
|
||||||
|
_ = RunMockPreviewAsync(chatBox, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StopMockPreview()
|
||||||
|
{
|
||||||
|
_mockCts?.Cancel();
|
||||||
|
_mockCts?.Dispose();
|
||||||
|
_mockCts = 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>
|
||||||
|
public void UpdateConfiguredBoxes(IEnumerable<Scene> scenes)
|
||||||
|
{
|
||||||
|
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))
|
||||||
|
UpdatePreview(chatBox);
|
||||||
|
};
|
||||||
|
UpdatePreview(chatBox);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Windows.Media;
|
using System.Linq;
|
||||||
using System.Windows.Media.Imaging;
|
|
||||||
using System.Windows.Threading;
|
|
||||||
using ytLive.Helpers;
|
using ytLive.Helpers;
|
||||||
using ytLive.Models;
|
using ytLive.Models;
|
||||||
using ytLive.Services;
|
using ytLive.Services;
|
||||||
@@ -11,22 +9,12 @@ namespace ytLive.ViewModels;
|
|||||||
public partial class MainViewModel : ViewModelBase
|
public partial class MainViewModel : ViewModelBase
|
||||||
{
|
{
|
||||||
|
|
||||||
private readonly ChatBoxRenderer _chatBoxRenderer;
|
private readonly ChatOverlayLayer _chatLayer;
|
||||||
|
|
||||||
// Mock chat messages for the ChatBox preview — branded placeholders so the
|
/// <summary>The live chat message buffer — the same collection the
|
||||||
// creator sees where the overlay lands before going live.
|
/// <see cref="ChatOverlayLayer"/> owns; here so XAML can bind it and the
|
||||||
private static readonly IReadOnlyList<ChatMessage> MockChatMessages = new[]
|
/// left panel can subscribe to its changes.</summary>
|
||||||
{
|
public ObservableCollection<ChatMessage> ChatMessages => _chatLayer.Messages;
|
||||||
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
|
/// <summary>The Add → YouTube Chat menu item: enabled only while no chat layer exists
|
||||||
/// anywhere — one chat overlay per layout.</summary>
|
/// anywhere — one chat overlay per layout.</summary>
|
||||||
@@ -37,158 +25,20 @@ public partial class MainViewModel : ViewModelBase
|
|||||||
public bool ShowChatInactiveMessage => !IsLive;
|
public bool ShowChatInactiveMessage => !IsLive;
|
||||||
|
|
||||||
private void OnChatMessageReceived(ChatMessage message)
|
private void OnChatMessageReceived(ChatMessage message)
|
||||||
{
|
=> _chatLayer.OnMessageReceived(message, Scenes);
|
||||||
System.Windows.Application.Current.Dispatcher.Invoke(() =>
|
|
||||||
{
|
|
||||||
ChatMessages.Add(message);
|
|
||||||
if (ChatMessages.Count > 500)
|
|
||||||
ChatMessages.RemoveAt(0);
|
|
||||||
|
|
||||||
// Reset chat fade timer on every message
|
private void UpdateChatBoxPreview(Source chatBox, IReadOnlyList<ChatMessage>? messages = null)
|
||||||
ResetChatFadeTimer();
|
=> _chatLayer.UpdatePreview(chatBox, messages);
|
||||||
|
|
||||||
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)
|
private VideoFrame? RenderChatBox(Source chatBox)
|
||||||
{
|
=> _chatLayer.RenderFrame(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()
|
private void StartMockChatPreview()
|
||||||
{
|
=> _chatLayer.StartMockPreview(Scenes);
|
||||||
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()
|
private void StopMockChatPreview()
|
||||||
{
|
=> _chatLayer.StopMockPreview();
|
||||||
_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()
|
private void UpdateActiveChatBoxPreviews()
|
||||||
{
|
=> _chatLayer.UpdateConfiguredBoxes(Scenes);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -274,7 +274,7 @@ public partial class MainViewModel : ViewModelBase
|
|||||||
_screenCaptureManager.CaptureFailed += (key, message) =>
|
_screenCaptureManager.CaptureFailed += (key, message) =>
|
||||||
AppLog.Write($"ScreenCaptureManager: capture '{key}' failed: {message}");
|
AppLog.Write($"ScreenCaptureManager: capture '{key}' failed: {message}");
|
||||||
|
|
||||||
_chatBoxRenderer = new ChatBoxRenderer();
|
_chatLayer = new ChatOverlayLayer(new ChatBoxRenderer());
|
||||||
|
|
||||||
_rtmpUrlProvider = () => _reusableStreamUrl; // TASK 5: the reusable stream's ingest URL
|
_rtmpUrlProvider = () => _reusableStreamUrl; // TASK 5: the reusable stream's ingest URL
|
||||||
_framePump = new FramePump(
|
_framePump = new FramePump(
|
||||||
|
|||||||
@@ -72,5 +72,6 @@ commit, tag `refactor-commit-N`, no per-commit push).
|
|||||||
- [x] **Commit D · `refactor-commit-D`** — **SocialsDialog** → `SocialsDialogViewModel.cs` 515 → **334** + new **`SocialSlotViewModel.cs`** (183 lines). Moved the `DialogEntry` record + the row-level `SocialSlotViewModel` (six-fixed-slot row VM: service/logo/lock/edit/validate state + computed `Show*` flags) into its own file; `SocialsDialogViewModel.cs` keeps the dialog VM (sign-in gate, validation orchestration, commit). `DialogEntry` stays `public` in the same namespace so both files see it. Pending: LayoutStore.
|
- [x] **Commit D · `refactor-commit-D`** — **SocialsDialog** → `SocialsDialogViewModel.cs` 515 → **334** + new **`SocialSlotViewModel.cs`** (183 lines). Moved the `DialogEntry` record + the row-level `SocialSlotViewModel` (six-fixed-slot row VM: service/logo/lock/edit/validate state + computed `Show*` flags) into its own file; `SocialsDialogViewModel.cs` keeps the dialog VM (sign-in gate, validation orchestration, commit). `DialogEntry` stays `public` in the same namespace so both files see it. Pending: LayoutStore.
|
||||||
- [x] **Commit E · `refactor-commit-E`** — **LayoutStore** → `LayoutStore.cs` 1402 split into 6 concern-based partials: **`LayoutStore.cs`** (47: shell — `Instance`, fields, ctor + `EnsureSchema`, `GetUserVersion`, `Dispose`), **`LayoutStore.Migrations.cs`** (495: all `Migrate*` + `EnsureSchema`), **`LayoutStore.Load.cs`** (221: `Load`), **`LayoutStore.Save.cs`** (339: `Save`), **`LayoutStore.Settings.cs`** (292: mic/record-folder/reusable-stream + license/transition/broadcast/hotkey/window `GetSetting`/`UpsertSetting`), **`LayoutStore.Assets.cs`** (41: `GetAssetBytes`/`UpsertAsset`). **PHASE 3 COMPLETE — no production `.cs` over 500.**
|
- [x] **Commit E · `refactor-commit-E`** — **LayoutStore** → `LayoutStore.cs` 1402 split into 6 concern-based partials: **`LayoutStore.cs`** (47: shell — `Instance`, fields, ctor + `EnsureSchema`, `GetUserVersion`, `Dispose`), **`LayoutStore.Migrations.cs`** (495: all `Migrate*` + `EnsureSchema`), **`LayoutStore.Load.cs`** (221: `Load`), **`LayoutStore.Save.cs`** (339: `Save`), **`LayoutStore.Settings.cs`** (292: mic/record-folder/reusable-stream + license/transition/broadcast/hotkey/window `GetSetting`/`UpsertSetting`), **`LayoutStore.Assets.cs`** (41: `GetAssetBytes`/`UpsertAsset`). **PHASE 3 COMPLETE — no production `.cs` over 500.**
|
||||||
- [x] **Commit F · `refactor-commit-F`** — **Recording (context/concern)** → new **`MainViewModel.Recording.cs`** (121 lines). The encoder (`Services/Encoder/FfmpegEncoder.cs`) and frame pump (`Services/Encoder/FramePump.cs`) were ALREADY extracted services — the planned "extract FFmpegEncoder/StreamHealthMonitor/FramePump" was a no-op premise. The honest functional seam was the local recording-output concern: `StartRecordFile`, `FinalizeRecordingAsync`, `UniquePath`, `ChooseRecordFolder`, `ResetRecordFolder`, `DefaultRecordFolder` + fields `_recordFolder`/`RecordFolderDisplay`/`_activeRecordPath`/`_recordStartTime`/`_recordLength` moved out of the live-stream orchestration. `MainViewModel.Streaming.Operations.cs` 475 → **377**; `MainViewModel.Streaming.cs` 328 → **323**. Same partial class, MVVM glue untouched.
|
- [x] **Commit F · `refactor-commit-F`** — **Recording (context/concern)** → new **`MainViewModel.Recording.cs`** (121 lines). The encoder (`Services/Encoder/FfmpegEncoder.cs`) and frame pump (`Services/Encoder/FramePump.cs`) were ALREADY extracted services — the planned "extract FFmpegEncoder/StreamHealthMonitor/FramePump" was a no-op premise. The honest functional seam was the local recording-output concern: `StartRecordFile`, `FinalizeRecordingAsync`, `UniquePath`, `ChooseRecordFolder`, `ResetRecordFolder`, `DefaultRecordFolder` + fields `_recordFolder`/`RecordFolderDisplay`/`_activeRecordPath`/`_recordStartTime`/`_recordLength` moved out of the live-stream orchestration. `MainViewModel.Streaming.Operations.cs` 475 → **377**; `MainViewModel.Streaming.cs` 328 → **323**. Same partial class, MVVM glue untouched.
|
||||||
|
- [x] **Commit G · `refactor-commit-G`** — **ChatOverlayLayer (true component)** → new **`Services/ChatOverlayLayer.cs`** (199 lines). The first REAL decomposition of the 4,105-line god-object: `MainViewModel.Chat.cs` 194 → **44** (thin delegation). The layer owns all chat behavior — `Messages` buffer, `ChatBoxRenderer`, fade timer, mock-preview timer, per-source preview renders, live-output `RenderFrame`. The VM keeps only the binding surface: `ChatMessages` (delegates to `_chatLayer.Messages`, so XAML bind + LeftPanel `CollectionChanged` hold), `CanAddYouTubeChat`/`ShowChatInactiveMessage` (computed + `OnPropertyChanged` from VM setters), and thin forwards. Scenes are handed in as args (no `Func` seam) so the layer stays scene-graph-free. Zero behavior change; 246 pass / 2 known.
|
||||||
The partial `.cs` files land next to `MainViewModel.cs` in this folder as the split
|
The partial `.cs` files land next to `MainViewModel.cs` in this folder as the split
|
||||||
proceeds; each partial carries its own `using`s and re-declares nothing from core.
|
proceeds; each partial carries its own `using`s and re-declares nothing from core.
|
||||||
Reference in New Issue
Block a user