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:
2026-10-09 13:22:52 -07:00
parent 352f117cb2
commit 8303b1cf7f
4 changed files with 59 additions and 12 deletions
+1 -1
View File
@@ -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
View File
@@ -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) };
+9
View File
@@ -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.
+34
View File
@@ -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 */ }
}