diff --git a/ViewModels/MainViewModel.Socials.cs b/ViewModels/MainViewModel.Socials.cs
new file mode 100644
index 0000000..3e216a2
--- /dev/null
+++ b/ViewModels/MainViewModel.Socials.cs
@@ -0,0 +1,161 @@
+using System.Windows;
+using System.Windows.Input;
+using System.Windows.Media;
+using ytLive;
+using ytLive.Helpers;
+using ytLive.Models;
+using ytLive.Services;
+using ytLive.Services.Compositor;
+
+namespace ytLive.ViewModels;
+
+public partial class MainViewModel
+{
+ private SocialsConfig? _socials;
+ private VideoFrame? _socialBarFrame;
+ private readonly ISocialValidator _socialValidator = new HttpSocialValidator();
+
+ public ICommand OpenSocialDialogCommand { get; }
+
+ // ─── Social bar (global resource, shown on every scene) ───
+
+ /// True when the global socials config has at least one validated entry.
+ public bool HasSocials => _socials != null && _socials.Entries.Count > 0;
+
+ /// The bar is on the stream when it's enabled and has entries.
+ public bool SocialBarVisible => (_socials?.BarEnabled ?? false) && HasSocials;
+
+ /// Footer indicator dot: green when the bar is on the stream, red otherwise.
+ public SolidColorBrush SocialBarDotBrush => SocialBarVisible ? BarOnBrush : BarOffBrush;
+
+ /// Green glow on the bar while it's on the stream.
+ public SolidColorBrush SocialBarGlowBrush => BarOnBrush;
+
+ /// The global socials config (entries + bar settings), or null.
+ public SocialsConfig? Socials => _socials;
+
+ /// Canvas.Top for the social bar: 0 = top, 1040 = bottom (40px from the 1080 edge).
+ public double SocialBarTop => _socials?.BarPosition == SocialBarPosition.Top ? 0 : 1040;
+
+
+ ///
+ /// Resolves a missing nodeinfo software name (mastodon, peertube, ...) for every
+ /// fediverse entry that doesn't have one, so the bar shows the instance's real
+ /// logo instead of the generic fediverse glyph. Best-effort: a failed resolution
+ /// leaves the glyph untouched. Returns handle → software for what was resolved —
+ /// the caller applies + persists (the app hops to the dispatcher; a test applies
+ /// directly).
+ ///
+ public static async Task> HealFediverseSoftwareAsync(
+ SocialsConfig socials,
+ ISocialValidator validator,
+ Action? log = null)
+ {
+ var resolved = new Dictionary();
+ if (socials == null || validator == null) return resolved;
+ foreach (var entry in socials.Entries)
+ {
+ if (entry.Service != SocialService.Fediverse
+ || !string.IsNullOrWhiteSpace(entry.FediverseSoftware))
+ continue;
+ if (!SocialServiceIcons.TryParseFediverse(entry.Handle, out _, out var domain))
+ continue;
+ var software = await validator.ResolveFediverseSoftwareAsync(
+ domain, System.Threading.CancellationToken.None);
+ if (string.IsNullOrWhiteSpace(software)) continue;
+ resolved[entry.Handle] = software;
+ log?.Invoke($"Socials heal: {entry.Handle} → {software}");
+ }
+ return resolved;
+ }
+
+ /// Runs the heal off the UI thread and applies + saves any result.
+ private void HealFediverseSoftwareInBackground()
+ {
+ var socials = _socials;
+ if (socials == null) return;
+ _ = Task.Run(async () =>
+ {
+ try
+ {
+ var resolved = await HealFediverseSoftwareAsync(
+ socials, _socialValidator, m => AppLog.Write(m));
+ if (resolved.Count == 0) return;
+ await Application.Current.Dispatcher.InvokeAsync(() =>
+ {
+ var current = _socials;
+ if (current == null) return;
+ var applied = false;
+ foreach (var entry in current.Entries)
+ {
+ if (!resolved.TryGetValue(entry.Handle, out var software)) continue;
+ if (entry.FediverseSoftware == software) continue;
+ entry.FediverseSoftware = software;
+ applied = true;
+ }
+ if (applied) NotifySocialsChanged(); // re-renders the bar + saves
+ });
+ }
+ catch (Exception ex)
+ {
+ AppLog.Write($"Socials heal failed: {ex.Message}");
+ }
+ });
+ }
+
+ ///
+ /// Re-rasterizes the social bar strip the output compositor overlays. UI thread
+ /// only (WPF rendering); the resulting frame is immutable, so the frame pump may
+ /// read it from its own thread. Null when the bar is off or empty.
+ ///
+ private void RenderSocialBarFrame()
+ {
+ _socialBarFrame = _socials != null && _socials.BarEnabled
+ ? SocialBarRenderer.Render(_socials.Entries)
+ : null;
+ }
+
+ ///
+ /// Opens the Social Media Site Promotion dialog (6 slots, sign-in gate,
+ /// validation). On save the working copy replaces .
+ ///
+ private void OpenSocialDialog()
+ {
+ var dialog = new SocialsDialogViewModel(
+ _socialValidator,
+ SignInAsync,
+ SignOutYouTubeAsync,
+ _youtubeAuth.CurrentChannel,
+ _socials);
+ var window = new ytLive.SocialsDialog(dialog) { Owner = Application.Current.MainWindow };
+ if (window.ShowDialog() == true)
+ {
+ _socials = dialog.BuildSocialsConfig();
+ NotifySocialsChanged();
+ }
+ }
+
+ private void NotifySocialsChanged()
+ {
+ RenderSocialBarFrame();
+ OnPropertyChanged(nameof(HasSocials));
+ OnPropertyChanged(nameof(SocialBarVisible));
+ OnPropertyChanged(nameof(SocialBarDotBrush));
+ ScheduleSave();
+ }
+
+ /// Sets the bar's edge; called by the preview click-toggle.
+ public void SetSocialBarPosition(SocialBarPosition position)
+ {
+ if (_socials == null || _socials.BarPosition == position) return;
+ _socials.BarPosition = position;
+ OnPropertyChanged(nameof(SocialBarTop));
+ ScheduleSave();
+ }
+
+ /// Preview click flips the bar top ⇄ bottom (KISS — no drag math).
+ public void ToggleSocialBarPosition()
+ => SetSocialBarPosition(_socials?.BarPosition == SocialBarPosition.Top
+ ? SocialBarPosition.Bottom
+ : SocialBarPosition.Top);
+}
diff --git a/ViewModels/MainViewModel.cs b/ViewModels/MainViewModel.cs
index 34ffa95..5629916 100644
--- a/ViewModels/MainViewModel.cs
+++ b/ViewModels/MainViewModel.cs
@@ -83,9 +83,6 @@ public partial class MainViewModel : ViewModelBase
// news up the real service; the VM is built from XAML so no DI seam exists.
private readonly INotificationService _notifications = new NotificationService();
- private SocialsConfig? _socials;
- private VideoFrame? _socialBarFrame;
-
// Live frame pipeline (TASK 4 ship step 5): composites the active scene at the
// tier's FPS and paces frames into the encoder. The RTMP URL seam stays null
// until the live-stream create flow (TASK 5) supplies the reusable stream URL.
@@ -169,33 +166,11 @@ public partial class MainViewModel : ViewModelBase
=> StagedScene != null
&& !Scenes.Any(s => s.Elements.OfType().Any(e => e.Type == SourceType.ChatBox));
- // ─── Social bar (global resource, shown on every scene) ───
-
private static readonly SolidColorBrush BarOnBrush = CreateBrush("#2ecc71");
private static readonly SolidColorBrush BarOffBrush = CreateBrush("#e94560");
private static SolidColorBrush CreateBrush(string hex)
=> (SolidColorBrush)new BrushConverter().ConvertFromString(hex)!;
- /// True when the global socials config has at least one validated entry.
- public bool HasSocials => _socials != null && _socials.Entries.Count > 0;
-
- /// The bar is on the stream when it's enabled and has entries.
- public bool SocialBarVisible => (_socials?.BarEnabled ?? false) && HasSocials;
-
- /// Footer indicator dot: green when the bar is on the stream, red otherwise.
- public SolidColorBrush SocialBarDotBrush => SocialBarVisible ? BarOnBrush : BarOffBrush;
-
- /// Green glow on the bar while it's on the stream.
- public SolidColorBrush SocialBarGlowBrush => BarOnBrush;
-
- /// The global socials config (entries + bar settings), or null.
- public SocialsConfig? Socials => _socials;
-
- /// Canvas.Top for the social bar: 0 = top, 1040 = bottom (40px from the 1080 edge).
- public double SocialBarTop => _socials?.BarPosition == SocialBarPosition.Top ? 0 : 1040;
-
- private readonly ISocialValidator _socialValidator = new HttpSocialValidator();
-
/// True when the Elements panel should show the "No configurable elements" hint.
public bool IsElementsPanelEmpty => SelectedElement is { } el
&& !el.IsDraggable && !el.IsWebcam
@@ -586,7 +561,6 @@ public partial class MainViewModel : ViewModelBase
public ICommand RemoveSourceCommand { get; }
public ICommand EditElementCommand { get; }
public ICommand ToggleElementVisibilityCommand { get; }
- public ICommand OpenSocialDialogCommand { get; }
public ICommand StartStreamCommand { get; }
public ICommand EndStreamCommand { get; }
public ICommand ChooseRecordFolderCommand { get; }
@@ -1913,127 +1887,4 @@ public partial class MainViewModel : ViewModelBase
OnPropertyChanged(nameof(RecDotOpacity));
}
- // ─── Social bar ───
-
- ///
- /// Resolves a missing nodeinfo software name (mastodon, peertube, ...) for every
- /// fediverse entry that doesn't have one, so the bar shows the instance's real
- /// logo instead of the generic fediverse glyph. Best-effort: a failed resolution
- /// leaves the glyph untouched. Returns handle → software for what was resolved —
- /// the caller applies + persists (the app hops to the dispatcher; a test applies
- /// directly).
- ///
- public static async Task> HealFediverseSoftwareAsync(
- SocialsConfig socials,
- ISocialValidator validator,
- Action? log = null)
- {
- var resolved = new Dictionary();
- if (socials == null || validator == null) return resolved;
- foreach (var entry in socials.Entries)
- {
- if (entry.Service != SocialService.Fediverse
- || !string.IsNullOrWhiteSpace(entry.FediverseSoftware))
- continue;
- if (!SocialServiceIcons.TryParseFediverse(entry.Handle, out _, out var domain))
- continue;
- var software = await validator.ResolveFediverseSoftwareAsync(
- domain, System.Threading.CancellationToken.None);
- if (string.IsNullOrWhiteSpace(software)) continue;
- resolved[entry.Handle] = software;
- log?.Invoke($"Socials heal: {entry.Handle} → {software}");
- }
- return resolved;
- }
-
- /// Runs the heal off the UI thread and applies + saves any result.
- private void HealFediverseSoftwareInBackground()
- {
- var socials = _socials;
- if (socials == null) return;
- _ = Task.Run(async () =>
- {
- try
- {
- var resolved = await HealFediverseSoftwareAsync(
- socials, _socialValidator, m => AppLog.Write(m));
- if (resolved.Count == 0) return;
- await Application.Current.Dispatcher.InvokeAsync(() =>
- {
- var current = _socials;
- if (current == null) return;
- var applied = false;
- foreach (var entry in current.Entries)
- {
- if (!resolved.TryGetValue(entry.Handle, out var software)) continue;
- if (entry.FediverseSoftware == software) continue;
- entry.FediverseSoftware = software;
- applied = true;
- }
- if (applied) NotifySocialsChanged(); // re-renders the bar + saves
- });
- }
- catch (Exception ex)
- {
- AppLog.Write($"Socials heal failed: {ex.Message}");
- }
- });
- }
-
- ///
- /// Re-rasterizes the social bar strip the output compositor overlays. UI thread
- /// only (WPF rendering); the resulting frame is immutable, so the frame pump may
- /// read it from its own thread. Null when the bar is off or empty.
- ///
- private void RenderSocialBarFrame()
- {
- _socialBarFrame = _socials != null && _socials.BarEnabled
- ? SocialBarRenderer.Render(_socials.Entries)
- : null;
- }
-
- ///
- /// Opens the Social Media Site Promotion dialog (6 slots, sign-in gate,
- /// validation). On save the working copy replaces .
- ///
- private void OpenSocialDialog()
- {
- var dialog = new SocialsDialogViewModel(
- _socialValidator,
- SignInAsync,
- SignOutYouTubeAsync,
- _youtubeAuth.CurrentChannel,
- _socials);
- var window = new ytLive.SocialsDialog(dialog) { Owner = Application.Current.MainWindow };
- if (window.ShowDialog() == true)
- {
- _socials = dialog.BuildSocialsConfig();
- NotifySocialsChanged();
- }
- }
-
- private void NotifySocialsChanged()
- {
- RenderSocialBarFrame();
- OnPropertyChanged(nameof(HasSocials));
- OnPropertyChanged(nameof(SocialBarVisible));
- OnPropertyChanged(nameof(SocialBarDotBrush));
- ScheduleSave();
- }
-
- /// Sets the bar's edge; called by the preview click-toggle.
- public void SetSocialBarPosition(SocialBarPosition position)
- {
- if (_socials == null || _socials.BarPosition == position) return;
- _socials.BarPosition = position;
- OnPropertyChanged(nameof(SocialBarTop));
- ScheduleSave();
- }
-
- /// Preview click flips the bar top ⇄ bottom (KISS — no drag math).
- public void ToggleSocialBarPosition()
- => SetSocialBarPosition(_socials?.BarPosition == SocialBarPosition.Top
- ? SocialBarPosition.Bottom
- : SocialBarPosition.Top);
-
}
diff --git a/ViewModels/index.md b/ViewModels/index.md
index ca393e8..a197645 100644
--- a/ViewModels/index.md
+++ b/ViewModels/index.md
@@ -2,9 +2,21 @@
MVVM layer. See [`schema.md`](../schema.md) for the memory-map conventions.
+Each landed `MainViewModel` partial owns a row here describing its area; the
+`MainViewModel.cs` row describes only what still lives in core. Docking most
+areas' docs in their own rows keeps this index small to read.
+
| File | Purpose |
|------|---------|
-| `MainViewModel.cs` | `public partial class MainViewModel : ViewModelBase`. The app brain: scenes/elements collections + commands, stream state (`IsLive`/`IsOffline`/`IsConnected`), chat feed, overlays, layout save/open, **resolution dropdown** (`QualityOptions`, `SelectedQuality`, `ResolutionHelp`, `ApplyStreamQuality`) that computes the output rect over the 1920×1080 master (`OutputRectX/Y/W/H`, `DimRects`, `IsOutputCropped`, `ResolutionBadgeText`). Auth: loads the saved DPAPI session at startup (`LoadSavedSessionAsync`), `SignInAsync` feeds the GoLive dialog; **go-live (ship step 7): `BeginGoLive` accepts the dialog → sets `StreamVisibility` (always "Private") → fires `CreateBroadcastAsync` (stores `_currentBroadcastId` for TASK 9's bind/transition; failure → `StreamStatus.Error`) + `_framePump.StartAsync()`**; **End Livestream signs out** (`StopStream` clears session + token + `_currentBroadcastId` — crash-safe). **Connected account:** top-bar avatar/name (`AccountAvatarUrl`/`AccountDisplayName` via `SyncConnectedAccount`, cleared on End). **Element rows:** scenes are pure selection rows (no per-row icons); source rows carry edit/visibility/trash (`EditElementCommand` sets `element.IsEditing`, `ToggleElementVisibilityCommand` flips `IsVisible`, `RemoveSourceCommand`; the Backdrop's trash slot stays **reserved** via `HiddenBoolToVisibilityConverter` so the edit/eye icons keep their fixed columns — TASK 13); duplicate resource names get a no-space incrementing suffix via shared `NextSourceName` (Image, Image2, Image3… — next free number derived from actual names, so deletions never collide; used by `AddSource` + `AddReusedImage`). **Audio (KISS — the mic is the creator's only audio control; capture SHIPPED, runs for the app's lifetime so the meters preview live):** the sound meter is a **READ-ONLY realtime level display**: fill = `Math.Min(1, AudioLevelMeter.ToDisplay((float)AudioLevel) * MicVolume)` (`MeterFillWidth`/`MeterBrush`, green→yellow→red, zone markers at 60%/80% — `ToDisplay` maps the raw linear RMS onto a −60..0 dBFS scale with +10 dB amplification so real speech/game levels occupy the bar and peaks hit red at maxed volume), where `AudioLevel` (live input, 0 with no input) is fed by the audio mixer once capture lands and `MicVolume` acts as a gain on ambient noise; while the slider is dragged the bar previews the slider position (`SetVolumeAdjusting`), returning to the live level on release (0 with no input — clicking the meter does nothing); `MicVolume` (default 0.8) + read-only `MicMuted`/`MicMuteText`/`ToggleMicMuteCommand` — **MicVolume drives MicMuted** (muted ⇔ volume 0): sliding to 0 flips the speaker to muted, sliding up from 0 clears it; muting stores the prior volume, unmuting restores it (default 0.8 if unknown) and flashes the meter to the restored position ~300ms (`BeginVolumeFlash`/`EndVolumeFlash`); `MicSourceName` = picked voice source, shown left-justified inside the meter bar (the fill runs at 75% opacity so the text + ruler markings show through); `OpenMicPickerCommand`/`PickMicrophone()` open the `MicPickerDialog` (a picked device takes effect immediately — `PickMicrophone` swaps the live source via `_audioMixer.RestartMic()`, loopback keeps running); the **MIC label is a button** (`OpenMicPickerCommand`) with a **status dot** (`MicStatus`, `Models/MicStatus`: green = `MicConnected` via the source's `Started` event, yellow = `MicFailed` — in use/unplugged, red = no mic device at startup; `MicStatusBrush`/`MicStatusToolTip`); capture starts once at startup (`StartMicCaptureAsync`) — NOT go-live (`BeginGoLive`/`StopStream` no longer touch the mixer) — zero devices = red dot + the mixer never starts. **Game audio bar** (desktop/game, **overlaid at the bottom of the preview window** — bottom-center chip, a mirror of the mic bar; **always visible** — the TASK 4 show/hide gating (`IGameAudioDetector`/`GameAudioDetector`/`GameAudioHysteresis` + the 250ms poll timer) was **removed 2026-08-15** because the bar kept hiding the desktop meter whenever no full-screen game with sound was up): `GameAudioLevel` (loopback meter via `LoopbackLevelChanged`, scaled by volume), `GameMuted`/`GameMuteText`/`ToggleGameMuteCommand`, `GameAudioVolume` (0..1 volume slider), `Begin/End/CancelGameVolumeFlash` (mirrors the mic bar's volume flash); the game speaker + slider share the mic bar's `PreviewMouseLeftButtonDown/Up` + `LostMouseCapture` code-behind pattern. **Webcam:** owns `CameraManager` (MediaCapture), `AddWebcamToActiveSceneAsync` (picker → default 480×270 bottom-right placement → acquire; the Windows camera picker ALWAYS opens so the creator chooses — never silently reuses the previous camera; picking a different camera swaps the app-wide identity via `SwapWebcamIdentityAsync`, same path as `ChangeWebcamAsync`; propagates the running shared bitmap via `CameraManager.GetPreviewBitmap` first, so a webcam added mid-session never renders a transparent container; greys out when the active scene already has a config via `CanAddWebcamToActiveScene`), `ChangeWebcamAsync` (device swap — `ReleaseAllAsync` old + re-acquire), `ShowWebcamInActiveScene` (reveal hidden config / empty-canvas right-click), `ReacquireWebcam` after layout load (re-propagates the shared bitmap to every config), `OnCameraPreviewBitmapChanged` forwards the shared bitmap into every `WebcamSceneConfig.VideoImageSource`; `SelectedElement` drives the preview overlay + `internal ClampWebcamToBounds(config, sceneName)` (per-scene size cap seam: 50%-per-dimension everywhere, half-screen-AREA in Chat via `MaxWebcamWidthFor`/`MaxWebcamHeightFor`). **Scenes (five-scene catalog):** empty DB seeds Starting/Live/BRB/Chat/Ending (`SceneCatalog.All`); `AddScene(name)` accepts only canonical, missing names; `MissingScenes`/`ShowAddScene` drive the "+" button (hidden once all five exist; its menu lists only the missing scenes via `AddSceneCommand`). **Screen backdrop (Live-only by policy):** owns `ScreenCaptureManager` + `ScreenCaptureSourceFactory` (WinRT GraphicsCapture), `internal static EnsureBackdrop` heals one per backdrop-enabled scene (gated on `Scene.HasBackdrop`), `ReacquireScreenCaptures`/`RefreshBackdropAutoCapture`/`NoteBackgroundWindow` key by full-screen game monitor or the **primary display** via `Win32FullScreenDetector` (`GetDisplays()`/`PrimaryMonitorIndex()`), `ChangeBackdropCaptureAsync` (OS picker) + `SetBackdropCapture(DisplayInfo)` (in-app "Capture Display"), `RedesignateBackdropAsync` re-targets all backdrops and releases orphaned sessions, `internal static EnforceBackdropPolicy` normalizes `Scene.HasBackdrop` by name on every load + strips lingering non-Live backdrops, `CanChangeBackdrop` gates the capture menu to Live (the only scene with a backdrop), `BackdropImage` hides the preview watermark (`ShowPreviewPlaceholder`). **REC sign (2026-08-13):** top-center indicator always visible — `RecDotBrush` (offline `#555555`, live `#e94560`, live-private `#8f1f1f`), `RecTextBrush` (dim offline, white live), `RecDotOpacity` (0.55 offline, pulsing 1.0/0.35 live via `_recDotPulse` flipped on the live tick), `IsLivePrivate` (`IsLive && StreamVisibility == "Private"`) — notified from the `StreamStatus` + `StreamVisibility` setters; `LiveIndicatorVisible`/`LivePulseOpacity` removed. **Mic mute icon:** a second 16px clickable glyph (mic, red + slash when muted) between the meter and the speaker on the mic bar — same `ToggleMicMuteCommand`. **TASK 11 (2026-08-15):** the gain seams (`AudioGainProvider` → `micGain`/`loopbackGain`) reach the mixer at construction so volume sliders + mutes are stream-honest; the game bar also scales TRAX locally (`MusicPlayer.LocalGain`); `MicSourceName` persists via `LayoutStore`'s `Settings` table + restores before first `Start`; TRAX moved into the game audio bar (left of "Desktop Audio") in the preview overlay. **Health stats (TASK 4 ship step 6, 2026-08-13):** `OnFramePumpHealthUpdated` marshals `FramePump.HealthUpdated` (encoder's parsed bitrate/FPS/dropped/duration — raised on the stderr thread) onto the UI thread into `CurrentHealth` (bottom bar bindings); `ResetHealth(status)` zeroes dropped/duration on go-live/End so stats never linger |
+| `MainViewModel.cs` | Core. `public partial class MainViewModel : ViewModelBase`. **Broadcast/stream state:** go-live (`BeginGoLive` accepts the GoLive dialog → `StreamVisibility` always "Private" → `CreateBroadcastAsync` storing `_currentBroadcastId` for the bind/transition; failure → `StreamStatus.Error` + `_framePump.StartAsync()`), End (`StopStream` — signs out, clears session/token/`_currentBroadcastId`, crash-safe), `StreamStatus`/`StreamVisibility` setters, `IsOffline`/`IsLive`/`IsLivePrivate`. **Health stats:** `CurrentHealth`/`ResetHealth`/`OnFramePumpHealthUpdated` marshal `FramePump.HealthUpdated` (encoder bitrate/FPS/dropped/duration) off the stderr thread onto the UI thread. **Start/End UI:** REC/ON-AIR pills (`RecordPillOn`/`OnAirPillOn`, `CanStartSession`, `ShowPrimaryStartButton`/`ShowEndStreamButton`/`EndButtonText`), REC sign (`RecDotBrush`/`RecTextBrush`/`RecDotOpacity`, pulsing live via `_recDotPulse`), `ShowLogInButton`, `ChooseRecordFolder`/`ResetRecordFolder`/`_recordFolder`. **Resolution dropdown:** `QualityOptions`/`SelectedQuality`/`ResolutionHelp`/`ApplyStreamQuality` computes the output rect over the 1920×1080 master (`OutputRectX/Y/W/H`, `DimRects`, `IsOutputCropped`, `ResolutionBadgeText`). **Source commands + element rows:** scenes are pure selection rows; sources carry edit/visibility/trash (`EditElementCommand` sets `element.IsEditing`, `ToggleElementVisibilityCommand` flips `IsVisible`, `RemoveSourceCommand`; Backdrop trash slot reserved via `HiddenBoolToVisibilityConverter` — TASK 13); shared `NextSourceName` (Image, Image2, Image3… no-space increment from actual names — used by `AddSource` + `AddReusedImage`), `AddImageCommand`. **Overlays/lifecycle:** fields + ctor wiring (all command assignments, mixer/camera/chat/hotkey/subscription hookups), `Shutdown`/`SaveLayoutNow`/`ScheduleSave`/`LoadLayout`, `SetPanel`/`ShowOverlay`/`ShowLicenses`, `CreateBrush` + shared `BarOnBrush`/`BarOffBrush` (referenced by Audio/Trax partials), `OpenUrl`/`ComposeEmail`/`SubmitBug`/`SubmitFeature`, window-geometry pass-through, `SelectedElement`/`OnSelectedElementPropertyChanged`, `ShowPreviewPlaceholder`/`ShowChatInactiveMessage`, `BrandFlash*`, WebView2 hosting (`InitWebView2`/`RegisterLoadedWebSources`/`OnWebView2PreviewBitmapChanged`). |
+| `MainViewModel.Account.cs` | Session + connected account: `LoadSavedSessionAsync` (DPAPI restore at startup), `SignInAsync`/`ChangeAccountAsync`/`SignOutYouTubeAsync`, `IsConnected`/`AccountAvatarUrl`/`AccountDisplayName`/`AccountStatusLightToolTip` via `SyncConnectedAccount` (cleared on End). |
+| `MainViewModel.License.cs` | Polar license (freemium): `LicenseKey`/`LicenseMessage`/`IsLicenseValidating`/`IsLicenseEntryOpen`/`IsHubVisible`/`IsPremium`, `OfflineGracePeriod`, `ValidateLicenseAtStartupAsync`/`ActivateLicenseAsync`. |
+| `MainViewModel.Scenes.cs` | Five-scene catalog: `Scenes` + `StagedScene`/`LiveScene`/`IsEditMode`, `AddScene` (canonical names only), `MissingScenes`/`ShowAddScene`/`AddSceneCommand`; scene wiring `OnScenesChanged`/`WireScene`/`UnwireScene`; transitions `SelectedTransitionType`/`TransitionDurationMs`/`TransitionToScene`/`ApplyTransitionToLive`; global hotkeys `HandleHotkey`/`OpenHotkeyConfig`/`RefreshHotkeyLabels`; scene-gating hints (`CanChangeBackground`). |
+| `MainViewModel.Background.cs` | Screen backdrop (Live-only by policy): `ScreenCaptureManager` + `ScreenCaptureSourceFactory` (WinRT GraphicsCapture); background model `EnsureBackground`/`CreateBackground`/`NormalizeBackgrounds`/`HealBackgrounds`/`StampDefaultBackgroundAsset`; screen capture `ReacquireScreenCaptures`/`ResolveAutoCaptureKey`/`RedesignateBackgroundAsync`/`ClearBackgroundCaptureAsync`/`SetBackgroundCapture`/`OnScreenPreviewBitmapChanged`/`OnFullscreenMonitorChanged`/`RefreshBackgroundAutoCapture`/`BackgroundShowDesktop` (keys on full-screen-game monitor or primary display via `Win32FullScreenDetector`); display picker `Displays`/`SetBackgroundDisplayCommand`; preview fns `ActiveBackgroundImage`/`BackgroundImage`/`UpdateActiveBackground`/`UpdateBackgroundImage`/`RefreshSnapshotsAsync`/`LoadBackgroundImage`/`CreatePlaceholderSnapshot`; `ChangeCaptureCommand`/`RefreshCaptureCommand`/`ToggleShowDesktopCommand`/`BrowseBackgroundCommand`. `ShowPreviewPlaceholder` stays in core. |
+| `MainViewModel.Webcam.cs` | Webcam identity: `_webcam`/`ReacquireWebcam`/`SwapWebcamIdentityAsync`; per-scene configs `AddWebcamToStagedSceneAsync`/`ShowWebcamInStagedScene`/`ChangeWebcamAsync` (Windows picker always opens); capture `OnCameraPreviewBitmapChanged`/`OnCameraFailed`; gating `CanAddWebcam`/`CanChangeWebcam`/`CanShowWebcamInStagedScene`/`IsWebcamSelected`/`WebcamError`; size safeguards `WebcamMax*`/`ClampWebcamToBounds`/`HealLegacySquareRect`; `AddWebcamCommand`/`ChangeWebcamCommand`/`ShowWebcamCommand`. |
+| `MainViewModel.Audio.cs` | **Mic bar:** read-only realtime meter `AudioLevel`/`MeterFillWidth`/`MeterBrush` (fill = `Math.Min(1, AudioLevelMeter.ToDisplay((float)AudioLevel) * MicVolume)`, green→yellow→red at 60%/80% zones), `MicVolume` (0.8 default; drives `MicMuted` ⇔ volume 0; unmute flash `BeginVolumeFlash`/`EndVolumeFlash`/`CancelVolumeFlash`), `MicSourceName` (left-justified inside the bar, persists via `LayoutStore.Settings`), `ToggleMicMute`/`PickMicrophone` (`MicPickerDialog`; picked device swaps the live source via `_audioMixer.RestartMic()`, loopback keeps running), status dot `MicStatus`/`MicStatusBrush`/`MicStatusToolTip` (`Models.MicStatus`: green Connected / yellow Problem / red no-device), capture starts once at startup (`StartMicCaptureAsync`) — NOT go-live. **Game audio bar:** overlaid bottom-center, always visible (show/hide gating removed 2026-08-15); `GameAudioLevel` (loopback via `OnLoopbackLevelChanged`), `GameAudioVolume`/`GameMuted`/`GameMuteText`/`ToggleGameMute`/`PushSystemVolume`, mirror volume flash `SetGameVolumeAdjusting`/`BeginGameVolumeFlash`/`EndGameVolumeFlash`/`CancelGameVolumeFlash`. Mixer handlers `OnMicLevelChanged`/`OnLoopbackLevelChanged`/`OnMicConnected`/`OnMicFailed`. |
+| `MainViewModel.Trax.cs` | Background music (TASK 9, rides the loopback into the stream): `_musicPlayer` (`MusicPlayer`), `IsMusicPlaying`/`TraxError`/`TraxDotBrush`/`TraxToolTip`, interaction `TraxLeftClick`/`TraxRightClick`/`PickTraxTrack`, playback `LoadTrax`/`PlayTrax`/`PauseTrax`/`OnTraxPlaybackEnded`/`RefreshTraxUi`, `UpdateTraxLocalGain` (mirrors the game bar's mute onto `MusicPlayer.LocalGain` so the track ducks/restores with desktop audio). |
+| `MainViewModel.Socials.cs` | Social bar (global resource, overlaid on every scene): `_socials`/`_socialBarFrame`/`_socialValidator` (`ISocialValidator`/`HttpSocialValidator`), `HasSocials`/`SocialBarVisible`/`SocialBarDotBrush`/`SocialBarGlowBrush`/`Socials`/`SocialBarTop`, `OpenSocialDialogCommand`/`OpenSocialDialog` (spawns `SocialsDialogViewModel` + window; saved copy replaces `_socials`), `NotifySocialsChanged`/`RenderSocialBarFrame` (`SocialBarRenderer` → immutable `_socialBarFrame` the frame pump reads), fediverse heal `HealFediverseSoftwareAsync` (static, test-facing) + `HealFediverseSoftwareInBackground`, bar edge toggle `SetSocialBarPosition`/`ToggleSocialBarPosition`. `BarOnBrush`/`BarOffBrush`/`CreateBrush` stay in core (shared tokens). |
| `GoLiveViewModel.cs` | Start Stream dialog: **account row** (saved channel shown with Change Account, or Sign in to YouTube; Start gated on `IsSignedIn`/`IsBusy`) + title/description + **locked Visibility (always "Private" — ship step 7, no dropdown)**, start/cancel requests |
| **About hub (TASK 11, in `MainViewModel`)** | the About overlay is the in-app creator hub: `ChannelUrl`/`MastodonUrl`/`CoffeeUrl` (public consts, live) + `PremiumUrl` (tabled billing seam — TASK 12 → the greyed "Unlock Premium" button lights up when filled); `OpenLicensesCommand` → `ShowLicenses()` loads the shipped `THIRD-PARTY-NOTICES.txt` into `LicensesText` and sets `IsLicensesOpen` (in-app scrolling panel, never the OS viewer); `BackToAbout_Click` in `MainWindow.xaml.cs` flips it off. |
| `ReuseImageViewModel.cs` | Add Image dialog: candidate list (`ReuseImageCandidate`), reuse/new/cancel |
@@ -21,69 +33,21 @@ for what the ViewModels drive; base class in [`Helpers/ViewModelBase.cs`](../Hel
change, one commit per partial (each commit a roll-back point). **Commit naming:**
titles carry the series id (`Commit N/12`), tags are `refactor-commit-N` — e.g.
`git show refactor-commit-1`. Every partial lands with a `ViewModels/index.md`
-update in the SAME commit.
+update in the SAME commit. Each partial's docs live in its table row above; this
+tracker is just the code↔commit map.
Progress (Commit N/12 · tag):
-- **Commit 1/12 · `refactor-commit-1`: Account landed** → `MainViewModel.Account.cs`
- (session restore `LoadSavedSessionAsync`, sign-in/out/change account,
- `IsConnected`/`AccountAvatarUrl`/`AccountDisplayName`/`AccountStatusLightToolTip`).
-- **Commit 2/12 · `refactor-commit-2`: License landed** → `MainViewModel.License.cs`
- (Polar license state: `LicenseKey`/`LicenseMessage`/`IsLicenseValidating`/
- `IsLicenseEntryOpen`/`IsHubVisible`/`IsPremium`, `OfflineGracePeriod`,
- `ValidateLicenseAtStartupAsync`, `ActivateLicenseAsync`).
-- **Commit 3/12 · `refactor-commit-3`: Scenes landed** → `MainViewModel.Scenes.cs`
- (`Scenes` + `StagedScene`/`LiveScene`/`IsEditMode`, transitions
- `SelectedTransitionType`/`TransitionDurationMs`/`TransitionToScene`/
- `ApplyTransitionToLive`, global hotkeys `HandleHotkey`/`OpenHotkeyConfig`/
- `RefreshHotkeyLabels`, scene-gating hints, `MissingScenes`/`ShowAddScene`/
- `CanChangeBackground`, scene wiring `OnScenesChanged`/`WireScene`/`UnwireScene`,
- `AddScene`).
-- **Commit 4/12 · `refactor-commit-4`: Background landed** → `MainViewModel.Background.cs`
- (background model `EnsureBackground`/`CreateBackground`/`NormalizeBackgrounds`/
- `HealBackgrounds`/`StampDefaultBackgroundAsset`, display picker `Displays` +
- `SetBackgroundCapture`, screen capture `ReacquireScreenCaptures`/
- `ResolveAutoCaptureKey`/`RedesignateBackgroundAsync`/`ClearBackgroundCaptureAsync`/
- `OnScreenPreviewBitmapChanged`/`OnFullscreenMonitorChanged`/
- `RefreshBackgroundAutoCapture`/`BackgroundShowDesktop`, preview fns
- `ActiveBackgroundImage`/`BackgroundImage`/`UpdateActiveBackground`/
- `UpdateBackgroundImage`/`RefreshSnapshotsAsync`/`LoadBackgroundImage`,
- `ChangeCaptureCommand`/`RefreshCaptureCommand`/`SetBackgroundDisplayCommand`/
- `ToggleShowDesktopCommand`/`BrowseBackgroundCommand`). `ShowPreviewPlaceholder`
- stays in core.
-- **Commit 5/12 · `refactor-commit-5`: Webcam landed** → `MainViewModel.Webcam.cs`
- (webcam identity `_webcam`/`ReacquireWebcam`/`SwapWebcamIdentityAsync`, per-scene
- configs `AddWebcamToStagedSceneAsync`/`ShowWebcamInStagedScene`, capture lifetime
- `OnCameraPreviewBitmapChanged`/`OnCameraFailed`, gating `CanAddWebcam`/
- `CanChangeWebcam`/`CanShowWebcamInStagedScene`/`IsWebcamSelected`/`WebcamError`,
- size safeguard `WebcamMax*`/`ClampWebcamToBounds`/`HealLegacySquareRect`,
- `AddWebcamCommand`/`ChangeWebcamCommand`/`ShowWebcamCommand`).
-- **Commit 6/12 · `refactor-commit-6`: Audio landed** → `MainViewModel.Audio.cs`
- (mic bar `AudioLevel`/`MeterFillWidth`/`MeterBrush`/`MicVolume`/`MicMuted`/
- `MicMuteText`/`MicSourceName`/`ToggleMicMute`/`PickMicrophone`, volume flash
- `SetVolumeAdjusting`/`BeginVolumeFlash`/`EndVolumeFlash`/`CancelVolumeFlash`,
- mic status dot `MicStatus`/`MicStatusBrush`/`MicStatusToolTip`/`MicProblemBrush`,
- game bar `GameAudioLevel`/`GameMeterLevel`/`GameMeterFillWidth`/`GameMeterBrush`/
- `SetGameVolumeAdjusting`/`GameAudioVolume`/`GameMuted`/`GameMuteText`/
- `ToggleGameMute`, `PushSystemVolume`, mixer handlers `OnMicLevelChanged`/
- `OnLoopbackLevelChanged`/`OnMicConnected`/`OnMicFailed`, `StartMicCaptureAsync`,
- audio fields + `ToggleMicMuteCommand`/`ToggleGameMuteCommand`/`OpenMicPickerCommand`).
- TRAX stays in core (next commit).
-- **Commit 7/12 · `refactor-commit-7`: Trax landed** → `MainViewModel.Trax.cs`
- (`_musicPlayer`/`_music`/`_musicPlaying` + `TraxIdleBrush`, `IsMusicPlaying`/
- `TraxError`/`TraxDotBrush`/`TraxToolTip`, `TraxLeftClick`/`TraxRightClick`/
- `PickTraxTrack`/`LoadTrax`/`PlayTrax`/`PauseTrax`/`OnTraxPlaybackEnded`/
- `RefreshTraxUi`/`UpdateTraxLocalGain`). Core keeps the ctor hookup
- (`_musicPlayer.PlaybackEnded += ...`), the `LoadLayout`/`Shutdown`/
- `SaveLayoutNow` call sites, and the Scenes partial's hotkey dispatch into `TraxLeftClick`.
-
-- Core file keeps: fields, ctor, `Shutdown`/`SaveLayoutNow`/`ScheduleSave`/`LoadLayout`,
- `SetPanel`/`ShowOverlay`/`ShowLicenses`, `CreateBrush`, `OpenUrl`/`ComposeEmail`,
- `SubmitBug`/`SubmitFeature`, window-geometry pass-through.
-- Partials: `Account` ✔ (Commit 1), `License` ✔ (Commit 2), `Scenes` ✔ (Commit 3), `Background` ✔ (Commit 4),
- `Webcam` ✔ (Commit 5), `Audio` ✔ (Commit 6), `Trax` ✔ (Commit 7),
- `Socials` (social bar + dialog), `Streaming` (go-live/record + health + quality tiers + media source pipeline),
- `Chat` (YouTube chat + mock preview + chatbox render), `Overlays` (settings/bug/feature/about
- panels + license entry).
+- [x] **Commit 1/12 · `refactor-commit-1`** — **Account** → `MainViewModel.Account.cs`
+- [x] **Commit 2/12 · `refactor-commit-2`** — **License** → `MainViewModel.License.cs`
+- [x] **Commit 3/12 · `refactor-commit-3`** — **Scenes** → `MainViewModel.Scenes.cs`
+- [x] **Commit 4/12 · `refactor-commit-4`** — **Background** → `MainViewModel.Background.cs`
+- [x] **Commit 5/12 · `refactor-commit-5`** — **Webcam** → `MainViewModel.Webcam.cs`
+- [x] **Commit 6/12 · `refactor-commit-6`** — **Audio** → `MainViewModel.Audio.cs`
+- [x] **Commit 7/12 · `refactor-commit-7`** — **Trax** → `MainViewModel.Trax.cs`
+- [x] **Commit 8/12 · `refactor-commit-8`** — **Socials** → `MainViewModel.Socials.cs`
+- [ ] **Commit 9/12** — **Streaming** (go-live/record + health + quality tiers + media source pipeline)
+- [ ] **Commit 10/12** — **Chat** (YouTube chat + mock preview + chatbox render)
+- [ ] **Commit 11/12** — **Overlays** (settings/bug/feature/about panels + license entry)
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.
\ No newline at end of file