Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9baae33916 | |||
| 5eecf4536f |
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
using ytLive.Helpers;
|
||||
using ytLive.Services;
|
||||
|
||||
namespace ytLive.ViewModels;
|
||||
|
||||
public partial class MainViewModel
|
||||
{
|
||||
// TASK 10 — Polar license validation
|
||||
private string _licenseKey = string.Empty;
|
||||
private string _licenseMessage = string.Empty;
|
||||
private bool _isLicenseValidating;
|
||||
private bool _isLicenseEntryOpen;
|
||||
private static readonly TimeSpan OfflineGracePeriod = TimeSpan.FromDays(14);
|
||||
|
||||
/// <summary>True when the user holds a valid LlamaCasty license (Polar). When
|
||||
/// false, the branding flash runs on every live stream.</summary>
|
||||
private bool _isPremium;
|
||||
public bool IsPremium
|
||||
{
|
||||
get => _isPremium;
|
||||
private set
|
||||
{
|
||||
if (!SetProperty(ref _isPremium, value)) return;
|
||||
// Flip the branding flash when premium state changes.
|
||||
BrandFlashEnabled = !value;
|
||||
}
|
||||
}
|
||||
|
||||
// TASK 10 — license key entry
|
||||
public string LicenseKey
|
||||
{
|
||||
get => _licenseKey;
|
||||
set => SetProperty(ref _licenseKey, value);
|
||||
}
|
||||
|
||||
public string LicenseMessage
|
||||
{
|
||||
get => _licenseMessage;
|
||||
private set => SetProperty(ref _licenseMessage, value);
|
||||
}
|
||||
|
||||
public bool IsLicenseValidating
|
||||
{
|
||||
get => _isLicenseValidating;
|
||||
private set => SetProperty(ref _isLicenseValidating, value);
|
||||
}
|
||||
|
||||
public bool IsLicenseEntryOpen
|
||||
{
|
||||
get => _isLicenseEntryOpen;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _isLicenseEntryOpen, value))
|
||||
OnPropertyChanged(nameof(IsHubVisible));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>The About hub is visible only when neither the licenses panel
|
||||
/// nor the license entry panel is open.</summary>
|
||||
public bool IsHubVisible => !IsLicensesOpen && !IsLicenseEntryOpen;
|
||||
|
||||
// ─── TASK 10 — Polar license validation ───
|
||||
|
||||
/// <summary>At startup, load the cached license state and re-validate online
|
||||
/// if a key exists. If offline and within the 14-day grace period, the cached
|
||||
/// premium state is trusted. If the grace period is exceeded, premium lapses.</summary>
|
||||
private async Task ValidateLicenseAtStartupAsync()
|
||||
{
|
||||
var savedKey = _layoutStore.LoadLicenseKey();
|
||||
if (string.IsNullOrWhiteSpace(savedKey))
|
||||
{
|
||||
IsPremium = false;
|
||||
return;
|
||||
}
|
||||
|
||||
LicenseKey = savedKey;
|
||||
var validatedAt = _layoutStore.LoadLicenseValidatedAt();
|
||||
var cachedPremium = _layoutStore.LoadIsPremium();
|
||||
|
||||
// If the last validation was within the grace period, trust the cache
|
||||
// while we attempt an online re-validation in the background.
|
||||
if (validatedAt.HasValue && DateTime.UtcNow - validatedAt.Value < OfflineGracePeriod)
|
||||
{
|
||||
IsPremium = cachedPremium;
|
||||
}
|
||||
|
||||
// Try an online check — if it succeeds, it overrides the cache.
|
||||
try
|
||||
{
|
||||
var service = new PolarLicenseService();
|
||||
var result = await service.ValidateAsync(savedKey);
|
||||
if (result.IsValid)
|
||||
{
|
||||
IsPremium = true;
|
||||
_layoutStore.SaveIsPremium(true);
|
||||
_layoutStore.SaveLicenseValidatedAt(DateTime.UtcNow);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Key revoked or expired — clear premium.
|
||||
IsPremium = false;
|
||||
_layoutStore.SaveIsPremium(false);
|
||||
_layoutStore.SaveLicenseValidatedAt(DateTime.UtcNow);
|
||||
AppLog.Write($"License validation failed: {result.Error}");
|
||||
_notifications.Warning("Premium is no longer active",
|
||||
$"{result.Error} — watermark branding returns until the license is renewed.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Offline or network error — keep the cached state, log it.
|
||||
AppLog.Write($"License re-validation skipped (offline?): {ex.Message}");
|
||||
if (!validatedAt.HasValue || DateTime.UtcNow - validatedAt.Value >= OfflineGracePeriod)
|
||||
{
|
||||
IsPremium = false;
|
||||
_layoutStore.SaveIsPremium(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Called from the Activate button in the About overlay. Validates
|
||||
/// the entered key, persists it, and flips premium state.</summary>
|
||||
private async Task ActivateLicenseAsync()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(LicenseKey)) return;
|
||||
|
||||
IsLicenseValidating = true;
|
||||
LicenseMessage = "Validating\u2026";
|
||||
|
||||
try
|
||||
{
|
||||
var service = new PolarLicenseService();
|
||||
var result = await service.ValidateAsync(LicenseKey);
|
||||
if (result.IsValid)
|
||||
{
|
||||
IsPremium = true;
|
||||
_layoutStore.SaveLicenseKey(LicenseKey.Trim());
|
||||
_layoutStore.SaveIsPremium(true);
|
||||
_layoutStore.SaveLicenseValidatedAt(DateTime.UtcNow);
|
||||
LicenseMessage = "Premium activated! The watermark has been removed.";
|
||||
AppLog.Write("License activated successfully");
|
||||
}
|
||||
else
|
||||
{
|
||||
IsPremium = false;
|
||||
_layoutStore.SaveLicenseKey(null);
|
||||
_layoutStore.SaveIsPremium(false);
|
||||
LicenseMessage = result.Error ?? "Invalid license key.";
|
||||
AppLog.Write($"License activation failed: {result.Error}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LicenseMessage = $"Activation error: {ex.Message}";
|
||||
AppLog.Write($"License activation error: {ex}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsLicenseValidating = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -176,13 +173,6 @@ public partial class MainViewModel : ViewModelBase
|
||||
// TASK 20 step 2 — hotkey bindings (custom chord per action)
|
||||
private Dictionary<HotkeyId, HotkeyChord> _hotkeyBindings = new();
|
||||
|
||||
// TASK 10 — Polar license validation
|
||||
private string _licenseKey = string.Empty;
|
||||
private string _licenseMessage = string.Empty;
|
||||
private bool _isLicenseValidating;
|
||||
private bool _isLicenseEntryOpen;
|
||||
private static readonly TimeSpan OfflineGracePeriod = TimeSpan.FromDays(14);
|
||||
|
||||
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.
|
||||
@@ -470,20 +460,6 @@ public partial class MainViewModel : ViewModelBase
|
||||
/// <summary>The global socials config (entries + bar settings), or null.</summary>
|
||||
public SocialsConfig? Socials => _socials;
|
||||
|
||||
/// <summary>True when the user holds a valid LlamaCasty license (Polar). When
|
||||
/// false, the branding flash runs on every live stream.</summary>
|
||||
private bool _isPremium;
|
||||
public bool IsPremium
|
||||
{
|
||||
get => _isPremium;
|
||||
private set
|
||||
{
|
||||
if (!SetProperty(ref _isPremium, value)) return;
|
||||
// Flip the branding flash when premium state changes.
|
||||
BrandFlashEnabled = !value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Canvas.Top for the social bar: 0 = top, 1040 = bottom (40px from the 1080 edge).</summary>
|
||||
public double SocialBarTop => _socials?.BarPosition == SocialBarPosition.Top ? 0 : 1040;
|
||||
|
||||
@@ -535,37 +511,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
|
||||
// ─── visible; the game bar (meter + volume + mute) appears only while a
|
||||
// ─── full-screen game is producing sound. Both meters preview live. ───
|
||||
@@ -1062,11 +1007,6 @@ public partial class MainViewModel : ViewModelBase
|
||||
|
||||
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 ShowEmptySceneHint => StagedScene != null && StagedScene.Elements.Count == 0;
|
||||
public bool ShowPreviewPlaceholder => !ShowEmptySceneHint && ActiveBackgroundImage == null && BackgroundImage == null;
|
||||
@@ -1550,39 +1490,6 @@ public partial class MainViewModel : ViewModelBase
|
||||
set => SetProperty(ref _featureRequestEmail, value);
|
||||
}
|
||||
|
||||
// TASK 10 — license key entry
|
||||
public string LicenseKey
|
||||
{
|
||||
get => _licenseKey;
|
||||
set => SetProperty(ref _licenseKey, value);
|
||||
}
|
||||
|
||||
public string LicenseMessage
|
||||
{
|
||||
get => _licenseMessage;
|
||||
private set => SetProperty(ref _licenseMessage, value);
|
||||
}
|
||||
|
||||
public bool IsLicenseValidating
|
||||
{
|
||||
get => _isLicenseValidating;
|
||||
private set => SetProperty(ref _isLicenseValidating, value);
|
||||
}
|
||||
|
||||
public bool IsLicenseEntryOpen
|
||||
{
|
||||
get => _isLicenseEntryOpen;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _isLicenseEntryOpen, value))
|
||||
OnPropertyChanged(nameof(IsHubVisible));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>The About hub is visible only when neither the licenses panel
|
||||
/// nor the license entry panel is open.</summary>
|
||||
public bool IsHubVisible => !IsLicensesOpen && !IsLicenseEntryOpen;
|
||||
|
||||
// Commands
|
||||
public ICommand AddSceneCommand { get; }
|
||||
public ICommand AddSourceCommand { get; }
|
||||
@@ -1789,147 +1696,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 ───
|
||||
|
||||
/// <summary>At startup, load the cached license state and re-validate online
|
||||
/// if a key exists. If offline and within the 14-day grace period, the cached
|
||||
/// premium state is trusted. If the grace period is exceeded, premium lapses.</summary>
|
||||
private async Task ValidateLicenseAtStartupAsync()
|
||||
{
|
||||
var savedKey = _layoutStore.LoadLicenseKey();
|
||||
if (string.IsNullOrWhiteSpace(savedKey))
|
||||
{
|
||||
IsPremium = false;
|
||||
return;
|
||||
}
|
||||
|
||||
LicenseKey = savedKey;
|
||||
var validatedAt = _layoutStore.LoadLicenseValidatedAt();
|
||||
var cachedPremium = _layoutStore.LoadIsPremium();
|
||||
|
||||
// If the last validation was within the grace period, trust the cache
|
||||
// while we attempt an online re-validation in the background.
|
||||
if (validatedAt.HasValue && DateTime.UtcNow - validatedAt.Value < OfflineGracePeriod)
|
||||
{
|
||||
IsPremium = cachedPremium;
|
||||
}
|
||||
|
||||
// Try an online check — if it succeeds, it overrides the cache.
|
||||
try
|
||||
{
|
||||
var service = new PolarLicenseService();
|
||||
var result = await service.ValidateAsync(savedKey);
|
||||
if (result.IsValid)
|
||||
{
|
||||
IsPremium = true;
|
||||
_layoutStore.SaveIsPremium(true);
|
||||
_layoutStore.SaveLicenseValidatedAt(DateTime.UtcNow);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Key revoked or expired — clear premium.
|
||||
IsPremium = false;
|
||||
_layoutStore.SaveIsPremium(false);
|
||||
_layoutStore.SaveLicenseValidatedAt(DateTime.UtcNow);
|
||||
AppLog.Write($"License validation failed: {result.Error}");
|
||||
_notifications.Warning("Premium is no longer active",
|
||||
$"{result.Error} — watermark branding returns until the license is renewed.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Offline or network error — keep the cached state, log it.
|
||||
AppLog.Write($"License re-validation skipped (offline?): {ex.Message}");
|
||||
if (!validatedAt.HasValue || DateTime.UtcNow - validatedAt.Value >= OfflineGracePeriod)
|
||||
{
|
||||
IsPremium = false;
|
||||
_layoutStore.SaveIsPremium(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Called from the Activate button in the About overlay. Validates
|
||||
/// the entered key, persists it, and flips premium state.</summary>
|
||||
private async Task ActivateLicenseAsync()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(LicenseKey)) return;
|
||||
|
||||
IsLicenseValidating = true;
|
||||
LicenseMessage = "Validating\u2026";
|
||||
|
||||
try
|
||||
{
|
||||
var service = new PolarLicenseService();
|
||||
var result = await service.ValidateAsync(LicenseKey);
|
||||
if (result.IsValid)
|
||||
{
|
||||
IsPremium = true;
|
||||
_layoutStore.SaveLicenseKey(LicenseKey.Trim());
|
||||
_layoutStore.SaveIsPremium(true);
|
||||
_layoutStore.SaveLicenseValidatedAt(DateTime.UtcNow);
|
||||
LicenseMessage = "Premium activated! The watermark has been removed.";
|
||||
AppLog.Write("License activated successfully");
|
||||
}
|
||||
else
|
||||
{
|
||||
IsPremium = false;
|
||||
_layoutStore.SaveLicenseKey(null);
|
||||
_layoutStore.SaveIsPremium(false);
|
||||
LicenseMessage = result.Error ?? "Invalid license key.";
|
||||
AppLog.Write($"License activation failed: {result.Error}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LicenseMessage = $"Activation error: {ex.Message}";
|
||||
AppLog.Write($"License activation error: {ex}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsLicenseValidating = false;
|
||||
}
|
||||
}
|
||||
|
||||
private static string DefaultLayoutPath => Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||
"ytLlive",
|
||||
@@ -2997,31 +2763,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<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)
|
||||
{
|
||||
System.Windows.Application.Current.Dispatcher.Invoke(() =>
|
||||
@@ -3880,21 +3621,4 @@ public partial class MainViewModel : ViewModelBase
|
||||
? SocialBarPosition.Bottom
|
||||
: 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();
|
||||
}
|
||||
}
|
||||
|
||||
+14
-3
@@ -18,13 +18,24 @@ 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`).
|
||||
- **Commit 2/12 · `refactor-commit-2`: License landed** → `MainViewModel.License.cs`
|
||||
(Polar license state: `LicenseKey`/`LicenseMessage`/`IsLicenseValidating`/
|
||||
`IsLicenseEntryOpen`/`IsHubVisible`/`IsPremium`, `OfflineGracePeriod`,
|
||||
`ValidateLicenseAtStartupAsync`, `ActivateLicenseAsync`).
|
||||
|
||||
- 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`),
|
||||
`Scenes` (scene catalog + hotkeys + transitions), `Background` (background model + screen
|
||||
- Partials: `Account` ✔ (Commit 1), `License` ✔ (Commit 2), `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),
|
||||
`Streaming` (go-live/record + health + quality tiers + media source pipeline),
|
||||
|
||||
Reference in New Issue
Block a user