diff --git a/LightlessSync/UI/Models/Changelog.cs b/LightlessSync/UI/Models/Changelog.cs new file mode 100644 index 0000000..1a69756 --- /dev/null +++ b/LightlessSync/UI/Models/Changelog.cs @@ -0,0 +1,25 @@ +namespace LightlessSync.UI.Models +{ + public class ChangelogFile + { + public string Tagline { get; init; } = string.Empty; + public string Subline { get; init; } = string.Empty; + public List Changelog { get; init; } = new(); + } + + public class 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; } + } + + public class ChangelogVersion + { + public string Number { get; init; } = string.Empty; + public List Items { get; init; } = new(); + } +} \ No newline at end of file diff --git a/LightlessSync/UI/UpdateNotesUi.cs b/LightlessSync/UI/UpdateNotesUi.cs index 37763d3..558774f 100644 --- a/LightlessSync/UI/UpdateNotesUi.cs +++ b/LightlessSync/UI/UpdateNotesUi.cs @@ -12,6 +12,7 @@ using System.Text; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; using Dalamud.Interface; +using LightlessSync.UI.Models; namespace LightlessSync.UI; @@ -23,7 +24,7 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase private ChangelogFile _changelog = new(); private bool _scrollToTop; private int _selectedTab; - + private struct Particle { public Vector2 Position; @@ -37,17 +38,17 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase public float Depth; public float Hue; } - + private enum ParticleType { TwinklingStar, ShootingStar } - + private readonly List _particles = []; private float _particleSpawnTimer; private readonly Random _random = new(); - + private const float HeaderHeight = 150f; private const float ParticleSpawnInterval = 0.2f; private const int MaxParticles = 50; @@ -69,12 +70,12 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase RespectCloseHotkey = true; ShowCloseButton = true; - Flags = ImGuiWindowFlags.NoSavedSettings | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoTitleBar; + Flags = ImGuiWindowFlags.NoSavedSettings | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoCollapse | + ImGuiWindowFlags.NoTitleBar; SizeConstraints = new WindowSizeConstraints() { - MinimumSize = new Vector2(800, 700), - MaximumSize = new Vector2(800, 700), + MinimumSize = new Vector2(800, 700), MaximumSize = new Vector2(800, 700), }; LoadEmbeddedResources(); @@ -101,61 +102,63 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase var windowPos = ImGui.GetWindowPos(); var windowPadding = ImGui.GetStyle().WindowPadding; var headerWidth = (800f * ImGuiHelpers.GlobalScale) - (windowPadding.X * 2); - + var headerStart = windowPos + new Vector2(windowPadding.X, windowPadding.Y); var headerEnd = headerStart + new Vector2(headerWidth, HeaderHeight); var headerSize = new Vector2(headerWidth, HeaderHeight); - + var extendedParticleSize = new Vector2(headerWidth, HeaderHeight + ExtendedParticleHeight); - + DrawGradientBackground(headerStart, headerEnd); DrawHeaderText(headerStart); DrawHeaderButtons(headerStart, headerWidth); DrawBottomGradient(headerStart, headerEnd, headerWidth); - + ImGui.SetCursorPosY(windowPadding.Y + HeaderHeight + 5); ImGui.SetCursorPosX(20); 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}"); } } + ImGuiHelpers.ScaledDummy(3); - + DrawParticleEffects(headerStart, extendedParticleSize); } - + private void DrawGradientBackground(Vector2 headerStart, Vector2 headerEnd) { var drawList = ImGui.GetWindowDrawList(); - + 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, + headerStart, + headerEnd, ImGui.GetColorU32(darkPurple), ImGui.GetColorU32(darkPurple), ImGui.GetColorU32(deepPurple), ImGui.GetColorU32(deepPurple) ); - + var random = new Random(42); for (int i = 0; i < 50; i++) { @@ -167,12 +170,12 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase drawList.AddCircleFilled(starPos, 1f, ImGui.GetColorU32(new Vector4(1f, 1f, 1f, brightness))); } } - + private void DrawBottomGradient(Vector2 headerStart, Vector2 headerEnd, float width) { var drawList = ImGui.GetWindowDrawList(); var gradientHeight = 60f; - + for (int i = 0; i < gradientHeight; i++) { var progress = i / gradientHeight; @@ -190,23 +193,23 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase ); } } - + private void DrawHeaderText(Vector2 headerStart) { var textX = 20f; var textY = 30f; - + ImGui.SetCursorScreenPos(headerStart + new Vector2(textX, textY)); - + using (_uiShared.UidFont.Push()) { ImGui.TextColored(new Vector4(0.95f, 0.95f, 0.95f, 1.0f), "Lightless Sync"); } - + ImGui.SetCursorScreenPos(headerStart + new Vector2(textX, textY + 45f)); ImGui.TextColored(UIColors.Get("LightlessBlue"), "Update Notes"); } - + private void DrawHeaderButtons(Vector2 headerStart, float headerWidth) { var buttonSize = _uiShared.GetIconButtonSize(FontAwesomeIcon.Globe); @@ -216,9 +219,9 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase 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 })) @@ -227,71 +230,73 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase { 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; _particleSpawnTimer += deltaTime; - + if (_particleSpawnTimer > ParticleSpawnInterval && _particles.Count < MaxParticles) { SpawnParticle(bannerSize); _particleSpawnTimer = 0f; } - + if (_random.NextDouble() < 0.003) { SpawnShootingStar(bannerSize); } - + var drawList = ImGui.GetWindowDrawList(); - + for (int i = _particles.Count - 1; i >= 0; i--) { var particle = _particles[i]; - + var screenPos = bannerStart + particle.Position; - + if (particle.Type == ParticleType.ShootingStar && particle.Trail != null) { particle.Trail.Insert(0, particle.Position); if (particle.Trail.Count > MaxTrailLength) particle.Trail.RemoveAt(particle.Trail.Count - 1); } - + if (particle.Type == ParticleType.TwinklingStar) { particle.Twinkle += 0.005f * particle.Depth; } - + particle.Position += particle.Velocity * deltaTime; particle.Life -= deltaTime; - + var isOutOfBounds = particle.Position.X < -50 || particle.Position.X > bannerSize.X + 50 || particle.Position.Y < -50 || particle.Position.Y > bannerSize.Y + 50; - + if (particle.Life <= 0 || (particle.Type != ParticleType.TwinklingStar && isOutOfBounds)) { _particles.RemoveAt(i); continue; } - + if (particle.Type == ParticleType.TwinklingStar) { if (particle.Position.X < 0 || particle.Position.X > bannerSize.X) @@ -299,11 +304,11 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase if (particle.Position.Y < 0 || particle.Position.Y > bannerSize.Y) particle.Velocity = particle.Velocity with { Y = -particle.Velocity.Y }; } - + var fadeIn = Math.Min(1f, (particle.MaxLife - particle.Life) / 20f); var fadeOut = Math.Min(1f, particle.Life / 20f); var lifeFade = Math.Min(fadeIn, fadeOut); - + var edgeFadeX = Math.Min( Math.Min(1f, (particle.Position.X + EdgeFadeDistance) / EdgeFadeDistance), Math.Min(1f, (bannerSize.X - particle.Position.X + EdgeFadeDistance) / EdgeFadeDistance) @@ -313,22 +318,22 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase Math.Min(1f, (bannerSize.Y - particle.Position.Y + EdgeFadeDistance) / EdgeFadeDistance) ); var edgeFade = Math.Min(edgeFadeX, edgeFadeY); - + var baseAlpha = lifeFade * edgeFade; - var finalAlpha = particle.Type == ParticleType.TwinklingStar + var finalAlpha = particle.Type == ParticleType.TwinklingStar ? baseAlpha * (0.6f + 0.4f * MathF.Sin(particle.Twinkle)) : baseAlpha; - + if (particle.Type == ParticleType.ShootingStar && particle.Trail != null && particle.Trail.Count > 1) { var cyanColor = new Vector4(0.4f, 0.8f, 1.0f, 1.0f); - + for (int t = 1; t < particle.Trail.Count; t++) { var trailProgress = (float)t / particle.Trail.Count; var trailAlpha = Math.Min(1f, (1f - trailProgress) * finalAlpha * 1.8f); var trailWidth = (1f - trailProgress) * 3f + 1f; - + var glowAlpha = trailAlpha * 0.4f; drawList.AddLine( bannerStart + particle.Trail[t - 1], @@ -336,7 +341,7 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase ImGui.GetColorU32(cyanColor with { W = glowAlpha }), trailWidth + 4f ); - + drawList.AddLine( bannerStart + particle.Trail[t - 1], bannerStart + particle.Trail[t], @@ -349,61 +354,92 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase { DrawTwinklingStar(drawList, screenPos, particle.Size, particle.Hue, finalAlpha, particle.Depth); } - + _particles[i] = particle; } } - - private void DrawTwinklingStar(ImDrawListPtr drawList, Vector2 position, float size, float hue, float alpha, float depth) + + private void DrawTwinklingStar(ImDrawListPtr drawList, Vector2 position, float size, float hue, float alpha, + float depth) { var color = HslToRgb(hue, 1.0f, 0.85f); color.W = alpha; - + drawList.AddCircleFilled(position, size, ImGui.GetColorU32(color)); - + var glowColor = color with { W = alpha * 0.3f }; drawList.AddCircleFilled(position, size * (1.2f + depth * 0.3f), ImGui.GetColorU32(glowColor)); } - + private static Vector4 HslToRgb(float h, float s, float l) { h = h / 360f; float c = (1 - MathF.Abs(2 * l - 1)) * s; float x = c * (1 - MathF.Abs((h * 6) % 2 - 1)); float m = l - c / 2; - + float r, g, b; - if (h < 1f / 6f) { r = c; g = x; b = 0; } - else if (h < 2f / 6f) { r = x; g = c; b = 0; } - else if (h < 3f / 6f) { r = 0; g = c; b = x; } - else if (h < 4f / 6f) { r = 0; g = x; b = c; } - else if (h < 5f / 6f) { r = x; g = 0; b = c; } - else { r = c; g = 0; b = x; } - + if (h < 1f / 6f) + { + r = c; + g = x; + b = 0; + } + else if (h < 2f / 6f) + { + r = x; + g = c; + b = 0; + } + else if (h < 3f / 6f) + { + r = 0; + g = c; + b = x; + } + else if (h < 4f / 6f) + { + r = 0; + g = x; + b = c; + } + else if (h < 5f / 6f) + { + r = x; + g = 0; + b = c; + } + else + { + r = c; + g = 0; + b = x; + } + return new Vector4(r + m, g + m, b + m, 1.0f); } - - + + private void SpawnParticle(Vector2 bannerSize) { var position = new Vector2( (float)_random.NextDouble() * bannerSize.X, (float)_random.NextDouble() * bannerSize.Y ); - + var depthLayers = new[] { 0.5f, 1.0f, 1.5f }; var depth = depthLayers[_random.Next(depthLayers.Length)]; - + var velocity = new Vector2( ((float)_random.NextDouble() - 0.5f) * 0.05f * depth, ((float)_random.NextDouble() - 0.5f) * 0.05f * depth ); - + var isBlue = _random.NextDouble() < 0.5; var hue = isBlue ? 220f + (float)_random.NextDouble() * 30f : 270f + (float)_random.NextDouble() * 40f; var size = (0.5f + (float)_random.NextDouble() * 2f) * depth; var maxLife = 120f + (float)_random.NextDouble() * 60f; - + _particles.Add(new Particle { Position = position, @@ -418,13 +454,13 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase Hue = hue }); } - + private void SpawnShootingStar(Vector2 bannerSize) { var maxLife = 80f + (float)_random.NextDouble() * 40f; var startX = bannerSize.X * (0.3f + (float)_random.NextDouble() * 0.6f); var startY = -10f; - + _particles.Add(new Particle { Position = new Vector2(startX, startY), @@ -442,16 +478,16 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase Hue = 270f }); } - + 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"))) @@ -463,10 +499,12 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase } } } + 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)) + using (var child = ImRaii.Child("###ll_changelog", new Vector2(0, ImGui.GetContentRegionAvail().Y - 60), false, + ImGuiWindowFlags.AlwaysVerticalScrollbar)) { if (!child) return; @@ -476,7 +514,7 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase _scrollToTop = false; ImGui.SetScrollHereY(0); } - + ImGui.PushTextWrapPos(); foreach (var entry in _changelog.Changelog) @@ -494,10 +532,10 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase : new Vector4(0.95f, 0.95f, 1.0f, 1.0f); bool isOpen; - var flags = entry.IsCurrent == true - ? ImGuiTreeNodeFlags.DefaultOpen + 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"))) @@ -539,7 +577,7 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase ImGuiHelpers.ScaledDummy(8); } - + private static void DrawFeatureSection(string title, Vector4 accentColor) { var drawList = ImGui.GetWindowDrawList(); @@ -548,24 +586,24 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase var backgroundMin = startPos + new Vector2(-8, -4); var backgroundMax = startPos + new Vector2(availableWidth + 8, 28); - + var bgColor = new Vector4(0.12f, 0.12f, 0.15f, 0.7f); drawList.AddRectFilled(backgroundMin, backgroundMax, ImGui.GetColorU32(bgColor), 6f); - + drawList.AddRectFilled( - backgroundMin, - backgroundMin + new Vector2(4, backgroundMax.Y - backgroundMin.Y), - ImGui.GetColorU32(accentColor), + backgroundMin, + backgroundMin + new Vector2(4, backgroundMax.Y - backgroundMin.Y), + ImGui.GetColorU32(accentColor), 3f ); - + var glowColor = accentColor with { W = 0.15f }; drawList.AddRect( - backgroundMin, - backgroundMax, - ImGui.GetColorU32(glowColor), - 6f, - ImDrawFlags.None, + backgroundMin, + backgroundMax, + ImGui.GetColorU32(glowColor), + 6f, + ImDrawFlags.None, 1.5f ); @@ -597,26 +635,4 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase // Ignore - window will gracefully render with defaults } } - 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(); - } }