diff --git a/ViewModels/MainViewModel.Account.cs b/ViewModels/MainViewModel.Account.cs
new file mode 100644
index 0000000..35d2e10
--- /dev/null
+++ b/ViewModels/MainViewModel.Account.cs
@@ -0,0 +1,132 @@
+using ytLive.Helpers;
+using ytLive.Models;
+using ytLive.Services;
+
+namespace ytLive.ViewModels;
+
+public partial class MainViewModel
+{
+ private bool _isConnected;
+ private string _accountAvatarUrl = string.Empty;
+ private string _accountDisplayName = string.Empty;
+
+ public bool IsConnected
+ {
+ get => _isConnected;
+ set
+ {
+ if (SetProperty(ref _isConnected, value))
+ {
+ // ON-AIR requires a sign-in; dropping the session turns the pill
+ // off so the state can never offer a stream without an account.
+ if (!value) OnAirPillOn = false;
+ OnPropertyChanged(nameof(CanToggleOnAir));
+ OnPropertyChanged(nameof(ShowPrimaryStartButton));
+ OnPropertyChanged(nameof(AccountStatusLightToolTip));
+ }
+ }
+ }
+
+ /// The connected YouTube account's avatar + name — shown in the top
+ /// bar so the creator always sees WHICH account is about to go live.
+ public string AccountAvatarUrl
+ {
+ get => _accountAvatarUrl;
+ private set => SetProperty(ref _accountAvatarUrl, value);
+ }
+
+ public string AccountDisplayName
+ {
+ get => _accountDisplayName;
+ private set => SetProperty(ref _accountDisplayName, value);
+ }
+
+ /// Tooltip for the account status light explaining the color.
+ public string AccountStatusLightToolTip =>
+ IsLive ? "Live on YouTube — the top bar is red to show the stream is on the air."
+ : IsConnected ? "You're signed in to YouTube. The light is green; ON-AIR is available."
+ : "Signed out — the light is red and the ON-AIR pill is greyed out. You can still record locally.";
+
+ // Restores the DPAPI-saved OAuth session so sign-in survives restarts.
+ // Best-effort: refresh a near-expiry access token; a session that can no
+ // longer refresh is discarded and the user signs in again.
+ private async Task LoadSavedSessionAsync()
+ {
+ try
+ {
+ var saved = TokenStore.Load();
+ if (saved == null) return;
+
+ _youtubeAuth.SetSession(saved);
+ if (saved.TokenExpiry <= DateTime.UtcNow.AddMinutes(5))
+ {
+ var refreshed = await Task.Run(() => _youtubeAuth.RefreshToken());
+ if (!refreshed)
+ {
+ TokenStore.Clear();
+ AppLog.Write("Saved session could not be refreshed; signed out");
+ _notifications.Info("Signed out of YouTube",
+ "The saved session expired — sign in again when you next go live.");
+ return;
+ }
+ }
+
+ IsConnected = true;
+ SyncConnectedAccount();
+ }
+ catch (Exception ex)
+ {
+ AppLog.Write($"LoadSavedSessionAsync failed: {ex}");
+ }
+ }
+
+ private void SyncConnectedAccount()
+ {
+ var channel = _youtubeAuth.CurrentChannel;
+ AccountAvatarUrl = channel?.ProfileImageUrl ?? string.Empty;
+ AccountDisplayName = channel?.DisplayName ?? string.Empty;
+ }
+
+ // Runs the OAuth flow (browser + loopback callback) on a background thread.
+ // The resulting channel is returned to the caller and persisted via the
+ // auth service's sessionChanged hook.
+ private async Task SignInAsync()
+ {
+ try
+ {
+ var channel = await Task.Run(() => _youtubeAuth.AuthenticateAsync());
+ if (channel == null)
+ {
+ _notifications.Warning("Sign-in was unsuccessful", "Please try again.");
+ return null;
+ }
+
+ IsConnected = true;
+ SyncConnectedAccount();
+ return channel;
+ }
+ catch (Exception ex)
+ {
+ _notifications.Error("Sign-in failed", ex.Message);
+ return null;
+ }
+ }
+
+ /// Signs out of YouTube (the delete-the-YouTube-slot action in the dialog).
+ private async Task SignOutYouTubeAsync()
+ {
+ _youtubeAuth.ClearSession();
+ TokenStore.Clear();
+ IsConnected = false;
+ SyncConnectedAccount();
+ NotifySocialsChanged();
+ AppLog.Write("Socials: signed out of YouTube");
+ await Task.CompletedTask;
+ }
+
+ private async Task ChangeAccountAsync()
+ {
+ await SignOutYouTubeAsync();
+ await SignInAsync();
+ }
+}
\ No newline at end of file
diff --git a/ViewModels/MainViewModel.cs b/ViewModels/MainViewModel.cs
index bb57eae..cd0ba3c 100644
--- a/ViewModels/MainViewModel.cs
+++ b/ViewModels/MainViewModel.cs
@@ -35,9 +35,6 @@ public partial class MainViewModel : ViewModelBase
private Scene? _liveScene;
private SceneElement? _selectedElement;
private ImageSource? _activeBackgroundImage;
- private bool _isConnected;
- private string _accountAvatarUrl = string.Empty;
- private string _accountDisplayName = string.Empty;
private double _audioLevel;
private bool _volumeAdjusting;
private bool _volumeFlash;
@@ -535,37 +532,6 @@ public partial class MainViewModel : ViewModelBase
}
}
- public bool IsConnected
- {
- get => _isConnected;
- set
- {
- if (SetProperty(ref _isConnected, value))
- {
- // ON-AIR requires a sign-in; dropping the session turns the pill
- // off so the state can never offer a stream without an account.
- if (!value) OnAirPillOn = false;
- OnPropertyChanged(nameof(CanToggleOnAir));
- OnPropertyChanged(nameof(ShowPrimaryStartButton));
- OnPropertyChanged(nameof(AccountStatusLightToolTip));
- }
- }
- }
-
- /// The connected YouTube account's avatar + name — shown in the top
- /// bar so the creator always sees WHICH account is about to go live.
- public string AccountAvatarUrl
- {
- get => _accountAvatarUrl;
- private set => SetProperty(ref _accountAvatarUrl, value);
- }
-
- public string AccountDisplayName
- {
- get => _accountDisplayName;
- private set => SetProperty(ref _accountDisplayName, value);
- }
-
// ─── Audio: the mic bar (meter + volume + mute + status dot) is always
// ─── visible; the game bar (meter + volume + mute) appears only while a
// ─── full-screen game is producing sound. Both meters preview live. ───
@@ -1062,11 +1028,6 @@ public partial class MainViewModel : ViewModelBase
public bool ShowLogInButton => !IsConnected && IsOffline && !IsRecording;
- /// Tooltip for the account status light explaining the color.
- public string AccountStatusLightToolTip =>
- IsLive ? "Live on YouTube — the top bar is red to show the stream is on the air."
- : IsConnected ? "You're signed in to YouTube. The light is green; ON-AIR is available."
- : "Signed out — the light is red and the ON-AIR pill is greyed out. You can still record locally.";
public bool ShowChatInactiveMessage => !IsLive;
public bool ShowEmptySceneHint => StagedScene != null && StagedScene.Elements.Count == 0;
public bool ShowPreviewPlaceholder => !ShowEmptySceneHint && ActiveBackgroundImage == null && BackgroundImage == null;
@@ -1789,46 +1750,6 @@ public partial class MainViewModel : ViewModelBase
AppLog.Write("MainViewModel ctor end");
}
- // Restores the DPAPI-saved OAuth session so sign-in survives restarts.
- // Best-effort: refresh a near-expiry access token; a session that can no
- // longer refresh is discarded and the user signs in again.
- private async Task LoadSavedSessionAsync()
- {
- try
- {
- var saved = TokenStore.Load();
- if (saved == null) return;
-
- _youtubeAuth.SetSession(saved);
- if (saved.TokenExpiry <= DateTime.UtcNow.AddMinutes(5))
- {
- var refreshed = await Task.Run(() => _youtubeAuth.RefreshToken());
- if (!refreshed)
- {
- TokenStore.Clear();
- AppLog.Write("Saved session could not be refreshed; signed out");
- _notifications.Info("Signed out of YouTube",
- "The saved session expired — sign in again when you next go live.");
- return;
- }
- }
-
- IsConnected = true;
- SyncConnectedAccount();
- }
- catch (Exception ex)
- {
- AppLog.Write($"LoadSavedSessionAsync failed: {ex}");
- }
- }
-
- private void SyncConnectedAccount()
- {
- var channel = _youtubeAuth.CurrentChannel;
- AccountAvatarUrl = channel?.ProfileImageUrl ?? string.Empty;
- AccountDisplayName = channel?.DisplayName ?? string.Empty;
- }
-
// ─── TASK 10 — Polar license validation ───
/// At startup, load the cached license state and re-validate online
@@ -2997,31 +2918,6 @@ public partial class MainViewModel : ViewModelBase
Process.Start(new ProcessStartInfo(url) { UseShellExecute = true });
}
- // Runs the OAuth flow (browser + loopback callback) on a background thread.
- // The resulting channel is returned to the caller and persisted via the
- // auth service's sessionChanged hook.
- private async Task SignInAsync()
- {
- try
- {
- var channel = await Task.Run(() => _youtubeAuth.AuthenticateAsync());
- if (channel == null)
- {
- _notifications.Warning("Sign-in was unsuccessful", "Please try again.");
- return null;
- }
-
- IsConnected = true;
- SyncConnectedAccount();
- return channel;
- }
- catch (Exception ex)
- {
- _notifications.Error("Sign-in failed", ex.Message);
- return null;
- }
- }
-
private void OnChatMessageReceived(ChatMessage message)
{
System.Windows.Application.Current.Dispatcher.Invoke(() =>
@@ -3880,21 +3776,4 @@ public partial class MainViewModel : ViewModelBase
? SocialBarPosition.Bottom
: SocialBarPosition.Top);
- /// Signs out of YouTube (the delete-the-YouTube-slot action in the dialog).
- private async Task SignOutYouTubeAsync()
- {
- _youtubeAuth.ClearSession();
- TokenStore.Clear();
- IsConnected = false;
- SyncConnectedAccount();
- NotifySocialsChanged();
- AppLog.Write("Socials: signed out of YouTube");
- await Task.CompletedTask;
}
-
- private async Task ChangeAccountAsync()
- {
- await SignOutYouTubeAsync();
- await SignInAsync();
- }
-}
diff --git a/ViewModels/index.md b/ViewModels/index.md
index 7b024a5..5f9408e 100644
--- a/ViewModels/index.md
+++ b/ViewModels/index.md
@@ -18,12 +18,20 @@ for what the ViewModels drive; base class in [`Helpers/ViewModelBase.cs`](../Hel
## 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):
+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`).
- Core file keeps: fields, ctor, `Shutdown`/`SaveLayoutNow`/`ScheduleSave`/`LoadLayout`,
`SetPanel`/`ShowOverlay`/`ShowLicenses`, `CreateBrush`, `OpenUrl`/`ComposeEmail`,
`SubmitBug`/`SubmitFeature`, window-geometry pass-through.
-- Partials: `Account` (sign-in/session/avatar), `License` (Polar key validation + `IsPremium`),
+- Partials: `Account` ✔ (Commit 1), `License` (Polar key validation + `IsPremium`),
`Scenes` (scene catalog + hotkeys + transitions), `Background` (background model + screen
capture + display picker), `Webcam` (webcam identity + per-scene configs + captures),
`Audio` (mic + game bars + TRAX), `Trax` (music player), `Socials` (social bar + dialog),