From 534f56a7c4393226bfcea13be664665c730276e6 Mon Sep 17 00:00:00 2001 From: gramps Date: Fri, 30 Oct 2026 14:41:31 -0700 Subject: [PATCH] =?UTF-8?q?feat(scoreboard):=20units=207+8=20=E2=80=94=20t?= =?UTF-8?q?he=20layer's=20properties=20panel=20(facts,=20colours,=20plate,?= =?UTF-8?q?=20size)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Selecting a Scoreboard layer now shows its settings, using the app's own widgets only: - eight fact checkboxes (subscribers, watch hours, Super Chats, new members, milestones, last member, last Super Chat, last gift) - text colour and plate colour via YtComboBox over BorderColorPalette (the app's one palette — TASK 63's rule: a picker IS the app's drop-down, never a hand-built menu) - plate on/off, plate opacity, text size 14-24 Every change pushes through the presenter (SetStyle/SetTiles) and repaints the strip, so the preview and the output follow immediately. One look per layout, stored on the view-model: the scoreboard is a single billboard, not eight independent widgets. Guard: ScoreboardPropertiesTests asserts the panel is gated on SelectedElement.IsScoreboard, binds all eight facts and the four look controls, uses YtComboBox/YtCheckBox only, and that the VM surface exists for each name (a typo would otherwise bind to nothing in silence). --- Controls/LeftPanel.xaml | 55 ++++++++++++++++++ ViewModels/MainViewModel.Scoreboard.cs | 71 +++++++++++++++++++++++ ytLive.Tests/ScoreboardPropertiesTests.cs | 54 +++++++++++++++++ 3 files changed, 180 insertions(+) create mode 100644 ytLive.Tests/ScoreboardPropertiesTests.cs diff --git a/Controls/LeftPanel.xaml b/Controls/LeftPanel.xaml index 6276f79..6a6e8c8 100644 --- a/Controls/LeftPanel.xaml +++ b/Controls/LeftPanel.xaml @@ -447,6 +447,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ViewModels/MainViewModel.Scoreboard.cs b/ViewModels/MainViewModel.Scoreboard.cs index 43433b9..b33cebe 100644 --- a/ViewModels/MainViewModel.Scoreboard.cs +++ b/ViewModels/MainViewModel.Scoreboard.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using System.Windows.Media; @@ -59,6 +60,76 @@ public partial class MainViewModel /// (unit 7) — kept here so the panel binds one object instead of six primitives. internal ScoreboardStyle ScoreboardStyleState => _scoreboardStyle; + // ─── Properties panel surface (per layout: one scoreboard look) ──────────── + + public string ScoreboardTextColor + { + get => _scoreboardStyle.TextColor; + set { _scoreboardStyle = _scoreboardStyle with { TextColor = value }; PushStyle(); } + } + + public bool ScoreboardBackgroundEnabled + { + get => _scoreboardStyle.BackgroundEnabled; + set { _scoreboardStyle = _scoreboardStyle with { BackgroundEnabled = value }; PushStyle(); } + } + + public string ScoreboardBackgroundColor + { + get => _scoreboardStyle.BackgroundColor; + set { _scoreboardStyle = _scoreboardStyle with { BackgroundColor = value }; PushStyle(); } + } + + public double ScoreboardBackgroundOpacity + { + get => _scoreboardStyle.BackgroundOpacity; + set + { + _scoreboardStyle = _scoreboardStyle with { BackgroundOpacity = Math.Clamp(value, 0, 1) }; + PushStyle(); + } + } + + public double ScoreboardFontSize + { + get => _scoreboardStyle.FontSize; + set + { + _scoreboardStyle = _scoreboardStyle with + { + FontSize = Math.Clamp(value, ScoreboardLayout.FloorFontSize, ScoreboardLayout.MaxFontSize), + }; + PushStyle(); + } + } + + /// The colours offered for text and plate: the app's one palette. + public IReadOnlyList ScoreboardColorOptions => BorderColorPalette.All; + + public bool ScoreboardShowSubscribers { get => _scoreboardTiles.Subscribers; set => SetTile(t => t.Subscribers = value); } + public bool ScoreboardShowWatchHours { get => _scoreboardTiles.WatchHours; set => SetTile(t => t.WatchHours = value); } + public bool ScoreboardShowSuperChats { get => _scoreboardTiles.SuperChats; set => SetTile(t => t.SuperChats = value); } + public bool ScoreboardShowNewMembers { get => _scoreboardTiles.NewMembers; set => SetTile(t => t.NewMembers = value); } + public bool ScoreboardShowMilestones { get => _scoreboardTiles.Milestones; set => SetTile(t => t.Milestones = value); } + public bool ScoreboardShowLastMember { get => _scoreboardTiles.LastMember; set => SetTile(t => t.LastMember = value); } + public bool ScoreboardShowLastSuperChat { get => _scoreboardTiles.LastSuperChat; set => SetTile(t => t.LastSuperChat = value); } + public bool ScoreboardShowLastGift { get => _scoreboardTiles.LastGift; set => SetTile(t => t.LastGift = value); } + + private void SetTile(Action change) + { + change(_scoreboardTiles); + _scoreboardPresenter?.SetTiles(_scoreboardTiles); + RefreshScoreboardPreviews(); + OnPropertyChanged(nameof(ScoreboardTilesState)); + } + + private void PushStyle() + { + _scoreboardPresenter?.SetStyle(_scoreboardStyle); + RefreshScoreboardPreviews(); + OnPropertyChanged(nameof(ScoreboardStyleState)); + } + /// Publishes the strip to every scoreboard layer's preview element. The pump thread /// must never touch a WriteableBitmap, so this runs on the UI thread only. private void RefreshScoreboardPreviews() diff --git a/ytLive.Tests/ScoreboardPropertiesTests.cs b/ytLive.Tests/ScoreboardPropertiesTests.cs new file mode 100644 index 0000000..899e0de --- /dev/null +++ b/ytLive.Tests/ScoreboardPropertiesTests.cs @@ -0,0 +1,54 @@ +using System.Reflection; +using Xunit; +using ytLive.ViewModels; + +namespace ytLive.Tests; + +/// The scoreboard's look and its eight fact switches live in the LAYER PROPERTIES, and the +/// panel must use the app's widgets — a hand-painted control is the TASK 58/63 defect, and the +/// guard that catches it is here because the type-based sweep cannot see a bespoke picker. +public class ScoreboardPropertiesTests +{ + [Fact] + public void The_scoreboard_properties_are_themed_controls_bound_to_the_layer_surface() + { + var panel = SourceAssert.Text("Controls", "LeftPanel.xaml"); + + // Gated on the selected layer being a scoreboard. + Assert.Contains("SelectedElement.IsScoreboard", panel); + + // The eight fact switches. + foreach (var binding in new[] + { + "ScoreboardShowSubscribers", "ScoreboardShowWatchHours", "ScoreboardShowSuperChats", + "ScoreboardShowNewMembers", "ScoreboardShowMilestones", "ScoreboardShowLastMember", + "ScoreboardShowLastSuperChat", "ScoreboardShowLastGift", + }) + Assert.Contains(binding, panel); + + // Look controls: text colour, plate colour, plate opacity, text size. + foreach (var binding in new[] + { + "ScoreboardTextColor", "ScoreboardBackgroundColor", + "ScoreboardBackgroundOpacity", "ScoreboardFontSize", + }) + Assert.Contains(binding, panel); + + // Theme widgets only: the colour pickers are the app's drop-down (YtComboBox), never a + // hand-built menu, and the toggles are YtCheckBox. + var block = panel[panel.IndexOf("Scoreboard config", System.StringComparison.Ordinal)..]; + block = block[..block.IndexOf("", System.StringComparison.Ordinal)]; + Assert.Contains("Style=\"{StaticResource YtComboBox}\"", block); + Assert.Contains("Style=\"{StaticResource YtCheckBox}\"", block); + Assert.DoesNotContain("ContextMenu", block); + + // The VM surface exists for every binding above (a typo would bind to nothing silently). + var type = typeof(MainViewModel); + foreach (var name in new[] + { + "ScoreboardTextColor", "ScoreboardBackgroundColor", "ScoreboardBackgroundOpacity", + "ScoreboardFontSize", "ScoreboardColorOptions", "ScoreboardShowSubscribers", + }) + Assert.NotNull(type.GetProperty(name)); + } +}