From 6944db8ab4a0802791169620502c70a9d4c1a794 Mon Sep 17 00:00:00 2001 From: gramps Date: Sun, 30 Aug 2026 22:09:02 -0700 Subject: [PATCH] =?UTF-8?q?refactor:=20split=20MainViewModel=20core=20unde?= =?UTF-8?q?r=20500=20lines=20=E2=80=94=20500-limit=20Phase=203,=20Commit?= =?UTF-8?q?=20A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Core MainViewModel.cs 758 -> 495 lines. Moved the source add/remove/image block (AddSource, NextSourceName, AddImage, BuildImageCandidates, PickImageBytes, AddAsset, AddImageSource, AddReusedImage, RemoveElement) into a new MainViewModel.Sources.cs partial, and WebView2 source hosting (RegisterLoadedWebSources, InitWebView2, OnWebView2PreviewBitmapChanged) into a new MainViewModel.Web.cs partial. Pruned now-dead usings from core. Zero behavior change; build 0 warnings; 246 pass, only the 2 known failures. Docs: ViewModels/index.md Phase 3 tracker updated in same commit. --- ViewModels/MainViewModel.Sources.cs | 251 ++++++++++++++++++++++++++ ViewModels/MainViewModel.Web.cs | 41 +++++ ViewModels/MainViewModel.cs | 268 ---------------------------- ViewModels/index.md | 5 + 4 files changed, 297 insertions(+), 268 deletions(-) create mode 100644 ViewModels/MainViewModel.Sources.cs create mode 100644 ViewModels/MainViewModel.Web.cs diff --git a/ViewModels/MainViewModel.Sources.cs b/ViewModels/MainViewModel.Sources.cs new file mode 100644 index 0000000..f615c1f --- /dev/null +++ b/ViewModels/MainViewModel.Sources.cs @@ -0,0 +1,251 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Windows; +using Microsoft.Win32; +using ytLive.Helpers; +using ytLive.Models; +using ytLive.Services; +using ytLive.ViewModels; + +namespace ytLive.ViewModels; + +public partial class MainViewModel +{ + private void AddSource(object? parameter) + { + var scene = StagedScene; + if (scene == null) return; + + var sourceType = parameter is SourceType typed + ? typed + : Enum.TryParse(parameter?.ToString(), true, out var parsed) ? parsed : SourceType.Image; + var baseName = sourceType switch + { + SourceType.Image => "Image", + SourceType.TextOverlay => "Text", + SourceType.Countdown => "Countdown", + SourceType.WebSource => "Web Resource", + SourceType.ChatBox => "YouTube Chat", + _ => "Layer", + }; + + // The (+) menu only adds regular layers. The Background is a locked + // per-screen layer managed by EnsureBackground/NormalizeBackgrounds — + // never user-created, never deleted. YouTube Chat is one-per-layout. + if (sourceType is SourceType.DisplayCapture or SourceType.WindowCapture or SourceType.Background) + return; + if (sourceType == SourceType.ChatBox && !CanAddYouTubeChat) + return; + + // Web resources use "Web Resource-N" naming (0-indexed per scene) + var webCount = scene.Elements.OfType().Count(s => s.Type == SourceType.WebSource); + var source = new Source + { + Name = sourceType == SourceType.WebSource + ? $"{baseName}-{webCount}" + : NextSourceName(scene, baseName), + Type = sourceType, + X = sourceType == SourceType.ChatBox ? 1400 : 0, + Y = sourceType == SourceType.ChatBox ? 20 : 0, + Width = sourceType == SourceType.WebSource ? 640 : sourceType == SourceType.ChatBox ? 500 : 0, + Height = sourceType == SourceType.WebSource ? 480 : sourceType == SourceType.ChatBox ? 648 : 0, + }; + scene.Elements.Add(source); + + if (sourceType == SourceType.WebSource) + { + _webView2Manager?.Register(source); + source.PropertyChanged += (_, e) => + { + if (e.PropertyName == nameof(Source.WebUri)) + _webView2Manager?.OnWebUriChanged(source); + }; + } + + if (sourceType == SourceType.ChatBox) + { + source.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(source); + }; + UpdateChatBoxPreview(source); + } + + OnPropertyChanged(nameof(ShowEmptySceneHint)); + OnPropertyChanged(nameof(ShowSourcesEmptyHint)); + UpdateActiveBackground(); + } + + // Duplicate resource names get an incrementing suffix with no space: Image, + // Image2, Image3… The next free number is derived from the names actually in + // the scene, so deleting a middle resource never collides with a survivor. + private static string NextSourceName(Scene scene, string baseName) + { + var taken = scene.Elements.OfType() + .Select(s => s.Name) + .Where(n => string.Equals(n, baseName, StringComparison.OrdinalIgnoreCase) + || (n.Length > baseName.Length + && n.StartsWith(baseName, StringComparison.OrdinalIgnoreCase) + && int.TryParse(n.Substring(baseName.Length), out _))) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + if (!taken.Contains(baseName)) return baseName; + for (var i = 2; ; i++) + if (!taken.Contains($"{baseName}{i}")) + return $"{baseName}{i}"; + } + + private void AddImage() + { + var scene = StagedScene; + if (scene == null) return; + + var candidates = BuildImageCandidates(); + if (candidates.Count == 0) + { + var bytes = PickImageBytes("Choose an image"); + if (bytes != null) AddImageSource(bytes); + return; + } + + var dialog = new ReuseImageDialog(new ReuseImageViewModel(candidates)) + { + Owner = Application.Current.MainWindow + }; + if (dialog.ShowDialog() != true) return; + + if (dialog.WantsNew) + { + var bytes = PickImageBytes("Choose an image"); + if (bytes != null) AddImageSource(bytes); + } + else + { + AddReusedImage(dialog.PickedAssetId!); + } + } + + private List BuildImageCandidates() + { + var candidates = new List(); + foreach (var scene in Scenes) + foreach (var source in scene.Elements.OfType().Where(s => s.Type == SourceType.Image && !string.IsNullOrWhiteSpace(s.AssetId))) + { + candidates.Add(new ReuseImageCandidate + { + SceneName = scene.Name, + SourceName = source.Name, + AssetId = source.AssetId! + }); + } + return candidates; + } + + private byte[]? PickImageBytes(string title) + { + var dialog = new OpenFileDialog + { + Title = title, + Filter = "Image files (*.png;*.jpg;*.jpeg;*.bmp;*.gif;*.webp)|*.png;*.jpg;*.jpeg;*.bmp;*.gif;*.webp|All files (*.*)|*.*" + }; + if (dialog.ShowDialog() != true) return null; + try + { + return File.ReadAllBytes(dialog.FileName); + } + catch (Exception ex) + { + _notifications.Warning("Couldn't read that image", ex.Message); + return null; + } + } + + private string? AddAsset(byte[] bytes) + { + try + { + var image = ImageCache.FromBytes(bytes); + var id = _layoutStore.UpsertAsset(bytes, image?.PixelWidth ?? 0, image?.PixelHeight ?? 0); + if (id != null && image != null) ImageCache.Put(id, image); + return id; + } + catch (Exception ex) + { + Debug.WriteLine($"Asset store failed: {ex.Message}"); + return null; + } + } + + private void AddImageSource(byte[] bytes) + { + if (bytes.Length == 0) return; + var assetId = AddAsset(bytes); + if (assetId != null) AddReusedImage(assetId); + } + + private void AddReusedImage(string assetId) + { + var scene = StagedScene; + if (scene == null || string.IsNullOrWhiteSpace(assetId)) return; + + var source = new Source { Name = NextSourceName(scene, "Image"), Type = SourceType.Image, AssetId = assetId }; + + var image = ImageCache.Get(assetId); + if (image != null) + { + var scale = Math.Min(640.0 / image.PixelWidth, 480.0 / image.PixelHeight); + source.Width = image.PixelWidth * scale; + source.Height = image.PixelHeight * scale; + source.X = (1920 - source.Width) / 2; + source.Y = (1080 - source.Height) / 2; + } + + scene.Elements.Add(source); + SelectedElement = source; + OnPropertyChanged(nameof(ShowEmptySceneHint)); + OnPropertyChanged(nameof(ShowSourcesEmptyHint)); + UpdateActiveBackground(); + } + + // Removes an element from the active scene. For a webcam config this drops the + // scene's usage and releases one camera reference (CameraManager stops the + // session when the last using scene lets go). When the last config anywhere is + // removed, the webcam identity is cleared too — re-adding opens the picker + // again instead of silently resurrecting the old camera. + private void RemoveElement(SceneElement? element) + { + var scene = StagedScene; + if (scene == null || element == null) return; + if (element is Source { IsBackground: true }) return; + if (element is WebcamSceneConfig && _webcam != null) + { + if (SelectedElement == element) + SelectedElement = null; + _ = _cameraManager.ReleaseAsync(_webcam.DeviceId); + } + if (element is Source { Type: SourceType.WebSource } webSource) + _webView2Manager?.Unregister(webSource.Id); + scene.Elements.Remove(element); + + if (element is WebcamSceneConfig && _webcam != null + && !Scenes.Any(s => s.Elements.OfType().Any())) + { + _webcam = null; + OnPropertyChanged(nameof(CanChangeWebcam)); + OnPropertyChanged(nameof(CanAddWebcam)); + OnPropertyChanged(nameof(CanShowWebcamInStagedScene)); + } + + OnPropertyChanged(nameof(ShowEmptySceneHint)); + OnPropertyChanged(nameof(ShowSourcesEmptyHint)); + UpdateActiveBackground(); + } +} diff --git a/ViewModels/MainViewModel.Web.cs b/ViewModels/MainViewModel.Web.cs new file mode 100644 index 0000000..48048b0 --- /dev/null +++ b/ViewModels/MainViewModel.Web.cs @@ -0,0 +1,41 @@ +using System.Linq; +using System.Windows.Controls; +using System.Windows.Media.Imaging; +using ytLive.Models; +using ytLive.Services; +using ytLive.ViewModels; + +namespace ytLive.ViewModels; + +public partial class MainViewModel +{ + private void RegisterLoadedWebSources() + { + if (_webView2Manager == null) return; + + foreach (var source in Scenes.SelectMany(s => s.Elements).OfType() + .Where(s => s.Type == SourceType.WebSource)) + { + _webView2Manager.Register(source); + source.PropertyChanged += (_, e) => + { + if (e.PropertyName == nameof(Source.WebUri)) + _webView2Manager.OnWebUriChanged(source); + }; + } + } + + public void InitWebView2(Panel hostPanel) + { + _webView2Manager = new WebView2Manager(hostPanel, System.Windows.Application.Current.Dispatcher); + _webView2Manager.PreviewBitmapChanged += OnWebView2PreviewBitmapChanged; + RegisterLoadedWebSources(); + } + + private void OnWebView2PreviewBitmapChanged(string sourceId, WriteableBitmap bitmap) + { + foreach (var scene in Scenes) + foreach (var el in scene.Elements.OfType().Where(s => s.Id == sourceId)) + el.VideoImageSource = bitmap; + } +} diff --git a/ViewModels/MainViewModel.cs b/ViewModels/MainViewModel.cs index da01221..11d6139 100644 --- a/ViewModels/MainViewModel.cs +++ b/ViewModels/MainViewModel.cs @@ -1,5 +1,3 @@ -using System.Collections.ObjectModel; -using System.Collections.Specialized; using System.ComponentModel; using System.Diagnostics; using System.IO; @@ -8,10 +6,7 @@ using System.Windows.Controls; using System.Windows.Input; using System.Windows.Interop; using System.Windows.Media; -using System.Windows.Media.Imaging; using System.Windows.Threading; -using Microsoft.Win32; -using NAudio.CoreAudioApi; using ytLive.Helpers; using ytLive.Models; using ytLive.Services; @@ -383,35 +378,6 @@ public partial class MainViewModel : ViewModelBase AppLog.Write("LoadLayout end"); } - private void RegisterLoadedWebSources() - { - if (_webView2Manager == null) return; - - foreach (var source in Scenes.SelectMany(s => s.Elements).OfType() - .Where(s => s.Type == SourceType.WebSource)) - { - _webView2Manager.Register(source); - source.PropertyChanged += (_, e) => - { - if (e.PropertyName == nameof(Source.WebUri)) - _webView2Manager.OnWebUriChanged(source); - }; - } - } - - public void InitWebView2(Panel hostPanel) - { - _webView2Manager = new WebView2Manager(hostPanel, System.Windows.Application.Current.Dispatcher); - _webView2Manager.PreviewBitmapChanged += OnWebView2PreviewBitmapChanged; - RegisterLoadedWebSources(); - } - - private void OnWebView2PreviewBitmapChanged(string sourceId, WriteableBitmap bitmap) - { - foreach (var scene in Scenes) - foreach (var el in scene.Elements.OfType().Where(s => s.Id == sourceId)) - el.VideoImageSource = bitmap; - } public void Shutdown() { @@ -463,240 +429,6 @@ public partial class MainViewModel : ViewModelBase OnPropertyChanged(nameof(BackgroundVisible)); } - private void AddSource(object? parameter) - { - var scene = StagedScene; - if (scene == null) return; - - var sourceType = parameter is SourceType typed - ? typed - : Enum.TryParse(parameter?.ToString(), true, out var parsed) ? parsed : SourceType.Image; - var baseName = sourceType switch - { - SourceType.Image => "Image", - SourceType.TextOverlay => "Text", - SourceType.Countdown => "Countdown", - SourceType.WebSource => "Web Resource", - SourceType.ChatBox => "YouTube Chat", - _ => "Layer", - }; - - // The (+) menu only adds regular layers. The Background is a locked - // per-screen layer managed by EnsureBackground/NormalizeBackgrounds — - // never user-created, never deleted. YouTube Chat is one-per-layout. - if (sourceType is SourceType.DisplayCapture or SourceType.WindowCapture or SourceType.Background) - return; - if (sourceType == SourceType.ChatBox && !CanAddYouTubeChat) - return; - - // Web resources use "Web Resource-N" naming (0-indexed per scene) - var webCount = scene.Elements.OfType().Count(s => s.Type == SourceType.WebSource); - var source = new Source - { - Name = sourceType == SourceType.WebSource - ? $"{baseName}-{webCount}" - : NextSourceName(scene, baseName), - Type = sourceType, - X = sourceType == SourceType.ChatBox ? 1400 : 0, - Y = sourceType == SourceType.ChatBox ? 20 : 0, - Width = sourceType == SourceType.WebSource ? 640 : sourceType == SourceType.ChatBox ? 500 : 0, - Height = sourceType == SourceType.WebSource ? 480 : sourceType == SourceType.ChatBox ? 648 : 0, - }; - scene.Elements.Add(source); - - if (sourceType == SourceType.WebSource) - { - _webView2Manager?.Register(source); - source.PropertyChanged += (_, e) => - { - if (e.PropertyName == nameof(Source.WebUri)) - _webView2Manager?.OnWebUriChanged(source); - }; - } - - if (sourceType == SourceType.ChatBox) - { - source.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(source); - }; - UpdateChatBoxPreview(source); - } - - OnPropertyChanged(nameof(ShowEmptySceneHint)); - OnPropertyChanged(nameof(ShowSourcesEmptyHint)); - UpdateActiveBackground(); - } - - // Duplicate resource names get an incrementing suffix with no space: Image, - // Image2, Image3… The next free number is derived from the names actually in - // the scene, so deleting a middle resource never collides with a survivor. - private static string NextSourceName(Scene scene, string baseName) - { - var taken = scene.Elements.OfType() - .Select(s => s.Name) - .Where(n => string.Equals(n, baseName, StringComparison.OrdinalIgnoreCase) - || (n.Length > baseName.Length - && n.StartsWith(baseName, StringComparison.OrdinalIgnoreCase) - && int.TryParse(n.Substring(baseName.Length), out _))) - .ToHashSet(StringComparer.OrdinalIgnoreCase); - if (!taken.Contains(baseName)) return baseName; - for (var i = 2; ; i++) - if (!taken.Contains($"{baseName}{i}")) - return $"{baseName}{i}"; - } - - private void AddImage() - { - var scene = StagedScene; - if (scene == null) return; - - var candidates = BuildImageCandidates(); - if (candidates.Count == 0) - { - var bytes = PickImageBytes("Choose an image"); - if (bytes != null) AddImageSource(bytes); - return; - } - - var dialog = new ReuseImageDialog(new ReuseImageViewModel(candidates)) - { - Owner = Application.Current.MainWindow - }; - if (dialog.ShowDialog() != true) return; - - if (dialog.WantsNew) - { - var bytes = PickImageBytes("Choose an image"); - if (bytes != null) AddImageSource(bytes); - } - else - { - AddReusedImage(dialog.PickedAssetId!); - } - } - - private List BuildImageCandidates() - { - var candidates = new List(); - foreach (var scene in Scenes) - foreach (var source in scene.Elements.OfType().Where(s => s.Type == SourceType.Image && !string.IsNullOrWhiteSpace(s.AssetId))) - { - candidates.Add(new ReuseImageCandidate - { - SceneName = scene.Name, - SourceName = source.Name, - AssetId = source.AssetId! - }); - } - return candidates; - } - - private byte[]? PickImageBytes(string title) - { - var dialog = new OpenFileDialog - { - Title = title, - Filter = "Image files (*.png;*.jpg;*.jpeg;*.bmp;*.gif;*.webp)|*.png;*.jpg;*.jpeg;*.bmp;*.gif;*.webp|All files (*.*)|*.*" - }; - if (dialog.ShowDialog() != true) return null; - try - { - return File.ReadAllBytes(dialog.FileName); - } - catch (Exception ex) - { - _notifications.Warning("Couldn't read that image", ex.Message); - return null; - } - } - - private string? AddAsset(byte[] bytes) - { - try - { - var image = ImageCache.FromBytes(bytes); - var id = _layoutStore.UpsertAsset(bytes, image?.PixelWidth ?? 0, image?.PixelHeight ?? 0); - if (id != null && image != null) ImageCache.Put(id, image); - return id; - } - catch (Exception ex) - { - Debug.WriteLine($"Asset store failed: {ex.Message}"); - return null; - } - } - - private void AddImageSource(byte[] bytes) - { - if (bytes.Length == 0) return; - var assetId = AddAsset(bytes); - if (assetId != null) AddReusedImage(assetId); - } - - private void AddReusedImage(string assetId) - { - var scene = StagedScene; - if (scene == null || string.IsNullOrWhiteSpace(assetId)) return; - - var source = new Source { Name = NextSourceName(scene, "Image"), Type = SourceType.Image, AssetId = assetId }; - - var image = ImageCache.Get(assetId); - if (image != null) - { - var scale = Math.Min(640.0 / image.PixelWidth, 480.0 / image.PixelHeight); - source.Width = image.PixelWidth * scale; - source.Height = image.PixelHeight * scale; - source.X = (1920 - source.Width) / 2; - source.Y = (1080 - source.Height) / 2; - } - - scene.Elements.Add(source); - SelectedElement = source; - OnPropertyChanged(nameof(ShowEmptySceneHint)); - OnPropertyChanged(nameof(ShowSourcesEmptyHint)); - UpdateActiveBackground(); - } - - // Removes an element from the active scene. For a webcam config this drops the - // scene's usage and releases one camera reference (CameraManager stops the - // session when the last using scene lets go). When the last config anywhere is - // removed, the webcam identity is cleared too — re-adding opens the picker - // again instead of silently resurrecting the old camera. - private void RemoveElement(SceneElement? element) - { - var scene = StagedScene; - if (scene == null || element == null) return; - if (element is Source { IsBackground: true }) return; - if (element is WebcamSceneConfig && _webcam != null) - { - if (SelectedElement == element) - SelectedElement = null; - _ = _cameraManager.ReleaseAsync(_webcam.DeviceId); - } - if (element is Source { Type: SourceType.WebSource } webSource) - _webView2Manager?.Unregister(webSource.Id); - scene.Elements.Remove(element); - - if (element is WebcamSceneConfig && _webcam != null - && !Scenes.Any(s => s.Elements.OfType().Any())) - { - _webcam = null; - OnPropertyChanged(nameof(CanChangeWebcam)); - OnPropertyChanged(nameof(CanAddWebcam)); - OnPropertyChanged(nameof(CanShowWebcamInStagedScene)); - } - - OnPropertyChanged(nameof(ShowEmptySceneHint)); - OnPropertyChanged(nameof(ShowSourcesEmptyHint)); - UpdateActiveBackground(); - } // Scene-element → latest frame, for the live compositor. The map mirrors the // preview: webcam by DeviceId, live captures (the background) by CaptureKey, diff --git a/ViewModels/index.md b/ViewModels/index.md index 43d2b38..81bfc8f 100644 --- a/ViewModels/index.md +++ b/ViewModels/index.md @@ -62,5 +62,10 @@ Progress (Commit N/12 · tag): - [x] **Commit 17 · `refactor-commit-17`** — **LeftPanel** → `Controls/LeftPanel.xaml(.cs)` (Row 1 Col 0 left panel: layers/properties edit UI + live chat list). Moved ALL property-editor and chat handlers: `SourceNameBox_*`/`CommitSourceEdit`, `AddSourceButton_Click`, `OnChatMessagesChanged` (now self-subscribed on Loaded), `MirrorButton_Click`, `ShapeButton_Click`, `SourceMenu_Remove`, `BackgroundMenu_*`, `OpacitySlider_ValueChanged`, list drag-to-reorder (`_dragStartPoint`+`List_Preview*`+`EndListDrag`+`FindItemContainerAt`), and the snapshot family (`TakeSnapshot`, `WebcamBorder*`, `Countdown*`, `Web*`). Window exposes via `_leftPanel.IsClickInside(original)` (deselect guard) and `_leftPanel.OnSelectionChanged(vm)` (opacity label + snapshot). Pending: overlay host region. - [x] **Commit 18 · `refactor-commit-18`** — **OverlayHost** → `Controls/OverlayHost.xaml(.cs)` (settings/bug/feature/about dialogs). `BackToAbout_Click` moved with it (uses inherited `DataContext`). Window keeps the thin `WebViewHostPanel` + `ToastArea` root stubs so name lookups are untouched (WebView2 ctor init, `NotificationService.InArea("ToastArea")`) — toast/WebView2 behavior unchanged. **Phase 2 COMPLETE** — MainWindow.xaml carved down to a 123-line shell over 6 controls (TopBar, HealthBanner, BottomBar, SceneThumbnailStrip, LeftPanel, PreviewPane, OverlayHost). Pending: session wrap-up (HANDOFF.md) + push on approval. +**Phase 3 — 500-line compliance (production `.cs` only).** Bring all production +source files under 500 lines. One roll-back commit per file; same drill +(0-warning build, `verify.sh` 246+2, explicit-file scope-check, tracker in same +commit, tag `refactor-commit-N`, no per-commit push). +- [x] **Commit A · `refactor-commit-A`** — **core** → `MainViewModel.cs` 758 → **495**. Moved source add/remove/image block to new **`MainViewModel.Sources.cs`** (234 lines: `AddSource`, `NextSourceName`, `AddImage`, `BuildImageCandidates`, `PickImageBytes`, `AddAsset`, `AddImageSource`, `AddReusedImage`, `RemoveElement`) and WebView2 hosting (`RegisterLoadedWebSources`, `InitWebView2`, `OnWebView2PreviewBitmapChanged`) to new **`MainViewModel.Web.cs`**. Pruned now-dead usings (`Microsoft.Win32`, `NAudio.CoreAudioApi`, `System.Windows.Media.Imaging`, `Collections.ObjectModel/Specialized`). Pending: Streaming, Background, SocialsDialogViewModel, LayoutStore. 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. \ No newline at end of file