diff --git a/MainWindow.xaml b/MainWindow.xaml
index 5a57213..8a2100d 100644
--- a/MainWindow.xaml
+++ b/MainWindow.xaml
@@ -376,6 +376,39 @@
+
+
+
+
+
+
+
+
+
@@ -622,7 +655,7 @@
-
$"Version {typeof(MainViewModel).Assembly.GetName().Version?.ToString(3)}";
@@ -135,6 +143,33 @@ public class MainViewModel : ViewModelBase
public bool ShowPreviewPlaceholder => !ShowEmptySceneHint && ActiveBackgroundImage == null;
public bool ShowSourcesEmptyHint => ActiveScene == null || ActiveScene.Sources.Count == 0;
+ // Paid unlock flips this off (see ai.md "Monetization"). When disabled the
+ // cadence timer is stopped and any active flash is hidden immediately.
+ public bool BrandFlashEnabled
+ {
+ get => _brandFlashEnabled;
+ set
+ {
+ if (!SetProperty(ref _brandFlashEnabled, value)) return;
+ if (value)
+ {
+ if (IsLive) StartBrandFlashTimer();
+ }
+ else
+ {
+ _brandFlashTimer.Stop();
+ _brandFlashOffTimer.Stop();
+ BrandFlashActive = false;
+ }
+ }
+ }
+
+ public bool BrandFlashActive
+ {
+ get => _brandFlashActive;
+ private set => SetProperty(ref _brandFlashActive, value);
+ }
+
private void UpdateActiveBackground()
{
var background = ActiveScene?.Sources.FirstOrDefault(s => s.Type == SourceType.Background);
@@ -356,6 +391,12 @@ public class MainViewModel : ViewModelBase
_saveDebounce = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(1500) };
_saveDebounce.Tick += (_, _) => SaveLayoutNow();
+
+ _brandFlashTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(300) };
+ _brandFlashTimer.Tick += OnBrandFlashTimerTick;
+ _brandFlashOffTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(750) };
+ _brandFlashOffTimer.Tick += (_, _) => { _brandFlashOffTimer.Stop(); BrandFlashActive = false; };
+
Scenes.CollectionChanged += OnScenesChanged;
AddSceneCommand = new RelayCommand(_ => AddScene());
@@ -852,15 +893,44 @@ public class MainViewModel : ViewModelBase
LiveElapsedText = "00:00:00";
LivePulseOpacity = 1.0;
_liveTimer.Start();
+ if (BrandFlashEnabled) StartBrandFlashTimer();
}
else
{
_liveTimer.Stop();
+ _brandFlashTimer.Stop();
+ _brandFlashOffTimer.Stop();
+ BrandFlashActive = false;
LiveElapsedText = "00:00:00";
LivePulseOpacity = 1.0;
}
}
+ // First flash shortly after go-live, then every 300s (the cadence resets on
+ // the first tick so the interval is 5s only for that one shot).
+ private void StartBrandFlashTimer()
+ {
+ _brandFlashTimer.Stop();
+ _brandFlashTimer.Interval = TimeSpan.FromSeconds(5);
+ _brandFlashTimer.Start();
+ }
+
+ private void OnBrandFlashTimerTick(object? sender, EventArgs e)
+ {
+ _brandFlashTimer.Stop();
+ _brandFlashTimer.Interval = TimeSpan.FromSeconds(300);
+ _brandFlashTimer.Start();
+ ShowBrandFlash();
+ }
+
+ private void ShowBrandFlash()
+ {
+ if (!IsLive || !BrandFlashEnabled) return;
+ BrandFlashActive = true;
+ _brandFlashOffTimer.Stop();
+ _brandFlashOffTimer.Start();
+ }
+
private void OnLiveTimerTick(object? sender, EventArgs e)
{
_liveElapsed = _liveElapsed.Add(TimeSpan.FromSeconds(1));
diff --git a/ai.md b/ai.md
index c66ab5b..f33ec90 100644
--- a/ai.md
+++ b/ai.md
@@ -81,20 +81,26 @@ Apply this to every UI decision:
- Visual/drag-and-drop scene building over property panels
- Every action produces a visible outcome — no dead ends
-## Monetization (design decision — the watermark is the sword)
+## Monetization (design decision — the branding flash is the sword)
Free forever: all streams unlimited, no time caps, no subscription, no per-feature paywalls. The
**one paid line is a one-time unlock** (delivered via itch.io — they handle hosting, payment, and key
delivery; we never own a server or a key shop):
-- **Free:** a small "made with ytLlive" watermark is always on, every frame, every stream — the sword
- of Damocles. Standard practice; only Streamlabs runs watermark-nagging to a capitalist extreme.
-- **Paid (one-time):** watermark removed + **Alerts** (Super Chat / membership / subscribe pop-ins).
+- **Free:** a periodic full-frame branding flash — "made with ytLlive!" rendered big and centered at
+ ~25% opacity for about one second (soft 250ms fade in/out), repeated every 300s, on the live output
+ (and on v0.2 local recordings). Implemented as `BrandFlashLayer` in the preview compositor
+ (`MainWindow.xaml` CanvasGrid) + `BrandFlashTimer` in `MainViewModel` — cadence 300s, first flash
+ ~5s after go-live, only while live or recording. An always-on watermark can be cropped or covered;
+ an intermittent full-frame flash can't be cropped and is impractical to edit around on a live feed.
+- **Paid (one-time):** branding flash removed (flips `BrandFlashEnabled` off) + **Alerts**
+ (Super Chat / membership / subscribe pop-ins).
-Deliberately rejected: hard stream-time cutoffs (the worst dead end — a stream dying mid-broadcast
-reads as broken, and YouTube streams routinely run 2-4 hours), soft-limit nagging, freemium tiers,
-and donation-only (relies on the kindness of strangers). Resolution/quality ceilings are **deferred** —
-that decision belongs to the resolution & streaming-constraints conversation, not monetization.
+Deliberately rejected: always-on watermark (obscurable — replaced by the flash), hard stream-time
+cutoffs (the worst dead end — a stream dying mid-broadcast reads as broken, and YouTube streams
+routinely run 2-4 hours), soft-limit nagging, freemium tiers, and donation-only (relies on the
+kindness of strangers). Resolution/quality ceilings are **deferred** — that decision belongs to the
+resolution & streaming-constraints conversation, not monetization.
## Auth gates Go Live, but not exploration