using Dalamud.Bindings.ImGui; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Utility; using LightlessSync.LightlessConfiguration; using LightlessSync.Services; using LightlessSync.Services.Mediator; using Microsoft.Extensions.Logging; using System.IO; using System.Numerics; using System.Reflection; using System.Text; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; using Dalamud.Interface; namespace LightlessSync.UI; public class UpdateNotesUi : WindowMediatorSubscriberBase { private readonly LightlessConfigService _configService; private readonly UiSharedService _uiShared; private readonly List _contributors = []; private readonly List _credits = []; private readonly List _supporters = []; private ChangelogFile _changelog = new(); private bool _scrollToTop; private int _selectedTab; // Particle system for visual effects private struct Particle { public Vector2 Position; public Vector2 Velocity; public float Life; public float MaxLife; public float Size; public Vector4 Color; public ParticleType Type; public float Rotation; public float RotationSpeed; public List? Trail; public bool IsLargeMoon; } private enum ParticleType { Star, Moon, Sparkle, FastFallingStar } private readonly List _particles = []; private float _particleTimer; private readonly Random _particleRandom = new(); private Particle? _largeMoon; private float _largeMoonTimer; public UpdateNotesUi(ILogger logger, LightlessMediator mediator, UiSharedService uiShared, LightlessConfigService configService, PerformanceCollectorService performanceCollectorService) : base(logger, mediator, "Lightless Sync — Update Notes", performanceCollectorService) { _configService = configService; _uiShared = uiShared; AllowClickthrough = false; AllowPinning = false; RespectCloseHotkey = true; ShowCloseButton = true; Flags = ImGuiWindowFlags.NoSavedSettings | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoTitleBar; SizeConstraints = new WindowSizeConstraints() { MinimumSize = new Vector2(800, 700), MaximumSize = new Vector2(800, 700), }; LoadEmbeddedResources(); } public override void OnOpen() { _scrollToTop = true; } protected override void DrawInternal() { if (_uiShared.IsInGpose) return; DrawHeader(); ImGuiHelpers.ScaledDummy(6); DrawChangelog(); DrawCloseButton(); } private void DrawHeader() { var windowPos = ImGui.GetWindowPos(); var windowPadding = ImGui.GetStyle().WindowPadding; var headerWidth = (800f * ImGuiHelpers.GlobalScale) - (windowPadding.X * 2); var headerHeight = 140f * ImGuiHelpers.GlobalScale; var headerStart = windowPos + new Vector2(windowPadding.X, windowPadding.Y); var headerEnd = headerStart + new Vector2(headerWidth, headerHeight); DrawGradientBackground(headerStart, headerEnd); DrawParticleEffects(headerStart, new Vector2(headerWidth, headerHeight)); DrawHeaderText(headerStart); DrawHeaderButtons(headerStart, headerWidth); ImGui.SetCursorPosY(windowPadding.Y + headerHeight + 5); // Version badge with icon ImGui.SetCursorPosX(12); using (ImRaii.PushFont(UiBuilder.IconFont)) { ImGui.TextColored(UIColors.Get("LightlessGreen"), FontAwesomeIcon.Star.ToIconString()); } ImGui.SameLine(); ImGui.TextColored(UIColors.Get("LightlessGreen"), "What's New"); if (!string.IsNullOrEmpty(_changelog.Tagline)) { ImGui.SameLine(); ImGui.SetCursorPosX(ImGui.GetCursorPosX() + 10); ImGui.TextColored(new Vector4(0.75f, 0.75f, 0.85f, 1.0f), _changelog.Tagline); if (!string.IsNullOrEmpty(_changelog.Subline)) { ImGui.SameLine(); ImGui.TextColored(new Vector4(0.65f, 0.65f, 0.75f, 1.0f), $" – {_changelog.Subline}"); } } ImGui.Separator(); ImGuiHelpers.ScaledDummy(3); } private void DrawGradientBackground(Vector2 headerStart, Vector2 headerEnd) { var drawList = ImGui.GetWindowDrawList(); // Dark night sky background with stars pattern var darkPurple = new Vector4(0.08f, 0.05f, 0.15f, 1.0f); var deepPurple = new Vector4(0.12f, 0.08f, 0.20f, 1.0f); drawList.AddRectFilledMultiColor( headerStart, headerEnd, ImGui.GetColorU32(darkPurple), ImGui.GetColorU32(darkPurple), ImGui.GetColorU32(deepPurple), ImGui.GetColorU32(deepPurple) ); // Add some static "distant stars" for depth var random = new Random(42); // Fixed seed for consistent pattern for (int i = 0; i < 50; i++) { var starPos = headerStart + new Vector2( (float)random.NextDouble() * (headerEnd.X - headerStart.X), (float)random.NextDouble() * (headerEnd.Y - headerStart.Y) ); var brightness = 0.3f + (float)random.NextDouble() * 0.4f; drawList.AddCircleFilled(starPos, 1f, ImGui.GetColorU32(new Vector4(1f, 1f, 1f, brightness))); } // Accent border at bottom with glow drawList.AddLine( new Vector2(headerStart.X, headerEnd.Y), headerEnd, ImGui.GetColorU32(UIColors.Get("LightlessPurple")), 2f ); } private void DrawHeaderText(Vector2 headerStart) { // Title text overlay - drawn after particles so it's on top ImGui.SetCursorScreenPos(headerStart + new Vector2(20, 30)); using (_uiShared.UidFont.Push()) { ImGui.TextColored(new Vector4(0.95f, 0.95f, 0.95f, 1.0f), "Lightless Sync"); } ImGui.SetCursorScreenPos(headerStart + new Vector2(20, 75)); ImGui.TextColored(UIColors.Get("LightlessBlue"), "Update Notes"); } private void DrawHeaderButtons(Vector2 headerStart, float headerWidth) { var buttonSize = _uiShared.GetIconButtonSize(FontAwesomeIcon.Globe); var spacing = 8f * ImGuiHelpers.GlobalScale; var rightPadding = 15f * ImGuiHelpers.GlobalScale; var topPadding = 15f * ImGuiHelpers.GlobalScale; // Position for buttons in top right var buttonY = headerStart.Y + topPadding; var gitButtonX = headerStart.X + headerWidth - rightPadding - buttonSize.X; var discordButtonX = gitButtonX - buttonSize.X - spacing; ImGui.SetCursorScreenPos(new Vector2(discordButtonX, buttonY)); using (ImRaii.PushColor(ImGuiCol.Button, new Vector4(0, 0, 0, 0))) using (ImRaii.PushColor(ImGuiCol.ButtonHovered, UIColors.Get("LightlessPurple") with { W = 0.3f })) using (ImRaii.PushColor(ImGuiCol.ButtonActive, UIColors.Get("LightlessPurpleActive") with { W = 0.5f })) { if (_uiShared.IconButton(FontAwesomeIcon.Comments)) { Util.OpenLink("https://discord.gg/dsbjcXMnhA"); } if (ImGui.IsItemHovered()) { ImGui.SetTooltip("Join our Discord"); } ImGui.SetCursorScreenPos(new Vector2(gitButtonX, buttonY)); if (_uiShared.IconButton(FontAwesomeIcon.Code)) { Util.OpenLink("https://git.lightless-sync.org/Lightless-Sync"); } if (ImGui.IsItemHovered()) { ImGui.SetTooltip("View on Git"); } } } private void DrawParticleEffects(Vector2 bannerStart, Vector2 bannerSize) { var deltaTime = ImGui.GetIO().DeltaTime; _particleTimer += deltaTime; _largeMoonTimer += deltaTime; // Spawn new particles if (_particleTimer > 0.3f && _particles.Count < 30) { SpawnParticle(bannerStart, bannerSize); _particleTimer = 0f; } // Spawn or update large moon if (_largeMoon == null || _largeMoonTimer > 45f) { SpawnLargeMoon(bannerStart, bannerSize); _largeMoonTimer = 0f; } var drawList = ImGui.GetWindowDrawList(); // Update and draw large moon first (background layer) if (_largeMoon != null) { var moon = _largeMoon.Value; moon.Position += moon.Velocity * deltaTime; moon.Life -= deltaTime; // Keep moon within banner bounds with padding var padding = moon.Size + 10; if (moon.Life <= 0 || moon.Position.X < bannerStart.X - padding || moon.Position.X > bannerStart.X + bannerSize.X + padding || moon.Position.Y < bannerStart.Y - padding || moon.Position.Y > bannerStart.Y + bannerSize.Y + padding) { _largeMoon = null; } else { float alpha = Math.Min(1f, moon.Life / moon.MaxLife); var color = moon.Color with { W = moon.Color.W * alpha }; DrawMoon(drawList, moon.Position, moon.Size, color); _largeMoon = moon; } } // Update and draw regular particles for (int i = _particles.Count - 1; i >= 0; i--) { var particle = _particles[i]; // Update trail for stars if ((particle.Type == ParticleType.Star || particle.Type == ParticleType.FastFallingStar) && particle.Trail != null) { particle.Trail.Insert(0, particle.Position); var maxTrailLength = particle.Type == ParticleType.FastFallingStar ? 15 : 8; if (particle.Trail.Count > maxTrailLength) particle.Trail.RemoveAt(particle.Trail.Count - 1); } particle.Position += particle.Velocity * deltaTime; particle.Life -= deltaTime; particle.Rotation += particle.RotationSpeed * deltaTime; if (particle.Life <= 0 || particle.Position.X > bannerStart.X + bannerSize.X + 50 || particle.Position.X < bannerStart.X - 50 || particle.Position.Y < bannerStart.Y - 50 || particle.Position.Y > bannerStart.Y + bannerSize.Y + 50) { _particles.RemoveAt(i); continue; } float alpha = Math.Min(1f, particle.Life / particle.MaxLife); var color = particle.Color with { W = particle.Color.W * alpha }; // Draw trail for stars if ((particle.Type == ParticleType.Star || particle.Type == ParticleType.FastFallingStar) && particle.Trail != null && particle.Trail.Count > 1) { for (int t = 1; t < particle.Trail.Count; t++) { float trailAlpha = alpha * (1f - (t / (float)particle.Trail.Count)) * (particle.Type == ParticleType.FastFallingStar ? 0.7f : 0.5f); var trailColor = color with { W = trailAlpha }; float thickness = particle.Type == ParticleType.FastFallingStar ? 2.5f : 1.5f; drawList.AddLine( particle.Trail[t - 1], particle.Trail[t], ImGui.GetColorU32(trailColor), thickness ); } } // Draw based on particle type switch (particle.Type) { case ParticleType.Star: case ParticleType.FastFallingStar: DrawStar(drawList, particle.Position, particle.Size, color, particle.Rotation); break; case ParticleType.Moon: DrawMoon(drawList, particle.Position, particle.Size, color); break; case ParticleType.Sparkle: DrawSparkle(drawList, particle.Position, particle.Size, color, particle.Rotation); break; } _particles[i] = particle; } } private void DrawStar(ImDrawListPtr drawList, Vector2 position, float size, Vector4 color, float rotation) { // Draw a 5-pointed star var points = new Vector2[10]; for (int i = 0; i < 10; i++) { float angle = (i * MathF.PI / 5) + rotation; float radius = (i % 2 == 0) ? size : size * 0.4f; points[i] = position + new Vector2(MathF.Cos(angle) * radius, MathF.Sin(angle) * radius); } // Draw filled star for (int i = 0; i < 5; i++) { drawList.AddTriangleFilled( position, points[i * 2], points[(i * 2 + 2) % 10], ImGui.GetColorU32(color) ); } // Glow effect var glowColor = color with { W = color.W * 0.3f }; drawList.AddCircleFilled(position, size * 1.5f, ImGui.GetColorU32(glowColor)); } private void DrawMoon(ImDrawListPtr drawList, Vector2 position, float size, Vector4 color) { // Enhanced glow for larger moons var glowRadius = size > 15f ? 2.5f : 1.8f; var glowColor = color with { W = color.W * (size > 15f ? 0.15f : 0.25f) }; drawList.AddCircleFilled(position, size * glowRadius, ImGui.GetColorU32(glowColor)); // Draw crescent moon drawList.AddCircleFilled(position, size, ImGui.GetColorU32(color)); // Draw shadow circle to create crescent var shadowColor = new Vector4(0.08f, 0.05f, 0.15f, 1.0f); drawList.AddCircleFilled(position + new Vector2(size * 0.4f, 0), size * 0.8f, ImGui.GetColorU32(shadowColor)); // Additional glow layer for large moons if (size > 15f) { var outerGlow = color with { W = color.W * 0.08f }; drawList.AddCircleFilled(position, size * 3.5f, ImGui.GetColorU32(outerGlow)); } } private void DrawSparkle(ImDrawListPtr drawList, Vector2 position, float size, Vector4 color, float rotation) { // Draw a 4-pointed sparkle (plus shape) var thickness = size * 0.3f; // Horizontal line drawList.AddLine( position + new Vector2(-size, 0), position + new Vector2(size, 0), ImGui.GetColorU32(color), thickness ); // Vertical line drawList.AddLine( position + new Vector2(0, -size), position + new Vector2(0, size), ImGui.GetColorU32(color), thickness ); // Center glow drawList.AddCircleFilled(position, size * 0.4f, ImGui.GetColorU32(color)); var glowColor = color with { W = color.W * 0.4f }; drawList.AddCircleFilled(position, size * 1.2f, ImGui.GetColorU32(glowColor)); } private void SpawnParticle(Vector2 bannerStart, Vector2 bannerSize) { var typeRoll = _particleRandom.Next(100); var particleType = typeRoll switch { < 35 => ParticleType.Star, < 50 => ParticleType.Moon, < 65 => ParticleType.FastFallingStar, _ => ParticleType.Sparkle }; Vector2 position; Vector2 velocity; // Stars: spawn from top, move diagonally down if (particleType == ParticleType.Star) { // Spawn from top edge position = new Vector2( bannerStart.X + (float)_particleRandom.NextDouble() * bannerSize.X, bannerStart.Y - 10 ); // Move diagonally down (shooting star effect) var angle = MathF.PI * 0.25f + (float)(_particleRandom.NextDouble() - 0.5) * 0.5f; // 45° ± variation var speed = 30f + (float)_particleRandom.NextDouble() * 40f; velocity = new Vector2(MathF.Cos(angle) * speed, MathF.Sin(angle) * speed); } // Fast falling stars: spawn from top, fall straight down very fast else if (particleType == ParticleType.FastFallingStar) { // Spawn from top edge, random X position position = new Vector2( bannerStart.X + (float)_particleRandom.NextDouble() * bannerSize.X, bannerStart.Y - 10 ); // Fall almost straight down with slight horizontal drift var horizontalDrift = -10f + (float)_particleRandom.NextDouble() * 20f; var speed = 120f + (float)_particleRandom.NextDouble() * 80f; // Much faster! velocity = new Vector2(horizontalDrift, speed); } // Moons: drift slowly across else if (particleType == ParticleType.Moon) { // Spawn from left side position = new Vector2( bannerStart.X - 10, bannerStart.Y + (float)_particleRandom.NextDouble() * bannerSize.Y ); // Drift slowly to the right with slight vertical movement velocity = new Vector2( 15f + (float)_particleRandom.NextDouble() * 10f, -5f + (float)_particleRandom.NextDouble() * 10f ); } // Sparkles: float gently else { position = new Vector2( bannerStart.X + (float)_particleRandom.NextDouble() * bannerSize.X, bannerStart.Y + (float)_particleRandom.NextDouble() * bannerSize.Y ); velocity = new Vector2( -5f + (float)_particleRandom.NextDouble() * 10f, -5f + (float)_particleRandom.NextDouble() * 10f ); } var particle = new Particle { Position = position, Velocity = velocity, MaxLife = particleType switch { ParticleType.Star => 3f + (float)_particleRandom.NextDouble() * 2f, ParticleType.Moon => 8f + (float)_particleRandom.NextDouble() * 4f, ParticleType.FastFallingStar => 1.5f + (float)_particleRandom.NextDouble() * 1f, _ => 6f + (float)_particleRandom.NextDouble() * 4f }, Size = particleType switch { ParticleType.Star => 2.5f + (float)_particleRandom.NextDouble() * 2f, ParticleType.Moon => 3f + (float)_particleRandom.NextDouble() * 2f, ParticleType.FastFallingStar => 3f + (float)_particleRandom.NextDouble() * 2f, _ => 2f + (float)_particleRandom.NextDouble() * 2f }, Color = particleType switch { ParticleType.Star => new Vector4(1.0f, 1.0f, 0.9f, 0.9f), ParticleType.Moon => UIColors.Get("LightlessBlue") with { W = 0.7f }, ParticleType.FastFallingStar => new Vector4(1.0f, 0.95f, 0.85f, 1.0f), // Bright white-yellow _ => UIColors.Get("LightlessPurple") with { W = 0.8f } }, Type = particleType, Rotation = (float)_particleRandom.NextDouble() * MathF.PI * 2, RotationSpeed = particleType == ParticleType.Star || particleType == ParticleType.FastFallingStar ? 2f : -0.5f + (float)_particleRandom.NextDouble() * 1f, Trail = particleType == ParticleType.Star || particleType == ParticleType.FastFallingStar ? new List() : null, IsLargeMoon = false }; particle.Life = particle.MaxLife; _particles.Add(particle); } private void SpawnLargeMoon(Vector2 bannerStart, Vector2 bannerSize) { // Large moon travels across the banner like a celestial body var spawnSide = _particleRandom.Next(4); Vector2 position; Vector2 velocity; switch (spawnSide) { case 0: // Spawn from left, move to right position = new Vector2( bannerStart.X - 50, bannerStart.Y + (float)_particleRandom.NextDouble() * bannerSize.Y ); velocity = new Vector2( 15f + (float)_particleRandom.NextDouble() * 10f, -5f + (float)_particleRandom.NextDouble() * 10f ); break; case 1: // Spawn from top, move down and across position = new Vector2( bannerStart.X + (float)_particleRandom.NextDouble() * bannerSize.X, bannerStart.Y - 50 ); velocity = new Vector2( -5f + (float)_particleRandom.NextDouble() * 10f, 10f + (float)_particleRandom.NextDouble() * 8f ); break; case 2: // Spawn from right, move to left position = new Vector2( bannerStart.X + bannerSize.X + 50, bannerStart.Y + (float)_particleRandom.NextDouble() * bannerSize.Y ); velocity = new Vector2( -(15f + (float)_particleRandom.NextDouble() * 10f), -5f + (float)_particleRandom.NextDouble() * 10f ); break; default: // Spawn from top-left corner, move diagonally position = new Vector2( bannerStart.X - 30, bannerStart.Y - 30 ); velocity = new Vector2( 12f + (float)_particleRandom.NextDouble() * 8f, 12f + (float)_particleRandom.NextDouble() * 8f ); break; } _largeMoon = new Particle { Position = position, Velocity = velocity, MaxLife = 40f, Life = 40f, Size = 25f + (float)_particleRandom.NextDouble() * 10f, Color = UIColors.Get("LightlessBlue") with { W = 0.35f }, Type = ParticleType.Moon, Rotation = 0, RotationSpeed = 0, Trail = null, IsLargeMoon = true }; } private void DrawCloseButton() { ImGuiHelpers.ScaledDummy(5); var closeWidth = 200f * ImGuiHelpers.GlobalScale; var closeHeight = 35f * ImGuiHelpers.GlobalScale; ImGui.SetCursorPosX((ImGui.GetWindowSize().X - closeWidth) / 2); using (ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 8f)) using (ImRaii.PushColor(ImGuiCol.Button, UIColors.Get("LightlessPurple"))) using (ImRaii.PushColor(ImGuiCol.ButtonHovered, UIColors.Get("LightlessPurpleActive"))) using (ImRaii.PushColor(ImGuiCol.ButtonActive, UIColors.Get("ButtonDefault"))) { if (ImGui.Button("Got it!", new Vector2(closeWidth, closeHeight))) { IsOpen = false; } } } private void DrawChangelog() { using (ImRaii.PushStyle(ImGuiStyleVar.ChildRounding, 6f)) using (var child = ImRaii.Child("###ll_changelog", new Vector2(0, ImGui.GetContentRegionAvail().Y - 60), false, ImGuiWindowFlags.AlwaysVerticalScrollbar)) { if (!child) return; if (_scrollToTop) { _scrollToTop = false; ImGui.SetScrollHereY(0); } ImGui.PushTextWrapPos(); foreach (var entry in _changelog.Changelog) DrawChangelogEntry(entry); ImGui.PopTextWrapPos(); ImGui.Spacing(); } } private void DrawChangelogEntry(ChangelogEntry entry) { var currentColor = entry.IsCurrent == true ? UIColors.Get("LightlessGreen") : new Vector4(0.75f, 0.75f, 0.85f, 1.0f); bool isOpen; var flags = entry.IsCurrent == true ? ImGuiTreeNodeFlags.DefaultOpen : ImGuiTreeNodeFlags.None; using (ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 4f)) using (ImRaii.PushColor(ImGuiCol.Header, UIColors.Get("ButtonDefault"))) using (ImRaii.PushColor(ImGuiCol.HeaderHovered, UIColors.Get("LightlessPurple"))) using (ImRaii.PushColor(ImGuiCol.HeaderActive, UIColors.Get("LightlessPurpleActive"))) using (ImRaii.PushColor(ImGuiCol.Text, currentColor)) { isOpen = ImGui.CollapsingHeader($" {entry.Name} — {entry.Date} ", flags); } ImGui.SameLine(); ImGui.TextColored(new Vector4(0.75f, 0.75f, 0.85f, 1.0f), $" — {entry.Tagline}"); if (!isOpen) return; ImGuiHelpers.ScaledDummy(8); if (!string.IsNullOrEmpty(entry.Message)) { ImGui.TextWrapped(entry.Message); ImGuiHelpers.ScaledDummy(8); return; } if (entry.Versions != null) { foreach (var version in entry.Versions) { DrawFeatureSection(version.Number, UIColors.Get("LightlessGreen")); foreach (var item in version.Items) { ImGui.BulletText(item); } ImGuiHelpers.ScaledDummy(5); } } ImGuiHelpers.ScaledDummy(8); } private static void DrawFeatureSection(string title, Vector4 accentColor) { var drawList = ImGui.GetWindowDrawList(); var startPos = ImGui.GetCursorScreenPos(); var availableWidth = ImGui.GetContentRegionAvail().X; var backgroundMin = startPos + new Vector2(-8, -4); var backgroundMax = startPos + new Vector2(availableWidth + 8, 28); // Background with subtle gradient var bgColor = new Vector4(0.12f, 0.12f, 0.15f, 0.7f); drawList.AddRectFilled(backgroundMin, backgroundMax, ImGui.GetColorU32(bgColor), 6f); // Accent line on left drawList.AddRectFilled( backgroundMin, backgroundMin + new Vector2(4, backgroundMax.Y - backgroundMin.Y), ImGui.GetColorU32(accentColor), 3f ); // Subtle glow effect var glowColor = accentColor with { W = 0.15f }; drawList.AddRect( backgroundMin, backgroundMax, ImGui.GetColorU32(glowColor), 6f, ImDrawFlags.None, 1.5f ); ImGui.SetCursorPosX(ImGui.GetCursorPosX() + 8); ImGui.Spacing(); ImGui.TextColored(accentColor, title); ImGui.Spacing(); } private void LoadEmbeddedResources() { try { var assembly = Assembly.GetExecutingAssembly(); ReadLines(assembly, "LightlessSync.UI.Changelog.supporters.txt", _supporters); ReadLines(assembly, "LightlessSync.UI.Changelog.contributors.txt", _contributors); ReadLines(assembly, "LightlessSync.UI.Changelog.credits.txt", _credits); using var changelogStream = assembly.GetManifestResourceStream("LightlessSync.UI.Changelog.changelog.yaml"); if (changelogStream != null) { using var reader = new StreamReader(changelogStream, Encoding.UTF8, true, 128); var yaml = reader.ReadToEnd(); var deserializer = new DeserializerBuilder() .WithNamingConvention(CamelCaseNamingConvention.Instance) .IgnoreUnmatchedProperties() .Build(); _changelog = deserializer.Deserialize(yaml) ?? new(); } } catch { // Ignore - window will gracefully render with defaults } } private static void ReadLines(Assembly assembly, string resourceName, List target) { using var stream = assembly.GetManifestResourceStream(resourceName); if (stream == null) return; using var reader = new StreamReader(stream, Encoding.UTF8, true, 128); string? line; while ((line = reader.ReadLine()) != null) { if (!string.IsNullOrWhiteSpace(line)) target.Add(line.Trim()); } } private sealed record ChangelogFile { public string Tagline { get; init; } = string.Empty; public string Subline { get; init; } = string.Empty; public List Changelog { get; init; } = new(); } private sealed record ChangelogEntry { public string Name { get; init; } = string.Empty; public string Date { get; init; } = string.Empty; public string Tagline { get; init; } = string.Empty; public bool? IsCurrent { get; init; } public string? Message { get; init; } public List? Versions { get; init; } } private sealed record ChangelogVersion { public string Number { get; init; } = string.Empty; public List Items { get; init; } = new(); } }