refactor: split License out of MainViewModel — Commit 2/12 (tag refactor-commit-2)
Pure member movement into MainViewModel.License.cs (zero behavior change): Polar license state (LicenseKey/LicenseMessage/IsLicenseValidating/ IsLicenseEntryOpen/IsHubVisible/IsPremium + OfflineGracePeriod), ValidateLicenseAtStartupAsync, ActivateLicenseAsync. docs in same commit: ViewModels/index.md split table (License ✔).
This commit is contained in:
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -173,13 +173,6 @@ public partial class MainViewModel : ViewModelBase
|
|||||||
// TASK 20 step 2 — hotkey bindings (custom chord per action)
|
// TASK 20 step 2 — hotkey bindings (custom chord per action)
|
||||||
private Dictionary<HotkeyId, HotkeyChord> _hotkeyBindings = new();
|
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";
|
private const string SupportEmail = "gramps@llamachile.shop";
|
||||||
// The creator-hub link targets (About overlay) — public so the hub's
|
// The creator-hub link targets (About overlay) — public so the hub's
|
||||||
// integration test can assert they point at real destinations.
|
// integration test can assert they point at real destinations.
|
||||||
@@ -467,20 +460,6 @@ public partial class MainViewModel : ViewModelBase
|
|||||||
/// <summary>The global socials config (entries + bar settings), or null.</summary>
|
/// <summary>The global socials config (entries + bar settings), or null.</summary>
|
||||||
public SocialsConfig? Socials => _socials;
|
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>
|
/// <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;
|
public double SocialBarTop => _socials?.BarPosition == SocialBarPosition.Top ? 0 : 1040;
|
||||||
|
|
||||||
@@ -1511,39 +1490,6 @@ public partial class MainViewModel : ViewModelBase
|
|||||||
set => SetProperty(ref _featureRequestEmail, value);
|
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
|
// Commands
|
||||||
public ICommand AddSceneCommand { get; }
|
public ICommand AddSceneCommand { get; }
|
||||||
public ICommand AddSourceCommand { get; }
|
public ICommand AddSourceCommand { get; }
|
||||||
@@ -1750,107 +1696,6 @@ public partial class MainViewModel : ViewModelBase
|
|||||||
AppLog.Write("MainViewModel ctor end");
|
AppLog.Write("MainViewModel ctor end");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── 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(
|
private static string DefaultLayoutPath => Path.Combine(
|
||||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||||
"ytLlive",
|
"ytLlive",
|
||||||
|
|||||||
+5
-2
@@ -27,12 +27,15 @@ Progress (Commit N/12 · tag):
|
|||||||
- **Commit 1/12 · `refactor-commit-1`: Account landed** → `MainViewModel.Account.cs`
|
- **Commit 1/12 · `refactor-commit-1`: Account landed** → `MainViewModel.Account.cs`
|
||||||
(session restore `LoadSavedSessionAsync`, sign-in/out/change account,
|
(session restore `LoadSavedSessionAsync`, sign-in/out/change account,
|
||||||
`IsConnected`/`AccountAvatarUrl`/`AccountDisplayName`/`AccountStatusLightToolTip`).
|
`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`,
|
- 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` ✔ (Commit 1), `License` (Polar key validation + `IsPremium`),
|
- Partials: `Account` ✔ (Commit 1), `License` ✔ (Commit 2), `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),
|
||||||
`Streaming` (go-live/record + health + quality tiers + media source pipeline),
|
`Streaming` (go-live/record + health + quality tiers + media source pipeline),
|
||||||
|
|||||||
Reference in New Issue
Block a user