Files
LlamaCasty/ViewModels/index.md
T

14 KiB
Raw Permalink Blame History

ViewModels — index

MVVM layer. See schema.md for the memory-map conventions.

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
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
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
SocialsDialogViewModel.cs Social Media Site Promotion dialog (six fixed slots, SocialSlotViewModel): row 0 = YouTube sign-in gate (sign-in provider fills the channel, delete = sign-out); rows 1-5 validated entries / empty / freemium locks 2-5 (isPremium seam); DetectService → ISocialValidator on confirm (input snaps to text + real logo on success, red do-not + stays editable on failure); CanSave blocked while any slot edits/validates/errors; BarEnabled switch; commit via CommittedEntries + BuildSocialsConfig() (YouTube first) + SaveRequested/CancelRequested. WPF-free — the window owns the dialogs (delete confirm, sign-in gate) and calls StartEdit/ConfirmEdit/DeleteSlotAsync.

Related: Models/index.md for the data; Services/index.md for what the ViewModels drive; base class in Helpers/ViewModelBase.cs.

MainViewModel partial split (in progress)

MainViewModel.cs is being split into one file per functional area, zero behavior 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.

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).

  • 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 (mic + game bars + TRAX), Trax (music player), 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).

The partial .cs files land next to MainViewModel.cs in this folder as the split proceeds; each partial carries its own usings and re-declares nothing from core.