refactor: split Account out of MainViewModel — Commit 1/12 (tag refactor-commit-1)
Pure member movement into MainViewModel.Account.cs (zero behavior change): session restore (LoadSavedSessionAsync), sign-in/out/change (SignInAsync, SignOutYouTubeAsync, ChangeAccountAsync), IsConnected + account avatar/name + AccountStatusLightToolTip. Partial carries its own usings (ytLive.Services for the INotificationService default interface methods). docs in same commit: ViewModels/index.md split table + tagging convention.
This commit is contained in:
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>The connected YouTube account's avatar + name — shown in the top
|
||||||
|
/// bar so the creator always sees WHICH account is about to go live.</summary>
|
||||||
|
public string AccountAvatarUrl
|
||||||
|
{
|
||||||
|
get => _accountAvatarUrl;
|
||||||
|
private set => SetProperty(ref _accountAvatarUrl, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string AccountDisplayName
|
||||||
|
{
|
||||||
|
get => _accountDisplayName;
|
||||||
|
private set => SetProperty(ref _accountDisplayName, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Tooltip for the account status light explaining the color.</summary>
|
||||||
|
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<YouTubeChannel?> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Signs out of YouTube (the delete-the-YouTube-slot action in the dialog).</summary>
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -35,9 +35,6 @@ public partial class MainViewModel : ViewModelBase
|
|||||||
private Scene? _liveScene;
|
private Scene? _liveScene;
|
||||||
private SceneElement? _selectedElement;
|
private SceneElement? _selectedElement;
|
||||||
private ImageSource? _activeBackgroundImage;
|
private ImageSource? _activeBackgroundImage;
|
||||||
private bool _isConnected;
|
|
||||||
private string _accountAvatarUrl = string.Empty;
|
|
||||||
private string _accountDisplayName = string.Empty;
|
|
||||||
private double _audioLevel;
|
private double _audioLevel;
|
||||||
private bool _volumeAdjusting;
|
private bool _volumeAdjusting;
|
||||||
private bool _volumeFlash;
|
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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>The connected YouTube account's avatar + name — shown in the top
|
|
||||||
/// bar so the creator always sees WHICH account is about to go live.</summary>
|
|
||||||
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
|
// ─── Audio: the mic bar (meter + volume + mute + status dot) is always
|
||||||
// ─── visible; the game bar (meter + volume + mute) appears only while a
|
// ─── visible; the game bar (meter + volume + mute) appears only while a
|
||||||
// ─── full-screen game is producing sound. Both meters preview live. ───
|
// ─── 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;
|
public bool ShowLogInButton => !IsConnected && IsOffline && !IsRecording;
|
||||||
|
|
||||||
/// <summary>Tooltip for the account status light explaining the color.</summary>
|
|
||||||
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 ShowChatInactiveMessage => !IsLive;
|
||||||
public bool ShowEmptySceneHint => StagedScene != null && StagedScene.Elements.Count == 0;
|
public bool ShowEmptySceneHint => StagedScene != null && StagedScene.Elements.Count == 0;
|
||||||
public bool ShowPreviewPlaceholder => !ShowEmptySceneHint && ActiveBackgroundImage == null && BackgroundImage == null;
|
public bool ShowPreviewPlaceholder => !ShowEmptySceneHint && ActiveBackgroundImage == null && BackgroundImage == null;
|
||||||
@@ -1789,46 +1750,6 @@ public partial class MainViewModel : ViewModelBase
|
|||||||
AppLog.Write("MainViewModel ctor end");
|
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 ───
|
// ─── TASK 10 — Polar license validation ───
|
||||||
|
|
||||||
/// <summary>At startup, load the cached license state and re-validate online
|
/// <summary>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 });
|
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<YouTubeChannel?> 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)
|
private void OnChatMessageReceived(ChatMessage message)
|
||||||
{
|
{
|
||||||
System.Windows.Application.Current.Dispatcher.Invoke(() =>
|
System.Windows.Application.Current.Dispatcher.Invoke(() =>
|
||||||
@@ -3880,21 +3776,4 @@ public partial class MainViewModel : ViewModelBase
|
|||||||
? SocialBarPosition.Bottom
|
? SocialBarPosition.Bottom
|
||||||
: SocialBarPosition.Top);
|
: SocialBarPosition.Top);
|
||||||
|
|
||||||
/// <summary>Signs out of YouTube (the delete-the-YouTube-slot action in the dialog).</summary>
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-2
@@ -18,12 +18,20 @@ for what the ViewModels drive; base class in [`Helpers/ViewModelBase.cs`](../Hel
|
|||||||
## MainViewModel partial split (in progress)
|
## MainViewModel partial split (in progress)
|
||||||
|
|
||||||
`MainViewModel.cs` is being split into one file per functional area, zero behavior
|
`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`,
|
- Core file keeps: fields, ctor, `Shutdown`/`SaveLayoutNow`/`ScheduleSave`/`LoadLayout`,
|
||||||
`SetPanel`/`ShowOverlay`/`ShowLicenses`, `CreateBrush`, `OpenUrl`/`ComposeEmail`,
|
`SetPanel`/`ShowOverlay`/`ShowLicenses`, `CreateBrush`, `OpenUrl`/`ComposeEmail`,
|
||||||
`SubmitBug`/`SubmitFeature`, window-geometry pass-through.
|
`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
|
`Scenes` (scene catalog + hotkeys + transitions), `Background` (background model + screen
|
||||||
capture + display picker), `Webcam` (webcam identity + per-scene configs + captures),
|
capture + display picker), `Webcam` (webcam identity + per-scene configs + captures),
|
||||||
`Audio` (mic + game bars + TRAX), `Trax` (music player), `Socials` (social bar + dialog),
|
`Audio` (mic + game bars + TRAX), `Trax` (music player), `Socials` (social bar + dialog),
|
||||||
|
|||||||
Reference in New Issue
Block a user