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.Numerics; using System.Reflection; using System.Text; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; using Dalamud.Interface; using LightlessSync.UI.Models; using LightlessSync.UI.Style; using LightlessSync.Utils; namespace LightlessSync.UI; // Inspiration taken from Brio and Character Select+ (goats) public class UpdateNotesUi : WindowMediatorSubscriberBase { private readonly UiSharedService _uiShared; private readonly LightlessConfigService _configService; private ChangelogFile _changelog = new(); private CreditsFile _credits = new(); private bool _scrollToTop; private bool _hasInitializedCollapsingHeaders; private readonly AnimatedHeader _animatedHeader = new(); public UpdateNotesUi(ILogger logger, LightlessMediator mediator, UiSharedService uiShared, LightlessConfigService configService, PerformanceCollectorService performanceCollectorService) : base(logger, mediator, "Lightless Sync — Update Notes", performanceCollectorService) { logger.LogInformation("UpdateNotesUi constructor called"); _uiShared = uiShared; _configService = configService; RespectCloseHotkey = true; ShowCloseButton = true; Flags = ImGuiWindowFlags.NoSavedSettings | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoMove; PositionCondition = ImGuiCond.Always; WindowBuilder.For(this) .AllowPinning(false) .AllowClickthrough(false) .SetFixedSize(new Vector2(800, 700)) .Apply(); LoadEmbeddedResources(); logger.LogInformation("UpdateNotesUi constructor completed successfully"); } public override void OnOpen() { _scrollToTop = true; _hasInitializedCollapsingHeaders = false; } public override void OnClose() { _animatedHeader.ClearParticles(); } private void CenterWindow() { var viewport = ImGui.GetMainViewport(); var center = viewport.GetCenter(); var windowSize = new Vector2(800f * ImGuiHelpers.GlobalScale, 700f * ImGuiHelpers.GlobalScale); Position = center - windowSize / 2f; } protected override void DrawInternal() { if (_uiShared.IsInGpose) return; CenterWindow(); DrawHeader(); ImGuiHelpers.ScaledDummy(6); DrawTabs(); DrawCloseButton(); } private void DrawHeader() { var windowPadding = ImGui.GetStyle().WindowPadding; var headerWidth = (800f * ImGuiHelpers.GlobalScale) - (windowPadding.X * 2); var buttons = new List { new(FontAwesomeIcon.Comments, "Join our Discord", () => Util.OpenLink("https://discord.gg/dsbjcXMnhA")), new(FontAwesomeIcon.Code, "View on Git", () => Util.OpenLink("https://git.lightless-sync.org/Lightless-Sync")) }; _animatedHeader.DrawWithButtons(headerWidth, "Lightless Sync", "Update Notes", buttons, _uiShared.UidFont); ImGui.SetCursorPosY(windowPadding.Y + _animatedHeader.Height + 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); } private void DrawTabs() { using (ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 6f)) using (ImRaii.PushColor(ImGuiCol.Tab, UIColors.Get("ButtonDefault"))) using (ImRaii.PushColor(ImGuiCol.TabHovered, UIColors.Get("LightlessPurple"))) using (ImRaii.PushColor(ImGuiCol.TabActive, UIColors.Get("LightlessPurpleActive"))) { using (var tabBar = ImRaii.TabBar("###ll_tabs", ImGuiTabBarFlags.None)) { if (!tabBar) return; using (var changelogTab = ImRaii.TabItem("Changelog")) { if (changelogTab) { DrawChangelog(); } } if (_credits.Credits != null && _credits.Credits.Count > 0) { using (var creditsTab = ImRaii.TabItem("Credits")) { if (creditsTab) { DrawCredits(); } } } } } } private void DrawCredits() { using (ImRaii.PushStyle(ImGuiStyleVar.ChildRounding, 6f)) using (var child = ImRaii.Child("###ll_credits", new Vector2(0, ImGui.GetContentRegionAvail().Y - 60), false, ImGuiWindowFlags.AlwaysVerticalScrollbar)) { if (!child) return; ImGui.PushTextWrapPos(); if (_credits.Credits != null) { foreach (var category in _credits.Credits) { DrawCreditCategory(category); ImGuiHelpers.ScaledDummy(10); } } ImGui.PopTextWrapPos(); ImGui.Spacing(); } } private static void DrawCreditCategory(CreditCategory category) { DrawFeatureSection(category.Category, UIColors.Get("LightlessBlue")); foreach (var item in category.Items) { if (!string.IsNullOrEmpty(item.Role)) { ImGui.BulletText($"{item.Name} — {item.Role}"); } else { ImGui.BulletText(item.Name); } } ImGuiHelpers.ScaledDummy(5); } 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))) { // Update last seen version when user acknowledges the update notes var ver = Assembly.GetExecutingAssembly().GetName().Version; var currentVersion = ver == null ? string.Empty : $"{ver.Major}.{ver.Minor}.{ver.Build}"; _configService.Current.LastSeenVersion = currentVersion; _configService.Save(); IsOpen = false; } if (ImGui.IsItemHovered()) { ImGui.SetTooltip("You can view this window again in the settings (title menu)"); } } } 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); } _hasInitializedCollapsingHeaders = true; ImGui.PopTextWrapPos(); ImGui.Spacing(); } } private void DrawChangelogEntry(ChangelogEntry entry) { var isCurrent = entry.IsCurrent ?? false; var currentColor = isCurrent ? UIColors.Get("LightlessGreen") : new Vector4(0.95f, 0.95f, 1.0f, 1.0f); var flags = isCurrent ? ImGuiTreeNodeFlags.DefaultOpen : ImGuiTreeNodeFlags.None; if (!_hasInitializedCollapsingHeaders) { ImGui.SetNextItemOpen(isCurrent, ImGuiCond.Always); } bool isOpen; using (ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 4f)) using (ImRaii.PushColor(ImGuiCol.Header, UIColors.Get("ButtonDefault"))) using (ImRaii.PushColor(ImGuiCol.Text, currentColor)) { isOpen = ImGui.CollapsingHeader($" {entry.Name} — {entry.Date} ", flags); ImGui.SameLine(); ImGui.TextColored(new Vector4(0.85f, 0.85f, 0.95f, 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); } } } 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); 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), 3f ); var glowColor = accentColor with { W = 0.15f }; drawList.AddRect( backgroundMin, backgroundMax, ImGui.GetColorU32(glowColor), 6f, ImDrawFlags.None, 1.5f ); // Calculate vertical centering var textSize = ImGui.CalcTextSize(title); var boxHeight = backgroundMax.Y - backgroundMin.Y; var verticalOffset = (boxHeight - textSize.Y) / 5f; ImGui.SetCursorPosX(ImGui.GetCursorPosX() + 8); ImGui.SetCursorPosY(ImGui.GetCursorPosY() + verticalOffset); ImGui.TextColored(accentColor, title); ImGui.SetCursorPosY(backgroundMax.Y - startPos.Y + ImGui.GetCursorPosY()); } private void LoadEmbeddedResources() { try { var assembly = Assembly.GetExecutingAssembly(); var deserializer = new DeserializerBuilder() .WithNamingConvention(CamelCaseNamingConvention.Instance) .IgnoreUnmatchedProperties() .Build(); // Load changelog using var changelogStream = assembly.GetManifestResourceStream("LightlessSync.Changelog.changelog.yaml"); if (changelogStream != null) { using var reader = new StreamReader(changelogStream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, 128); var yaml = reader.ReadToEnd(); _changelog = deserializer.Deserialize(yaml) ?? new(); } // Load credits using var creditsStream = assembly.GetManifestResourceStream("LightlessSync.Changelog.credits.yaml"); if (creditsStream != null) { using var reader = new StreamReader(creditsStream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, 128); var yaml = reader.ReadToEnd(); _credits = deserializer.Deserialize(yaml) ?? new(); } } catch { // Ignore - window will gracefully render with defaults } } }