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; 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; 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; SizeConstraints = new WindowSizeConstraints() { MinimumSize = new Vector2(600, 500), MaximumSize = new Vector2(900, 2000), }; LoadEmbeddedResources(); } public override void OnOpen() { _scrollToTop = true; } protected override void DrawInternal() { if (_uiShared.IsInGpose) return; DrawHeader(); DrawLinkButtons(); ImGuiHelpers.ScaledDummy(6); DrawTabs(); DrawCloseButton(); } private void DrawHeader() { using (_uiShared.UidFont.Push()) { ImGui.TextUnformatted("Lightless Sync"); } _uiShared.ColoredSeparator(UIColors.Get("LightlessBlue"), thickness: 2f); if (!string.IsNullOrEmpty(_changelog.Tagline)) { _uiShared.MediumText(_changelog.Tagline, UIColors.Get("LightlessBlue")); if (!string.IsNullOrEmpty(_changelog.Subline)) { ImGui.SameLine(); ImGui.TextColored(new Vector4(.75f, .75f, .85f, 1f), $" – {_changelog.Subline}"); } } ImGuiHelpers.ScaledDummy(5); } private void DrawLinkButtons() { 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"); } private void DrawCloseButton() { var closeWidth = 300f * ImGuiHelpers.GlobalScale; ImGui.SetCursorPosX((ImGui.GetWindowSize().X - closeWidth) / 2); if (ImGui.Button("Close", new Vector2(closeWidth, 0))) { IsOpen = false; } } private void DrawTabs() { using var tabBar = ImRaii.TabBar("lightless_update_tabs"); if (!tabBar) return; using (var changelogTab = ImRaii.TabItem(" Changelog ")) { if (changelogTab) { _selectedTab = 0; DrawChangelog(); } } using (var creditsTab = ImRaii.TabItem(" Supporters & Credits ")) { if (creditsTab) { _selectedTab = 1; DrawCredits(); } } } private void DrawChangelog() { using var child = ImRaii.Child("###ll_changelog", new Vector2(0, ImGui.GetContentRegionAvail().Y - 44), false); if (!child) return; if (_scrollToTop) { _scrollToTop = false; ImGui.SetScrollHereY(0); } 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) : new Vector4(0.75f, 0.75f, 0.85f, 1.0f); bool isOpen; using (ImRaii.PushColor(ImGuiCol.Text, currentColor)) { isOpen = ImGui.CollapsingHeader($" {entry.Name} — {entry.Date} "); } ImGui.SameLine(); ImGui.TextColored(new Vector4(0.75f, 0.75f, 0.85f, 1.0f), $" — {entry.Tagline}"); if (!isOpen) return; ImGuiHelpers.ScaledDummy(5); if (!string.IsNullOrEmpty(entry.Message)) { ImGui.TextWrapped(entry.Message); ImGuiHelpers.ScaledDummy(5); return; } if (entry.Versions != null) { foreach (var version in entry.Versions) { DrawFeatureHeader(version.Number, new Vector4(0.5f, 0.9f, 0.5f, 1.0f)); 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(8); ImGui.TextUnformatted("Credits"); _uiShared.RoundedSeparator(UIColors.Get("LightlessYellow"), thickness: 2f); foreach (var credit in _credits) ImGui.BulletText(credit); } 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(); } }