using System; using System.Collections.Generic; using System.Globalization; namespace ytLive.Models; /// Which of the eight facts the creator put on screen, and how each renders. /// /// Every tile is optional because the strip is an ego wall — the creator chooses the /// billboard. Goal tiles carry their targets here so the rendering stays a pure function of the /// state plus this selection. public sealed class ScoreboardTiles { /// Nothing enabled — the layer draws nothing. public static ScoreboardTiles None => new(); /// The selection a new layer starts with: the six session facts, no channel polls /// (those need the Analytics scope). public static ScoreboardTiles Default => new() { SuperChats = true, NewMembers = true, Milestones = true, LastMember = true, LastSuperChat = true, LastGift = true, }; public bool Subscribers { get; set; } public bool WatchHours { get; set; } public bool SuperChats { get; set; } public bool NewMembers { get; set; } public bool Milestones { get; set; } public bool LastMember { get; set; } public bool LastSuperChat { get; set; } public bool LastGift { get; set; } /// Goal targets for the two polled tiles; 0 means "no goal, show the raw number". public int SubscriberGoal { get; set; } public int WatchHourGoal { get; set; } /// Channel state fed by the polled side (subscribers, watch hours). Kept here so the /// strip is built from ONE source of truth: tallies + channel values + this selection. public int SubscriberCount { get; set; } public int WatchHoursValue { get; set; } /// Cache key: the selection alone. Values are folded in by the presenter's fingerprint, /// so this only needs to change when the ENABLED SET changes. public string CacheKey => $"{Subscribers}{WatchHours}{SuperChats}{NewMembers}{Milestones}{LastMember}{LastSuperChat}{LastGift}" + $"{SubscriberGoal}{WatchHourGoal}"; /// The strip's text, in reading order. Empty strings are dropped by the renderer. public IReadOnlyList Build(ScoreboardState state) { var tiles = new List(8); if (Subscribers) tiles.Add(SubscriberGoal > 0 ? $"Subs {SubscriberCount:N0}/{SubscriberGoal:N0}" : $"Subs {SubscriberCount:N0}"); if (WatchHours) // Analytics is a DAILY aggregate, so the tile must not imply live data. tiles.Add(WatchHourGoal > 0 ? $"Hours {WatchHoursValue:N0}/{WatchHourGoal:N0} (as of {DateTime.Now:MMM d})" : $"Hours {WatchHoursValue:N0} (as of {DateTime.Now:MMM d})"); if (SuperChats && state.SessionTotal.Total > 0) tiles.Add(state.HasMixedCurrencies ? $"SC {state.SessionTotal.Currency} {state.SessionTotal.Total:0.00} +{state.MoneyByCurrency.Count - 1}" : $"SC {state.SessionTotal.Currency} {state.SessionTotal.Total:0.00}"); if (NewMembers && state.NewMembers > 0) tiles.Add($"Members +{state.NewMembers}"); if (Milestones && state.MilestoneCount > 0) tiles.Add($"Milestones {state.MilestoneCount}"); if (LastMember && !string.IsNullOrWhiteSpace(state.LastMemberName)) tiles.Add($"Last member {state.LastMemberName}"); if (LastSuperChat && !string.IsNullOrWhiteSpace(state.LastSuperChatDisplay)) tiles.Add($"Last SC {state.LastSuperChatDisplay}"); if (LastGift && !string.IsNullOrWhiteSpace(state.LastGifterName)) tiles.Add($"Last gift {state.LastGifterName} ×{Math.Max(1, state.LastGiftCount)}"); return tiles; } }