Brand flash OFF by default: MainViewModel.ShowBrandFlash master switch (default false) gates BrandFlashEnabled; feature kept, tests opt in; new test guards the default (Unit: brand flash off)
This commit is contained in:
@@ -23,7 +23,7 @@ public partial class MainViewModel
|
||||
if (!SetProperty(ref _isPremium, value)) return;
|
||||
// The flash is composited into the broadcast, so the gate has to be pushed
|
||||
// all the way into the renderer — not just the preview.
|
||||
_brandFlash.Enabled = !value;
|
||||
_brandFlash.Enabled = ShowBrandFlash && !value;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+15
-11
@@ -51,11 +51,9 @@ public partial class MainViewModel : ViewModelBase
|
||||
|
||||
private WebView2Manager? _webView2Manager;
|
||||
|
||||
// Branding flash (monetization): a full-frame "made with LlamaCasty!" shown
|
||||
// for ~1s every 300s on the live output, ~25% opacity. Paid unlock sets
|
||||
// BrandFlashEnabled = false. See ai.md "Monetization".
|
||||
// The free-tier branding credit. Composed into the OUTPUT by FramePump, not just
|
||||
// previewed — see BrandFlashPresenter. Gated on IsPremium (Polar), never a toggle.
|
||||
// Branding credit: "made with LlamaCasty!" composed into the OUTPUT by FramePump, not
|
||||
// just previewed — see BrandFlashPresenter. OFF by default since 2026-10-09
|
||||
// (ShowBrandFlash = false, free-forever: no branding on the stream); the code stays.
|
||||
private readonly BrandFlashPresenter _brandFlash;
|
||||
|
||||
public SceneElement? SelectedElement
|
||||
@@ -122,11 +120,17 @@ public partial class MainViewModel : ViewModelBase
|
||||
|
||||
public bool ShowPreviewPlaceholder => !ShowEmptySceneHint && ActiveBackgroundImage == null && BackgroundImage == null;
|
||||
|
||||
// Paid unlock flips this off (see ai.md "Monetization"). When disabled the
|
||||
// presenter stops emitting, so any active credit dies on the next tick — and a
|
||||
// credit already in flight is cut, not run out. Derived, never assignable: the
|
||||
// whole point is that nobody can turn the advertising on a paying stream.
|
||||
public bool BrandFlashEnabled => !IsPremium;
|
||||
/// <summary>Master switch for the "made with LlamaCasty!" credit (creator ruling
|
||||
/// 2026-10-09: turn it OFF, but keep the feature). <c>false</c> = the credit never
|
||||
/// shows — not in the preview, the stream, or recordings. Read when the view model is
|
||||
/// constructed (and on every licence change), so set it before a <c>MainWindow</c> is
|
||||
/// created; the tests that exercise the credit opt in that way.</summary>
|
||||
internal static bool ShowBrandFlash { get; set; } = false;
|
||||
|
||||
// When disabled the presenter stops emitting, so any active credit dies on the next
|
||||
// tick — and a credit already in flight is cut, not run out. Derived, never
|
||||
// assignable from the UI: the master switch AND the licence must both allow it.
|
||||
public bool BrandFlashEnabled => ShowBrandFlash && !IsPremium;
|
||||
|
||||
// Window geometry pass-through (code-behind owns the WPF specifics).
|
||||
public (double Left, double Top, double Width, double Height, string State)? RestoreWindowState()
|
||||
@@ -184,7 +188,7 @@ public partial class MainViewModel : ViewModelBase
|
||||
// — go-live and local recording are the same FramePump, so one start covers
|
||||
// output, recording and the preview pane alike. Enabled is seeded from the
|
||||
// current licence state; ValidateLicenseAtStartupAsync re-pushes it later.
|
||||
_brandFlash.Enabled = !IsPremium;
|
||||
_brandFlash.Enabled = BrandFlashEnabled;
|
||||
_brandFlash.Start();
|
||||
|
||||
_healthPollTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(30) };
|
||||
|
||||
@@ -1767,6 +1767,15 @@ Apply this to every UI decision:
|
||||
|
||||
## Monetization (design decision — the branding flash is the sword)
|
||||
|
||||
> **⛔ Superseded 2026-10-08/09 — free forever, no branding on the stream.** The credit is now
|
||||
> **OFF by default**: `MainViewModel.ShowBrandFlash` (internal static, default `false`) is the
|
||||
> master switch, and `BrandFlashEnabled => ShowBrandFlash && !IsPremium` feeds the presenter at
|
||||
> construction and on every licence change. The code (`BrandFlashPresenter`, the FramePump
|
||||
> `brandFlash:` seam, the preview layer) is kept, not deleted — creator ruling: "don't delete it,
|
||||
> just make it conditionally displayed". `BrandFlashOutputTests` opt in by setting the switch
|
||||
> before constructing a `MainWindow`; `Credit_IsOffByDefault_EvenOnTheFreeTier` guards the default.
|
||||
> Everything below is the historical paid-tier design.
|
||||
|
||||
Full pricing and the Year 1 revenue forecast in `MONETIZATION.md` (gitignored). ⚠️ **No Polar
|
||||
product, discount code, or license key exists any more** — billing is Microsoft Store IAP.
|
||||
Brand palette and assets in `MARCOM.md` (gitignored). This section covers the in-app model.
|
||||
|
||||
@@ -164,6 +164,7 @@ public sealed class BrandFlashOutputTests
|
||||
{
|
||||
var tempDb = Path.Combine(Path.GetTempPath(), $"ytLlive-test-{Guid.NewGuid():N}.db");
|
||||
MainViewModel.LayoutPathOverride = tempDb;
|
||||
MainViewModel.ShowBrandFlash = true; // the credit is off by default (2026-10-09)
|
||||
var window = new MainWindow();
|
||||
var vm = (MainViewModel)window.DataContext;
|
||||
try
|
||||
@@ -202,6 +203,7 @@ public sealed class BrandFlashOutputTests
|
||||
{
|
||||
window.Close();
|
||||
MainViewModel.LayoutPathOverride = null;
|
||||
MainViewModel.ShowBrandFlash = false;
|
||||
SqliteConnection.ClearAllPools();
|
||||
try { File.Delete(tempDb); } catch { /* best-effort cleanup */ }
|
||||
}
|
||||
@@ -381,6 +383,36 @@ public sealed class BrandFlashOutputTests
|
||||
Assert.True(prop.CanRead, "the preview needs to read the gate state");
|
||||
}
|
||||
|
||||
/// <summary>Creator ruling 2026-10-09: the credit is OFF by default — a freshly
|
||||
/// constructed app with no licence still emits nothing to the preview or the output.</summary>
|
||||
[Fact]
|
||||
public void Credit_IsOffByDefault_EvenOnTheFreeTier()
|
||||
{
|
||||
_app.Run(() =>
|
||||
{
|
||||
Assert.False(MainViewModel.ShowBrandFlash);
|
||||
var tempDb = Path.Combine(Path.GetTempPath(), $"ytLlive-test-{Guid.NewGuid():N}.db");
|
||||
MainViewModel.LayoutPathOverride = tempDb;
|
||||
var window = new MainWindow();
|
||||
var vm = (MainViewModel)window.DataContext;
|
||||
try
|
||||
{
|
||||
Assert.False(vm.IsPremium);
|
||||
Assert.False(vm.BrandFlashEnabled);
|
||||
vm.AdvanceBrandFlash(120); // past several cadence windows
|
||||
Assert.Null(vm.BrandFlashFrame());
|
||||
Assert.False(vm.BrandFlashVisible);
|
||||
}
|
||||
finally
|
||||
{
|
||||
window.Close();
|
||||
MainViewModel.LayoutPathOverride = null;
|
||||
SqliteConnection.ClearAllPools();
|
||||
try { File.Delete(tempDb); } catch { /* best-effort cleanup */ }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- the credit is NOT a go-live-only behaviour ----------
|
||||
|
||||
/// <summary>Reported 2026-09-26: "the made with llamacasty flash should appear in
|
||||
@@ -400,6 +432,7 @@ public sealed class BrandFlashOutputTests
|
||||
{
|
||||
var tempDb = Path.Combine(Path.GetTempPath(), $"ytLlive-test-{Guid.NewGuid():N}.db");
|
||||
MainViewModel.LayoutPathOverride = tempDb;
|
||||
MainViewModel.ShowBrandFlash = true; // the credit is off by default (2026-10-09)
|
||||
var window = new MainWindow();
|
||||
var vm = (MainViewModel)window.DataContext;
|
||||
try
|
||||
@@ -441,6 +474,7 @@ public sealed class BrandFlashOutputTests
|
||||
{
|
||||
window.Close();
|
||||
MainViewModel.LayoutPathOverride = null;
|
||||
MainViewModel.ShowBrandFlash = false;
|
||||
SqliteConnection.ClearAllPools();
|
||||
try { File.Delete(tempDb); } catch { /* best-effort cleanup */ }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user