diff --git a/Controls/LeftPanel.xaml b/Controls/LeftPanel.xaml index 03437a9..a625fcd 100644 --- a/Controls/LeftPanel.xaml +++ b/Controls/LeftPanel.xaml @@ -453,6 +453,9 @@ + is the layer's master switch. Off means NOTHING is + /// polled, whatever the session is doing — a hidden scoreboard has no reason to spend quota + /// (creator ruling 2026-10-30). + public static ScoreboardPoll Due(SessionPhase phase, DateTime lastAnalyticsUtc, DateTime lastSubscriberUtc, + DateTime nowUtc, bool enabled = true) { + if (!enabled) return ScoreboardPoll.None; if (phase == SessionPhase.Idle) return ScoreboardPoll.None; var analyticsDue = phase == SessionPhase.Live && nowUtc - lastAnalyticsUtc >= AnalyticsInterval; diff --git a/Services/LayoutStore.Settings.cs b/Services/LayoutStore.Settings.cs index 2448026..58ae368 100644 --- a/Services/LayoutStore.Settings.cs +++ b/Services/LayoutStore.Settings.cs @@ -51,6 +51,12 @@ public partial class LayoutStore : IDisposable public void SaveMonoOutput(bool enabled) => UpsertSetting("Audio.MonoOutput", enabled ? "1" : "0"); + /// Scoreboard master switch (creator 2026-10-30). Persisted because "off" also means + /// "stop polling" — a switch that resets would quietly resume spending quota. + public bool LoadScoreboardEnabled() => GetSetting("Scoreboard.Enabled") != "0"; + + public void SaveScoreboardEnabled(bool enabled) => UpsertSetting("Scoreboard.Enabled", enabled ? "1" : "0"); + /// The user's chosen record output folder (TASK 18), or null to use the /// default %APPDATA%\ytLlive\recordings. Restored at startup. public string? LoadRecordFolder() diff --git a/ViewModels/MainViewModel.Scoreboard.cs b/ViewModels/MainViewModel.Scoreboard.cs index 9889548..30d0aba 100644 --- a/ViewModels/MainViewModel.Scoreboard.cs +++ b/ViewModels/MainViewModel.Scoreboard.cs @@ -28,6 +28,28 @@ public partial class MainViewModel internal ScoreboardRecorder ScoreboardRecorder => _scoreboard; + private bool _scoreboardEnabled = true; + + /// The scoreboard's master switch. Off hides the strip in EVERY scene (the per-scene + /// eye stays independent) and — the point of the switch — stops all polling: a scoreboard nobody + /// can see must not spend quota. + public bool ScoreboardEnabled + { + get => _scoreboardEnabled; + set + { + if (!SetProperty(ref _scoreboardEnabled, value)) return; + _layoutStore.SaveScoreboardEnabled(value); + RefreshScoreboardPreviews(); + OnPropertyChanged(nameof(ScoreboardPollsEnabled)); + } + } + + /// What the cadence policy asks: the master switch AND at least one scene showing it. + internal bool ScoreboardPollsEnabled => + _scoreboardEnabled && Scenes.Any(scene => + scene.Elements.OfType().Any(s => s.IsScoreboard && s.IsVisible && s.IsEnabled)); + internal ScoreboardTiles ScoreboardTilesState => _scoreboardTiles; /// Composited strip frame for the output path (cached by the presenter). @@ -146,7 +168,7 @@ public partial class MainViewModel /// must never touch a WriteableBitmap, so this runs on the UI thread only. private void RefreshScoreboardPreviews() { - var frame = _scoreboardPresenter?.Frame; + var frame = _scoreboardEnabled ? _scoreboardPresenter?.Frame : null; foreach (var scene in Scenes) foreach (var source in scene.Elements.OfType().Where(s => s.IsScoreboard)) { diff --git a/ViewModels/MainViewModel.cs b/ViewModels/MainViewModel.cs index 9d1c85a..cc31cda 100644 --- a/ViewModels/MainViewModel.cs +++ b/ViewModels/MainViewModel.cs @@ -279,6 +279,7 @@ public partial class MainViewModel : ViewModelBase MonoOutput = _layoutStore.LoadMonoOutput(); // applied to the mixer by the setter _musicPlayer.UserGain = (float)Helpers.GainDb.FromDb(TraxDefaultDb); // background by default InitScoreboard(); + ScoreboardEnabled = _layoutStore.LoadScoreboardEnabled(); try { var s = _layoutStore.GetSetting("ZoomShape"); diff --git a/ytLive.Tests/ScoreboardCadenceTests.cs b/ytLive.Tests/ScoreboardCadenceTests.cs index cc51ba7..0be472c 100644 --- a/ytLive.Tests/ScoreboardCadenceTests.cs +++ b/ytLive.Tests/ScoreboardCadenceTests.cs @@ -32,6 +32,11 @@ public class ScoreboardCadenceTests Assert.Equal(ScoreboardPoll.None, ScoreboardCadence.Due(SessionPhase.Live, old, now.AddSeconds(-10), now)); + // The master switch beats every other rule: off means silent, whatever the session is. + Assert.Equal(ScoreboardPoll.None, ScoreboardCadence.Due(SessionPhase.Live, old, old, now, enabled: false)); + Assert.Equal(ScoreboardPoll.None, ScoreboardCadence.Due(SessionPhase.Test, old, old, now, enabled: false)); + Assert.Equal(ScoreboardPoll.None, ScoreboardCadence.Due(SessionPhase.Recording, old, old, now, enabled: false)); + // Subscribers alone mid-window. Assert.Equal(ScoreboardPoll.SubscribersOnly, ScoreboardCadence.Due(SessionPhase.Live, now, old, now)); diff --git a/ytLive.Tests/ScoreboardPropertiesTests.cs b/ytLive.Tests/ScoreboardPropertiesTests.cs index 899e0de..69fa007 100644 --- a/ytLive.Tests/ScoreboardPropertiesTests.cs +++ b/ytLive.Tests/ScoreboardPropertiesTests.cs @@ -17,6 +17,9 @@ public class ScoreboardPropertiesTests // Gated on the selected layer being a scoreboard. Assert.Contains("SelectedElement.IsScoreboard", panel); + // The master switch sits in the same panel: off hides the strip and stops the polling. + Assert.Contains("ScoreboardEnabled", panel); + // The eight fact switches. foreach (var binding in new[] { @@ -48,6 +51,7 @@ public class ScoreboardPropertiesTests { "ScoreboardTextColor", "ScoreboardBackgroundColor", "ScoreboardBackgroundOpacity", "ScoreboardFontSize", "ScoreboardColorOptions", "ScoreboardShowSubscribers", + "ScoreboardEnabled", }) Assert.NotNull(type.GetProperty(name)); }