diff --git a/ViewModels/MainViewModel.Overlays.cs b/ViewModels/MainViewModel.Overlays.cs new file mode 100644 index 0000000..dd2f90a --- /dev/null +++ b/ViewModels/MainViewModel.Overlays.cs @@ -0,0 +1,193 @@ +using System.Diagnostics; +using System.IO; +using System.Windows.Input; +using ytLive.Helpers; + +namespace ytLive.ViewModels; + +public partial class MainViewModel : ViewModelBase +{ + + private bool _isSettingsOpen; + private bool _isBugOpen; + private bool _isFeatureOpen; + private bool _isAboutOpen; + private bool _isLicensesOpen; + private string _overlayTitle = string.Empty; + private string _bugReportText = string.Empty; + private string _bugReportEmail = string.Empty; + private string _featureRequestText = string.Empty; + private string _featureRequestEmail = string.Empty; + + private const string SupportEmail = "gramps@llamachile.shop"; + // The creator-hub link targets (About overlay) — public so the hub's + // integration test can assert they point at real destinations. + public const string ChannelUrl = "https://youtube.com/@llamachileshop"; + public const string MastodonUrl = "https://mastodon.llamachile.tube/@gramps"; + public const string PremiumUrl = "https://buy.polar.sh/polar_cl_ueMy8AyAClO0o0yo19IroZwKTbD587gzUsjEW3bFapw"; + public const string CoffeeUrl = "https://buymeacoffee.com/llamachiley"; + public static string AppVersionLabel => $"Version {typeof(MainViewModel).Assembly.GetName().Version?.ToString(3)}"; + + /// The About hub's Unlock Premium button lights up only once the + /// billing URL is set (Polar — polar.sh, pending TASK 10 implementation). + public bool IsPremiumAvailable => PremiumUrl.Length > 0; + + /// Full in-app licensing/legal text, loaded on demand from the + /// shipped THIRD-PARTY-NOTICES.txt (never opens the OS viewer). + public string LicensesText { get; private set; } = string.Empty; + + // Overlay panels + public bool IsSettingsOpen + { + get => _isSettingsOpen; + private set => SetPanel(ref _isSettingsOpen, value); + } + + public bool IsBugOpen + { + get => _isBugOpen; + private set => SetPanel(ref _isBugOpen, value); + } + + public bool IsFeatureOpen + { + get => _isFeatureOpen; + private set => SetPanel(ref _isFeatureOpen, value); + } + + public bool IsAboutOpen + { + get => _isAboutOpen; + private set => SetPanel(ref _isAboutOpen, value); + } + + /// About hub sub-view: the full licensing/legal texts panel + /// (in-app scrolling — no Notepad, no browser tab). + public bool IsLicensesOpen + { + get => _isLicensesOpen; + set + { + if (SetProperty(ref _isLicensesOpen, value)) + OnPropertyChanged(nameof(IsHubVisible)); + } + } + + public bool IsAnyOverlayOpen => IsSettingsOpen || IsBugOpen || IsFeatureOpen || IsAboutOpen; + + public string OverlayTitle + { + get => _overlayTitle; + private set => SetProperty(ref _overlayTitle, value); + } + + public string BugReportText + { + get => _bugReportText; + set => SetProperty(ref _bugReportText, value); + } + + public string BugReportEmail + { + get => _bugReportEmail; + set => SetProperty(ref _bugReportEmail, value); + } + + public string FeatureRequestText + { + get => _featureRequestText; + set => SetProperty(ref _featureRequestText, value); + } + + public string FeatureRequestEmail + { + get => _featureRequestEmail; + set => SetProperty(ref _featureRequestEmail, value); + } + + public ICommand OpenSettingsCommand { get; } + public ICommand OpenBugCommand { get; } + public ICommand OpenFeatureCommand { get; } + public ICommand OpenAboutCommand { get; } + public ICommand CloseOverlayCommand { get; } + public ICommand SubmitBugCommand { get; } + public ICommand SubmitFeatureCommand { get; } + public ICommand OpenChannelCommand { get; } + public ICommand OpenMastodonCommand { get; } + public ICommand OpenCoffeeCommand { get; } + public ICommand OpenPremiumCommand { get; } + public ICommand OpenLicensesCommand { get; } + + private void SetPanel(ref bool field, bool value, [System.Runtime.CompilerServices.CallerMemberName] string? propertyName = null) + { + if (SetProperty(ref field, value, propertyName)) + OnPropertyChanged(nameof(IsAnyOverlayOpen)); + } + + private void ShowOverlay(string? panel = null, string title = "") + { + IsSettingsOpen = panel == nameof(IsSettingsOpen); + IsBugOpen = panel == nameof(IsBugOpen); + IsFeatureOpen = panel == nameof(IsFeatureOpen); + IsAboutOpen = panel == nameof(IsAboutOpen); + OverlayTitle = title; + if (panel != nameof(IsAboutOpen)) + IsLicensesOpen = false; + } + + /// Shows the in-app licensing panel, loading the full texts from + /// the shipped THIRD-PARTY-NOTICES.txt on first open (never the OS viewer). + /// Falls back to a "not found" note if the file is missing so the panel + /// never hangs on nothing. + private void ShowLicenses() + { + if (string.IsNullOrEmpty(LicensesText)) + { + try + { + var path = Path.Combine(AppContext.BaseDirectory, "THIRD-PARTY-NOTICES.txt"); + LicensesText = File.Exists(path) + ? File.ReadAllText(path) + : "THIRD-PARTY-NOTICES.txt not found next to the executable."; + } + catch (Exception ex) + { + LicensesText = $"Could not read THIRD-PARTY-NOTICES.txt: {ex.Message}"; + } + } + IsLicensesOpen = true; + } + + private void SubmitBug() + { + var body = string.Join(Environment.NewLine, + BugReportText.Trim(), + string.Empty, + $"LlamaCasty {AppVersionLabel}", + string.IsNullOrWhiteSpace(BugReportEmail) ? string.Empty : $"Reply-to: {BugReportEmail.Trim()}"); + ComposeEmail("[LlamaCasty Bug Report]", body); + ShowOverlay(); + } + + private void SubmitFeature() + { + var body = string.Join(Environment.NewLine, + FeatureRequestText.Trim(), + string.Empty, + $"LlamaCasty {AppVersionLabel}", + string.IsNullOrWhiteSpace(FeatureRequestEmail) ? string.Empty : $"Reply-to: {FeatureRequestEmail.Trim()}"); + ComposeEmail("[LlamaCasty Feature Request]", body); + ShowOverlay(); + } + + private static void ComposeEmail(string subject, string body) + { + var uri = $"mailto:{SupportEmail}?subject={Uri.EscapeDataString(subject)}&body={Uri.EscapeDataString(body)}"; + OpenUrl(uri); + } + + private static void OpenUrl(string url) + { + Process.Start(new ProcessStartInfo(url) { UseShellExecute = true }); + } +} diff --git a/ViewModels/MainViewModel.cs b/ViewModels/MainViewModel.cs index db98863..da01221 100644 --- a/ViewModels/MainViewModel.cs +++ b/ViewModels/MainViewModel.cs @@ -29,17 +29,6 @@ public partial class MainViewModel : ViewModelBase private SceneElement? _selectedElement; - private bool _isSettingsOpen; - private bool _isBugOpen; - private bool _isFeatureOpen; - private bool _isAboutOpen; - private bool _isLicensesOpen; - private string _overlayTitle = string.Empty; - private string _bugReportText = string.Empty; - private string _bugReportEmail = string.Empty; - private string _featureRequestText = string.Empty; - private string _featureRequestEmail = string.Empty; - private LayoutStore _layoutStore; private string? _activeLayoutPath; private DispatcherTimer? _saveDebounce; @@ -65,23 +54,6 @@ public partial class MainViewModel : ViewModelBase private bool _brandFlashEnabled = true; private bool _brandFlashActive; - private const string SupportEmail = "gramps@llamachile.shop"; - // The creator-hub link targets (About overlay) — public so the hub's - // integration test can assert they point at real destinations. - public const string ChannelUrl = "https://youtube.com/@llamachileshop"; - public const string MastodonUrl = "https://mastodon.llamachile.tube/@gramps"; - public const string PremiumUrl = "https://buy.polar.sh/polar_cl_ueMy8AyAClO0o0yo19IroZwKTbD587gzUsjEW3bFapw"; - public const string CoffeeUrl = "https://buymeacoffee.com/llamachiley"; - public static string AppVersionLabel => $"Version {typeof(MainViewModel).Assembly.GetName().Version?.ToString(3)}"; - - /// The About hub's Unlock Premium button lights up only once the - /// billing URL is set (Polar — polar.sh, pending TASK 10 implementation). - public bool IsPremiumAvailable => PremiumUrl.Length > 0; - - /// Full in-app licensing/legal text, loaded on demand from the - /// shipped THIRD-PARTY-NOTICES.txt (never opens the OS viewer). - public string LicensesText { get; private set; } = string.Empty; - public SceneElement? SelectedElement { get => _selectedElement; @@ -148,51 +120,6 @@ public partial class MainViewModel : ViewModelBase private set => SetProperty(ref _brandFlashActive, value); } - // Overlay panels - public bool IsSettingsOpen - { - get => _isSettingsOpen; - private set => SetPanel(ref _isSettingsOpen, value); - } - - public bool IsBugOpen - { - get => _isBugOpen; - private set => SetPanel(ref _isBugOpen, value); - } - - public bool IsFeatureOpen - { - get => _isFeatureOpen; - private set => SetPanel(ref _isFeatureOpen, value); - } - - public bool IsAboutOpen - { - get => _isAboutOpen; - private set => SetPanel(ref _isAboutOpen, value); - } - - /// About hub sub-view: the full licensing/legal texts panel - /// (in-app scrolling — no Notepad, no browser tab). - public bool IsLicensesOpen - { - get => _isLicensesOpen; - set - { - if (SetProperty(ref _isLicensesOpen, value)) - OnPropertyChanged(nameof(IsHubVisible)); - } - } - - public bool IsAnyOverlayOpen => IsSettingsOpen || IsBugOpen || IsFeatureOpen || IsAboutOpen; - - public string OverlayTitle - { - get => _overlayTitle; - private set => SetProperty(ref _overlayTitle, value); - } - // Window geometry pass-through (code-behind owns the WPF specifics). public (double Left, double Top, double Width, double Height, string State)? RestoreWindowState() => _layoutStore.LoadWindowState(); @@ -200,30 +127,6 @@ public partial class MainViewModel : ViewModelBase public void PersistWindowState(double left, double top, double width, double height, string state) => _layoutStore.SaveWindowState(left, top, width, height, state); - public string BugReportText - { - get => _bugReportText; - set => SetProperty(ref _bugReportText, value); - } - - public string BugReportEmail - { - get => _bugReportEmail; - set => SetProperty(ref _bugReportEmail, value); - } - - public string FeatureRequestText - { - get => _featureRequestText; - set => SetProperty(ref _featureRequestText, value); - } - - public string FeatureRequestEmail - { - get => _featureRequestEmail; - set => SetProperty(ref _featureRequestEmail, value); - } - // Commands public ICommand AddSceneCommand { get; } public ICommand AddSourceCommand { get; } @@ -231,18 +134,6 @@ public partial class MainViewModel : ViewModelBase public ICommand RemoveSourceCommand { get; } public ICommand EditElementCommand { get; } public ICommand ToggleElementVisibilityCommand { get; } - public ICommand OpenSettingsCommand { get; } - public ICommand OpenBugCommand { get; } - public ICommand OpenFeatureCommand { get; } - public ICommand OpenAboutCommand { get; } - public ICommand CloseOverlayCommand { get; } - public ICommand SubmitBugCommand { get; } - public ICommand SubmitFeatureCommand { get; } - public ICommand OpenChannelCommand { get; } - public ICommand OpenMastodonCommand { get; } - public ICommand OpenCoffeeCommand { get; } - public ICommand OpenPremiumCommand { get; } - public ICommand OpenLicensesCommand { get; } public ICommand ActivateLicenseCommand { get; } public ICommand OpenLicenseEntryCommand { get; } public ICommand OpenHotkeyConfigCommand { get; } @@ -807,79 +698,6 @@ public partial class MainViewModel : ViewModelBase UpdateActiveBackground(); } - private void SetPanel(ref bool field, bool value, [System.Runtime.CompilerServices.CallerMemberName] string? propertyName = null) - { - if (SetProperty(ref field, value, propertyName)) - OnPropertyChanged(nameof(IsAnyOverlayOpen)); - } - - private void ShowOverlay(string? panel = null, string title = "") - { - IsSettingsOpen = panel == nameof(IsSettingsOpen); - IsBugOpen = panel == nameof(IsBugOpen); - IsFeatureOpen = panel == nameof(IsFeatureOpen); - IsAboutOpen = panel == nameof(IsAboutOpen); - OverlayTitle = title; - if (panel != nameof(IsAboutOpen)) - IsLicensesOpen = false; - } - - /// Shows the in-app licensing panel, loading the full texts from - /// the shipped THIRD-PARTY-NOTICES.txt on first open (never the OS viewer). - /// Falls back to a "not found" note if the file is missing so the panel - /// never hangs on nothing. - private void ShowLicenses() - { - if (string.IsNullOrEmpty(LicensesText)) - { - try - { - var path = Path.Combine(AppContext.BaseDirectory, "THIRD-PARTY-NOTICES.txt"); - LicensesText = File.Exists(path) - ? File.ReadAllText(path) - : "THIRD-PARTY-NOTICES.txt not found next to the executable."; - } - catch (Exception ex) - { - LicensesText = $"Could not read THIRD-PARTY-NOTICES.txt: {ex.Message}"; - } - } - IsLicensesOpen = true; - } - - private void SubmitBug() - { - var body = string.Join(Environment.NewLine, - BugReportText.Trim(), - string.Empty, - $"LlamaCasty {AppVersionLabel}", - string.IsNullOrWhiteSpace(BugReportEmail) ? string.Empty : $"Reply-to: {BugReportEmail.Trim()}"); - ComposeEmail("[LlamaCasty Bug Report]", body); - ShowOverlay(); - } - - private void SubmitFeature() - { - var body = string.Join(Environment.NewLine, - FeatureRequestText.Trim(), - string.Empty, - $"LlamaCasty {AppVersionLabel}", - string.IsNullOrWhiteSpace(FeatureRequestEmail) ? string.Empty : $"Reply-to: {FeatureRequestEmail.Trim()}"); - ComposeEmail("[LlamaCasty Feature Request]", body); - ShowOverlay(); - } - - private static void ComposeEmail(string subject, string body) - { - var uri = $"mailto:{SupportEmail}?subject={Uri.EscapeDataString(subject)}&body={Uri.EscapeDataString(body)}"; - OpenUrl(uri); - } - - private static void OpenUrl(string url) - { - Process.Start(new ProcessStartInfo(url) { UseShellExecute = true }); - } - // Scene-element → latest frame, for the live compositor. The map mirrors the // preview: webcam by DeviceId, live captures (the background) by CaptureKey, // images/background by AssetId. A null frame leaves the element transparent. diff --git a/ViewModels/index.md b/ViewModels/index.md index 6b75b56..7ef9d8a 100644 --- a/ViewModels/index.md +++ b/ViewModels/index.md @@ -8,7 +8,7 @@ areas' docs in their own rows keeps this index small to read. | File | Purpose | |------|---------| -| `MainViewModel.cs` | Core. `public partial class MainViewModel : ViewModelBase`. **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/Socials partials), `OpenUrl`/`ComposeEmail`/`SubmitBug`/`SubmitFeature`, window-geometry pass-through, `SelectedElement`/`OnSelectedElementPropertyChanged`, `ShowPreviewPlaceholder`, `BrandFlash*`, WebView2 hosting (`InitWebView2`/`RegisterLoadedWebSources`/`OnWebView2PreviewBitmapChanged`), frame-pump wiring (construction + `_rtmpUrlProvider` seam + `Dispose`, plus `ResolveOutputFrame` → `RenderChatBox` for chat layers) — the pump's stream-state consumers live in the Streaming partial. | +| `MainViewModel.cs` | Core. `public partial class MainViewModel : ViewModelBase`. **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`. **Lifecycle/ctor wiring:** fields + ctor wiring (all command assignments, mixer/camera/chat/hotkey/subscription hookups), `Shutdown`/`SaveLayoutNow`/`ScheduleSave`/`LoadLayout`, `CreateBrush` + shared `BarOnBrush`/`BarOffBrush` (referenced by Audio/Trax/Socials partials), window-geometry pass-through, `SelectedElement`/`OnSelectedElementPropertyChanged`, `ShowPreviewPlaceholder`, `BrandFlash*`, WebView2 hosting (`InitWebView2`/`RegisterLoadedWebSources`/`OnWebView2PreviewBitmapChanged`), frame-pump wiring (construction + `_rtmpUrlProvider` seam + `Dispose`, plus `ResolveOutputFrame` → `RenderChatBox` for chat layers) — the pump's stream-state consumers live in the Streaming partial. | | `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`). | @@ -18,9 +18,10 @@ areas' docs in their own rows keeps this index small to read. | `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). | | `MainViewModel.Streaming.cs` | Go-live/record + health + quality tiers + session UI. **Stream state:** `StreamStatus`/`StreamVisibility` setters + `_streamStatus` field, `IsOffline`/`IsLive`/`IsLivePrivate`. **Go-live flow:** `StartSession` (routes by pill state) → `BeginRecordOnly` (signed-out local recording) or `BeginGoLive` (GoLive dialog → captures title/description/visibility + `BroadcastForm.CaptureGoLive` → `PrepareAndStartLiveAsync`: reusable stream → `CreateBroadcast` (stores `_currentBroadcastId`) → empty-metadata warn → chat poll start (liveChatId) → `_framePump.StartAsync()` + health poll; failures → `StreamStatus.Error`, never a crash); `StartRecordFile` (auto-named `ty-…-0000.mp4`) + `FinalizeRecordingAsync` (rename modal, `UniquePath`); `StopStream` (posts Offline, stops chat/audio pipe/pump, clears `_currentBroadcastId`; session/token deliberately NOT cleared — sign-out stays explicit). **Health:** `CurrentHealth`/`ResetHealth`/`ApplyHealth`/`OnFramePumpHealthUpdated` (marshal `FramePump.HealthUpdated` off the stderr thread), `PollHealthAsync`/`OnHealthPollTick`/`ApplyHealthIssue` (report-by-exception banner `HealthIssueBanner`/`HealthIssueBackground` via `StreamHealthReporter`). **Session timer:** `_liveTimer` (ctor-wired) `UpdateSessionTimer`/`OnLiveTimerTick`/`LiveElapsedText`/`_recDotPulse`. **Start/End UI:** REC/ON-AIR pills (`RecordPillOn`/`OnAirPillOn`/`CanToggleOnAir`), `ShowPrimaryStartButton`/`CanStartSession`/`ShowEndStreamButton`/`EndButtonText`/`ShowLogInButton`, REC sign `RecDotBrush`/`RecTextBrush`/`RecDotOpacity`/`OnAirBrush`, live window tokens `WindowTitle`/`TopBarBackground`/`PreviewGlow*` via `UpdateLiveVisuals`; `ChooseRecordFolder`/`ResetRecordFolder`/`_recordFolder`/`RecordFolderDisplay`/`DefaultRecordFolder`. **Resolution dropdown:** `QualityOptions`/`SelectedQuality`/`ResolutionHelp`/`ApplyStreamQuality` + `OutputRect*`/`DimRects`/`IsOutputCropped`/`ResolutionBadgeText` over the 1920×1080 master. **Pipeline bits:** `_framePump`/`_liveTimer`/`_healthPollTimer` fields, `BuildEncoderOptions`. Commands `StartStreamCommand`/`EndStreamCommand`/`ChooseRecordFolderCommand`/`ResetRecordFolderCommand`. Broadcast pull-out: `CurrentBroadcastId`/`BroadcastForm`. | +| `MainViewModel.Overlays.cs` | Settings/bug/feature/about panels + creator-hub links: panel state `IsSettingsOpen`/`IsBugOpen`/`IsFeatureOpen`/`IsAboutOpen` (via `SetPanel` → `IsAnyOverlayOpen`), `IsLicensesOpen` (flips `IsHubVisible`), `OverlayTitle`, `ShowOverlay` (one-panel exclusivity) + `ShowLicenses` (loads shipped `THIRD-PARTY-NOTICES.txt` into `LicensesText` on first open, never the OS viewer); bug/feature report `BugReportText`/`BugReportEmail`/`FeatureRequestText`/`FeatureRequestEmail` + `SubmitBug`/`SubmitFeature`/`ComposeEmail`/`OpenUrl`; About hub consts `ChannelUrl`/`MastodonUrl`/`CoffeeUrl` (public, integration-test live) + `PremiumUrl` (tabled billing seam — TASK 12 → greyed "Unlock Premium" lights up when filled) + `IsPremiumAvailable` + `SupportEmail` + `AppVersionLabel`; commands `OpenSettingsCommand`/`OpenBugCommand`/`OpenFeatureCommand`/`OpenAboutCommand`/`CloseOverlayCommand`/`SubmitBugCommand`/`SubmitFeatureCommand`/`OpenChannelCommand`/`OpenMastodonCommand`/`OpenCoffeeCommand`/`OpenPremiumCommand`/`OpenLicensesCommand`. Ctor command assignments stay in core. | | `MainViewModel.Chat.cs` | Chat overlay + preview pipeline: `ChatMessages` (500-message cap; clear-on-fade), `OnChatMessageReceived` (marshal `_youtubeChat.MessageReceived` onto the dispatcher, switch previews mock→real), fade `_chatFadeTimer`/`ResetChatFadeTimer` (shortest `ChatFadeSeconds` across chat layers); live render `RenderChatBox` (called from `ResolveOutputFrame`, stays in core) + preview render `UpdateChatBoxPreview` (bitmap onto `chatBox.VideoImageSource`); mock preview `MockChatMessages`/`StartMockChatPreview`/`StopMockChatPreview`/`RunMockChatPreviewAsync`/`_mockChatCts`; `UpdateActiveChatBoxPreviews` (wires Chat* property-change subscriptions + first render); `CanAddYouTubeChat` (one-per-layout gate), `ShowChatInactiveMessage` (`!IsLive`). `_chatBoxRenderer` field. Ctor wiring (`MessageReceived` hookup, `new ChatBoxRenderer()`, `LoadLayout` preview start, `_youtubeChat.Dispose`) stays in core. | | `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. | +| **About hub (TASK 11, in `MainViewModel.Overlays.cs`)** | 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 | | `CameraPickerViewModel.cs` | Add Webcam dialog: async camera list (loading / has / none states), `UseRequested(CameraDeviceInfo)`/`CancelRequested` | | `MicPickerViewModel.cs` | Select Microphone dialog (voice source): async mic list (loading / has / none states), `UseRequested(MicrophoneDeviceInfo)`/`CancelRequested` | @@ -49,7 +50,8 @@ Progress (Commit N/12 · tag): - [x] **Commit 8/12 · `refactor-commit-8`** — **Socials** → `MainViewModel.Socials.cs` - [x] **Commit 9/12 · `refactor-commit-9`** — **Streaming** → `MainViewModel.Streaming.cs` - [x] **Commit 10/12 · `refactor-commit-10`** — **Chat** → `MainViewModel.Chat.cs` -- [ ] **Commit 11/12** — **Overlays** (settings/bug/feature/about panels + license entry) +- [x] **Commit 11/12 · `refactor-commit-11`** — **Overlays** → `MainViewModel.Overlays.cs` +- [x] **Phase 1 complete:** core `MainViewModel.cs` split into 10 functional partials (1,890 → 758 lines). Phase 2 (splitting `MainWindow.xaml` into UserControls) not yet started. 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