From d5c12e81c3b36472a842f38ae832110c2546e08d Mon Sep 17 00:00:00 2001 From: choco Date: Wed, 15 Oct 2025 22:59:08 +0200 Subject: [PATCH] banner with particles --- LightlessSync/LightlessPlugin.cs | 2 + LightlessSync/UI/UpdateNotesUi.cs | 742 ++++++++++++++++++++++++------ 2 files changed, 611 insertions(+), 133 deletions(-) diff --git a/LightlessSync/LightlessPlugin.cs b/LightlessSync/LightlessPlugin.cs index c431419..dcc1990 100644 --- a/LightlessSync/LightlessPlugin.cs +++ b/LightlessSync/LightlessPlugin.cs @@ -160,6 +160,8 @@ public class LightlessPlugin : MediatorSubscriberBase, IHostedService var currentVersion = ver == null ? string.Empty : $"{ver.Major}.{ver.Minor}.{ver.Build}"; var lastSeen = _lightlessConfigService.Current.LastSeenVersion ?? string.Empty; Logger?.LogDebug("Last seen version: {lastSeen}, current version: {currentVersion}", lastSeen, currentVersion); + Mediator.Publish(new UiToggleMessage(typeof(UpdateNotesUi))); + if (string.IsNullOrEmpty(lastSeen)) { _lightlessConfigService.Current.LastSeenVersion = currentVersion; diff --git a/LightlessSync/UI/UpdateNotesUi.cs b/LightlessSync/UI/UpdateNotesUi.cs index 57c8ef6..83ac903 100644 --- a/LightlessSync/UI/UpdateNotesUi.cs +++ b/LightlessSync/UI/UpdateNotesUi.cs @@ -12,6 +12,7 @@ using System.Reflection; using System.Text; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; +using Dalamud.Interface; namespace LightlessSync.UI; @@ -27,6 +28,36 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase 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, @@ -43,12 +74,12 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase RespectCloseHotkey = true; ShowCloseButton = true; - Flags = ImGuiWindowFlags.NoSavedSettings | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoCollapse; + Flags = ImGuiWindowFlags.NoSavedSettings | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoTitleBar; SizeConstraints = new WindowSizeConstraints() { - MinimumSize = new Vector2(600, 500), - MaximumSize = new Vector2(900, 2000), + MinimumSize = new Vector2(800, 700), + MaximumSize = new Vector2(800, 700), }; LoadEmbeddedResources(); @@ -65,118 +96,577 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase return; DrawHeader(); - DrawLinkButtons(); ImGuiHelpers.ScaledDummy(6); - DrawTabs(); + DrawChangelog(); DrawCloseButton(); } private void DrawHeader() { - using (_uiShared.UidFont.Push()) + 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.TextUnformatted("Lightless Sync"); + ImGui.TextColored(UIColors.Get("LightlessGreen"), FontAwesomeIcon.Star.ToIconString()); } - - _uiShared.ColoredSeparator(UIColors.Get("LightlessBlue"), thickness: 2f); - + ImGui.SameLine(); + + ImGui.TextColored(UIColors.Get("LightlessGreen"), "What's New"); + if (!string.IsNullOrEmpty(_changelog.Tagline)) { - _uiShared.MediumText(_changelog.Tagline, UIColors.Get("LightlessBlue")); + 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(.75f, .75f, .85f, 1f), $" – {_changelog.Subline}"); + ImGui.TextColored(new Vector4(0.65f, 0.65f, 0.75f, 1.0f), $" – {_changelog.Subline}"); } } - - ImGuiHelpers.ScaledDummy(5); + + ImGui.Separator(); + ImGuiHelpers.ScaledDummy(3); } - - private void DrawLinkButtons() + + private void DrawGradientBackground(Vector2 headerStart, Vector2 headerEnd) { - var segmentSize = ImGui.GetWindowSize().X / 4.2f; - var buttonSize = new Vector2(segmentSize, ImGui.GetTextLineHeight() * 1.6f); - - if (ImGui.Button("Discord", buttonSize)) - Util.OpenLink("https://discord.gg/dsbjcXMnhA"); - ImGui.SameLine(); - - if (ImGui.Button("GitHub", buttonSize)) - Util.OpenLink("https://github.com/Light-Public-Syncshells/LightlessSync"); - ImGui.SameLine(); - - if (ImGui.Button("Ko-fi", buttonSize)) - Util.OpenLink("https://ko-fi.com/lightlesssync"); - ImGui.SameLine(); - - if (ImGui.Button("More Links", buttonSize)) - Util.OpenLink("https://lightless.link"); + 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() { - var closeWidth = 300f * ImGuiHelpers.GlobalScale; + ImGuiHelpers.ScaledDummy(5); + + var closeWidth = 200f * ImGuiHelpers.GlobalScale; + var closeHeight = 35f * ImGuiHelpers.GlobalScale; ImGui.SetCursorPosX((ImGui.GetWindowSize().X - closeWidth) / 2); - if (ImGui.Button("Close", new Vector2(closeWidth, 0))) + + 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"))) { - IsOpen = false; - } - } - - private void DrawTabs() - { - using var tabBar = ImRaii.TabBar("lightless_update_tabs"); - if (!tabBar) - return; - - using (var changelogTab = ImRaii.TabItem(" Changelog ")) - { - if (changelogTab) + if (ImGui.Button("Got it!", new Vector2(closeWidth, closeHeight))) { - _selectedTab = 0; - DrawChangelog(); - } - } - - using (var creditsTab = ImRaii.TabItem(" Supporters & Credits ")) - { - if (creditsTab) - { - _selectedTab = 1; - DrawCredits(); + IsOpen = false; } } } - private void DrawChangelog() { - using var child = ImRaii.Child("###ll_changelog", new Vector2(0, ImGui.GetContentRegionAvail().Y - 44), false); - if (!child) - return; - - if (_scrollToTop) + using (ImRaii.PushStyle(ImGuiStyleVar.ChildRounding, 6f)) + using (var child = ImRaii.Child("###ll_changelog", new Vector2(0, ImGui.GetContentRegionAvail().Y - 60), false, ImGuiWindowFlags.AlwaysVerticalScrollbar)) { - _scrollToTop = false; - ImGui.SetScrollHereY(0); + if (!child) + return; + + if (_scrollToTop) + { + _scrollToTop = false; + ImGui.SetScrollHereY(0); + } + + ImGui.PushTextWrapPos(); + + foreach (var entry in _changelog.Changelog) + DrawChangelogEntry(entry); + + ImGui.PopTextWrapPos(); + ImGui.Spacing(); } - - foreach (var entry in _changelog.Changelog) - DrawChangelogEntry(entry); - - ImGui.Spacing(); } private void DrawChangelogEntry(ChangelogEntry entry) { var currentColor = entry.IsCurrent == true - ? new Vector4(0.5f, 0.9f, 0.5f, 1.0f) + ? 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} "); + isOpen = ImGui.CollapsingHeader($" {entry.Name} — {entry.Date} ", flags); } ImGui.SameLine(); @@ -185,12 +675,12 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase if (!isOpen) return; - ImGuiHelpers.ScaledDummy(5); + ImGuiHelpers.ScaledDummy(8); if (!string.IsNullOrEmpty(entry.Message)) { ImGui.TextWrapped(entry.Message); - ImGuiHelpers.ScaledDummy(5); + ImGuiHelpers.ScaledDummy(8); return; } @@ -198,70 +688,56 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase { foreach (var version in entry.Versions) { - DrawFeatureHeader(version.Number, new Vector4(0.5f, 0.9f, 0.5f, 1.0f)); + DrawFeatureSection(version.Number, UIColors.Get("LightlessGreen")); + foreach (var item in version.Items) + { ImGui.BulletText(item); - } - } + } - ImGuiHelpers.ScaledDummy(5); - } - - private static void DrawFeatureHeader(string title, Vector4 accentColor) - { - var drawList = ImGui.GetWindowDrawList(); - var startPos = ImGui.GetCursorScreenPos(); - - var backgroundMin = startPos + new Vector2(-10, -5); - var backgroundMax = startPos + new Vector2(ImGui.GetContentRegionAvail().X + 10, 25); - drawList.AddRectFilled(backgroundMin, backgroundMax, ImGui.GetColorU32(new Vector4(0.12f, 0.12f, 0.15f, 0.6f)), 4f); - drawList.AddRectFilled(backgroundMin, backgroundMin + new Vector2(3, backgroundMax.Y - backgroundMin.Y), ImGui.GetColorU32(accentColor), 2f); - - ImGui.Spacing(); - ImGui.TextColored(accentColor, title); - ImGui.Spacing(); - } - - private void DrawCredits() - { - ImGui.TextUnformatted("Maintained & Developed by the Lightless Sync team."); - ImGui.TextUnformatted("Thank you to all supporters and contributors!"); - ImGuiHelpers.ScaledDummy(5); - - var availableRegion = ImGui.GetContentRegionAvail(); - var halfWidth = new Vector2( - availableRegion.X / 2f - ImGui.GetStyle().ItemSpacing.X / 2f, - availableRegion.Y - 120 * ImGuiHelpers.GlobalScale); - - using (var leftChild = ImRaii.Child("left_supporters", halfWidth)) - { - if (leftChild) - { - ImGui.TextUnformatted("Supporters (Ko-fi / Patreon)"); - _uiShared.RoundedSeparator(UIColors.Get("LightlessBlue"), thickness: 2f); - foreach (var supporter in _supporters) - ImGui.BulletText(supporter); - } - } - - ImGui.SameLine(); - - using (var rightChild = ImRaii.Child("right_contributors", halfWidth)) - { - if (rightChild) - { - ImGui.TextUnformatted("Contributors"); - _uiShared.RoundedSeparator(UIColors.Get("LightlessBlue"), thickness: 2f); - foreach (var contributor in _contributors) - ImGui.BulletText(contributor); + ImGuiHelpers.ScaledDummy(5); } } ImGuiHelpers.ScaledDummy(8); - ImGui.TextUnformatted("Credits"); - _uiShared.RoundedSeparator(UIColors.Get("LightlessYellow"), thickness: 2f); - foreach (var credit in _credits) - ImGui.BulletText(credit); + } + + 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()