using System.Text.Json.Serialization; namespace Lumi.Companion.Overlay; public enum OverlayVisibilityMode { Off, WhenLive, On } public enum OverlayLayoutMode { Unified, DividedHorizontal, DividedVertical, Separate } public enum OverlayAnchor { TopLeft, Top, TopRight, Left, Right, BottomLeft, Bottom, BottomRight } public enum OverlayHorizontalAlignment { Left, Middle, Right } public enum OverlayVerticalAlignment { Top, Middle, Bottom } public enum OverlayNewestDirection { Start, End } public enum OverlayAnimation { None, Fade, Slide, FadeSlide, ScaleFade } public sealed class OverlaySourceSetting { public string Id { get; set; } = ""; public string Platform { get; set; } = ""; public string Label { get; set; } = ""; public bool Selected { get; set; } } public sealed class OverlayContainerSettings { public OverlayAnchor Anchor { get; set; } = OverlayAnchor.BottomLeft; public int PaddingTop { get; set; } = 24; public int PaddingRight { get; set; } = 24; public int PaddingBottom { get; set; } = 24; public int PaddingLeft { get; set; } = 24; public int Width { get; set; } = 700; public int Height { get; set; } = 600; public OverlayHorizontalAlignment HorizontalAlignment { get; set; } = OverlayHorizontalAlignment.Left; public OverlayVerticalAlignment VerticalAlignment { get; set; } = OverlayVerticalAlignment.Bottom; public OverlayNewestDirection NewestDirection { get; set; } = OverlayNewestDirection.End; public string CardBackground { get; set; } = "#12121266"; public string TextColor { get; set; } = "#bbbbbb"; public string FontFamily { get; set; } = "Segoe UI"; public double FontSize { get; set; } = 22; public string TextShadow { get; set; } = "#000000aa"; public int ShadowX { get; set; } = 1; public int ShadowY { get; set; } = 2; public int ShadowBlur { get; set; } = 3; public bool ShowAvatar { get; set; } = true; public bool ShowUsername { get; set; } = true; public bool ShowPlatform { get; set; } = true; public OverlayAnimation EntryAnimation { get; set; } = OverlayAnimation.FadeSlide; public OverlayAnimation ExitAnimation { get; set; } = OverlayAnimation.FadeSlide; public int EntryDurationMs { get; set; } = 260; public int ExitDurationMs { get; set; } = 220; public string Easing { get; set; } = "cubic-out"; public int SlideDistance { get; set; } = 24; public double ScaleAmount { get; set; } = 0.92; public int LifetimeMs { get; set; } = 12000; public int QueueLimit { get; set; } = 12; public OverlayContainerSettings CreateCopy() => new() { Anchor = Anchor, PaddingTop = PaddingTop, PaddingRight = PaddingRight, PaddingBottom = PaddingBottom, PaddingLeft = PaddingLeft, Width = Width, Height = Height, HorizontalAlignment = HorizontalAlignment, VerticalAlignment = VerticalAlignment, NewestDirection = NewestDirection, CardBackground = CardBackground, TextColor = TextColor, FontFamily = FontFamily, FontSize = FontSize, TextShadow = TextShadow, ShadowX = ShadowX, ShadowY = ShadowY, ShadowBlur = ShadowBlur, ShowAvatar = ShowAvatar, ShowUsername = ShowUsername, ShowPlatform = ShowPlatform, EntryAnimation = EntryAnimation, ExitAnimation = ExitAnimation, EntryDurationMs = EntryDurationMs, ExitDurationMs = ExitDurationMs, Easing = Easing, SlideDistance = SlideDistance, ScaleAmount = ScaleAmount, LifetimeMs = LifetimeMs, QueueLimit = QueueLimit }; } public sealed class LumiOverlaySettings { public int SchemaVersion { get; set; } = 1; public string ActiveProfileId { get; set; } = "default"; public bool Enabled { get; set; } = true; public OverlayVisibilityMode Visibility { get; set; } = OverlayVisibilityMode.On; public string? MonitorId { get; set; } public List Sources { get; set; } = []; public HashSet EventTypes { get; set; } = []; public OverlayLayoutMode Layout { get; set; } = OverlayLayoutMode.Unified; public int ChatPercentage { get; set; } = 70; public OverlayContainerSettings Unified { get; set; } = new(); public OverlayContainerSettings Chat { get; set; } = new(); public OverlayContainerSettings Events { get; set; } = new() { Anchor = OverlayAnchor.TopRight, Width = 520, Height = 420 }; public string ToggleVisibilityHotkey { get; set; } = "Ctrl+Alt+L"; public string TogglePreviewHotkey { get; set; } = "Ctrl+Alt+P"; [JsonIgnore] public IEnumerable SelectedSourceIds => Sources.Where(source => source.Selected).Select(source => source.Id); public LumiOverlaySettings CreateCopy() => new() { SchemaVersion = SchemaVersion, ActiveProfileId = ActiveProfileId, Enabled = Enabled, Visibility = Visibility, MonitorId = MonitorId, Sources = Sources.Select(source => new OverlaySourceSetting { Id = source.Id, Platform = source.Platform, Label = source.Label, Selected = source.Selected }).ToList(), EventTypes = EventTypes.ToHashSet(StringComparer.Ordinal), Layout = Layout, ChatPercentage = ChatPercentage, Unified = Unified.CreateCopy(), Chat = Chat.CreateCopy(), Events = Events.CreateCopy(), ToggleVisibilityHotkey = ToggleVisibilityHotkey, TogglePreviewHotkey = TogglePreviewHotkey }; public void Normalize() { SchemaVersion = 1; ActiveProfileId = Clean(ActiveProfileId, 80, "default"); MonitorId = string.IsNullOrWhiteSpace(MonitorId) ? null : MonitorId.Trim()[..Math.Min(MonitorId.Trim().Length, 300)]; Sources = Sources.Where(source => !string.IsNullOrWhiteSpace(source.Id)) .GroupBy(source => source.Id, StringComparer.Ordinal).Select(group => group.First()).Take(250).ToList(); foreach (var source in Sources) { source.Id = Clean(source.Id, 300, ""); source.Platform = Clean(source.Platform, 24, ""); source.Label = Clean(source.Label, 300, source.Id); } EventTypes = EventTypes.Where(value => !string.IsNullOrWhiteSpace(value)).Select(value => Clean(value, 120, "")).Take(100).ToHashSet(); ChatPercentage = Math.Clamp(ChatPercentage, 10, 90); NormalizeContainer(Unified); NormalizeContainer(Chat); NormalizeContainer(Events); ToggleVisibilityHotkey = Clean(ToggleVisibilityHotkey, 80, "Ctrl+Alt+L"); TogglePreviewHotkey = Clean(TogglePreviewHotkey, 80, "Ctrl+Alt+P"); } private static void NormalizeContainer(OverlayContainerSettings value) { value.Width = Math.Clamp(value.Width, 160, 16000); value.Height = Math.Clamp(value.Height, 100, 16000); value.PaddingTop = Math.Clamp(value.PaddingTop, 0, 4000); value.PaddingRight = Math.Clamp(value.PaddingRight, 0, 4000); value.PaddingBottom = Math.Clamp(value.PaddingBottom, 0, 4000); value.PaddingLeft = Math.Clamp(value.PaddingLeft, 0, 4000); AnchorLayout.ClampPadding(value); value.CardBackground = ColorValue(value.CardBackground, true, "#12121266"); value.TextColor = ColorValue(value.TextColor, false, "#bbbbbb"); value.TextShadow = ColorValue(value.TextShadow, true, "#000000aa"); value.FontFamily = Clean(value.FontFamily, 120, "Segoe UI"); value.FontSize = Math.Clamp(value.FontSize, 8, 120); value.ShadowX = Math.Clamp(value.ShadowX, -40, 40); value.ShadowY = Math.Clamp(value.ShadowY, -40, 40); value.ShadowBlur = Math.Clamp(value.ShadowBlur, 0, 40); value.EntryDurationMs = Math.Clamp(value.EntryDurationMs, 0, 5000); value.ExitDurationMs = Math.Clamp(value.ExitDurationMs, 0, 5000); value.SlideDistance = Math.Clamp(value.SlideDistance, 0, 1000); value.ScaleAmount = Math.Clamp(value.ScaleAmount, 0.1, 2); value.LifetimeMs = Math.Clamp(value.LifetimeMs, 1000, 300000); value.QueueLimit = Math.Clamp(value.QueueLimit, 1, 100); value.Easing = Clean(value.Easing, 40, "cubic-out"); } private static string Clean(string? value, int max, string fallback) { var clean = (value ?? "").Trim(); return clean.Length == 0 ? fallback : clean[..Math.Min(clean.Length, max)]; } private static string ColorValue(string? value, bool alpha, string fallback) => System.Text.RegularExpressions.Regex.IsMatch(value ?? "", alpha ? "^#[0-9a-fA-F]{8}$" : "^#[0-9a-fA-F]{6}$") ? value! : fallback; } public sealed record OverlaySource(string Id, string Platform, string Label, bool? Live, bool Available, string Status); public sealed record OverlayEventType(string Id, string Label, string Platform);