From a66a43dda8a16b5d3c569e3e5bb6029b767db711 Mon Sep 17 00:00:00 2001 From: choco Date: Wed, 15 Oct 2025 13:29:32 +0200 Subject: [PATCH 01/16] patch notes setup, added some of the older patchnotes into a changelog.yaml --- .../Configurations/LightlessConfig.cs | 1 + LightlessSync/LightlessPlugin.cs | 18 + LightlessSync/LightlessSync.csproj | 7 +- LightlessSync/Plugin.cs | 1 + LightlessSync/UI/Changelog/changelog.yaml | 180 ++++++++++ LightlessSync/UI/Changelog/contributors.txt | 1 + LightlessSync/UI/Changelog/credits.txt | 2 + LightlessSync/UI/Changelog/supporters.txt | 1 + LightlessSync/UI/UpdateNotesUi.cs | 332 ++++++++++++++++++ 9 files changed, 542 insertions(+), 1 deletion(-) create mode 100644 LightlessSync/UI/Changelog/changelog.yaml create mode 100644 LightlessSync/UI/Changelog/contributors.txt create mode 100644 LightlessSync/UI/Changelog/credits.txt create mode 100644 LightlessSync/UI/Changelog/supporters.txt create mode 100644 LightlessSync/UI/UpdateNotesUi.cs diff --git a/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs b/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs index bdf8542..d66e956 100644 --- a/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs +++ b/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs @@ -146,4 +146,5 @@ public class LightlessConfig : ILightlessConfiguration public DateTime BroadcastTtl { get; set; } = DateTime.MinValue; public bool SyncshellFinderEnabled { get; set; } = false; public string? SelectedFinderSyncshell { get; set; } = null; + public string LastSeenVersion { get; set; } = string.Empty; } diff --git a/LightlessSync/LightlessPlugin.cs b/LightlessSync/LightlessPlugin.cs index 66d2c2b..4f9b226 100644 --- a/LightlessSync/LightlessPlugin.cs +++ b/LightlessSync/LightlessPlugin.cs @@ -155,6 +155,24 @@ public class LightlessPlugin : MediatorSubscriberBase, IHostedService _runtimeServiceScope.ServiceProvider.GetRequiredService(); _runtimeServiceScope.ServiceProvider.GetRequiredService(); + // TODO: move this to a better place + var ver = Assembly.GetExecutingAssembly().GetName().Version; + 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); + if (string.IsNullOrEmpty(lastSeen)) + { + _lightlessConfigService.Current.LastSeenVersion = currentVersion; + _lightlessConfigService.Save(); + } + else if (!string.Equals(lastSeen, currentVersion, StringComparison.Ordinal)) + { + // TODO: actually check if setup is complete + Mediator.Publish(new UiToggleMessage(typeof(UpdateNotesUi))); + _lightlessConfigService.Current.LastSeenVersion = currentVersion; + _lightlessConfigService.Save(); + } + #if !DEBUG if (_lightlessConfigService.Current.LogLevel != LogLevel.Information) { diff --git a/LightlessSync/LightlessSync.csproj b/LightlessSync/LightlessSync.csproj index 5b31c88..b51bcd7 100644 --- a/LightlessSync/LightlessSync.csproj +++ b/LightlessSync/LightlessSync.csproj @@ -1,4 +1,4 @@ - + @@ -46,6 +46,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + @@ -64,6 +65,10 @@ PreserveNewest + + + + diff --git a/LightlessSync/Plugin.cs b/LightlessSync/Plugin.cs index bac9e29..9ec4bed 100644 --- a/LightlessSync/Plugin.cs +++ b/LightlessSync/Plugin.cs @@ -246,6 +246,7 @@ public sealed class Plugin : IDalamudPlugin collection.AddScoped(); collection.AddScoped(); collection.AddScoped(); + collection.AddScoped(); collection.AddScoped((s) => new EditProfileUi(s.GetRequiredService>(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), diff --git a/LightlessSync/UI/Changelog/changelog.yaml b/LightlessSync/UI/Changelog/changelog.yaml new file mode 100644 index 0000000..c28a2c3 --- /dev/null +++ b/LightlessSync/UI/Changelog/changelog.yaml @@ -0,0 +1,180 @@ +tagline: "Lightless Sync v1.12.3" +subline: "FILLER" +changelog: + - name: "v1.12.3" + tagline: "FILLER" + date: "October 15th 2025" + is_current: true + versions: + - number: "New Features" + icon: "" + items: + - "New in-game Patch Notes window." + - "Credits section to thank contributors and supporters." + - "Patch notes only show after updates, not during first-time setup." + - number: "Notifications" + icon: "" + items: + - "More customizable notification options." + - "Perfomance limiter shows as notifications." + - "All notifications can be configured or disabled in Settings → Notifications." + + - name: "v1.12.2" + tagline: "LightFinder fixes, Notifications overhaul" + date: "October 12th 2025" + is_current: false + versions: + - number: "LightFinder" + icon: "" + items: + - "Server-side improvements for LightFinder functionality." + - "Command changed from '/light lightfinder' to '/light finder'." + - "Option to enable LightFinder on connection (opt-in, refreshes every 3 hours)." + - "LightFinder indicator can now be shown on the server info bar." + - number: "Notifications" + icon: "" + items: + - "Completely reworked notification system with new UI." + - "Pair requests now show as notifications." + - "Download progress shows as notifications." + - "Customizable notification sounds, size, position, and duration." + - "All notifications can be configured or disabled in Settings → Notifications." + - number: "Bug Fixes" + icon: "" + items: + - "Fixed nameplate alignment issues with LightFinder and icons." + - "Icons now properly apply instead of swapping on choice." + - "Updated Discord URL." + - "File cache logic improvements." + + - name: "v1.12.1" + tagline: "LightFinder customization and download limiter" + date: "October 8th 2025" + is_current: false + versions: + - number: "New Features" + icon: "" + items: + - "LightFinder text can be modified to an icon with customizable positioning." + - "Option to hide your own indicator or paired player indicators." + - "Pair Download Limiter: Limit simultaneous downloads to 1-6 users to reduce network strain." + - "Added '/light lightfinder' command to open LightFinder UI." + - number: "Improvements" + icon: "" + items: + - "Right-click menu option for Send Pair Request can be disabled." + - "Syncshell finder improvements." + - "Download limiter settings available in Settings → Transfers." + + - name: "v1.12.0" + tagline: "LightFinder - Major feature release" + date: "October 5th 2025" + is_current: false + versions: + - number: "Major Features" + icon: "" + items: + - "Introduced LightFinder: Optional feature inspired by FFXIV's Party Finder." + - "Find fellow Lightless users and advertise your Syncshell to others." + - "When enabled, you're visible to other LightFinder users for 3 hours." + - "LightFinder tag displays above your nameplate when active." + - "Receive pair requests directly in UI without exchanging UIDs." + - "Syncshell Finder allows joining indexed Syncshells." + - "[L] Send Pair Request added to player context menus." + - number: "Vanity Features" + icon: "" + items: + - "Supporters can now customize their name color in the Lightless UI." + - "Color changes visible to all users." + - number: "General Improvements" + icon: "" + items: + - "Pairing nameplate color override can now override FC tags." + - "Added .kdb as whitelisted filetype for uploads." + - "Various UI fixes, updates, and improvements." + + - name: "v1.11.12" + tagline: "Syncshell grouping and performance options" + date: "September 16th 2025" + is_current: false + versions: + - number: "New Features" + icon: "" + items: + - "Ability to show grouped syncshells in main UI/all syncshells (default ON)." + - "Transfer ownership button available in Admin Panel user list." + - "Self-threshold warning now opens character analysis screen when clicked." + - number: "Performance" + icon: "" + items: + - "Auto-pause combat and auto-pause performance are now optional settings." + - "Both options are auto-enabled by default - disable at your own risk." + - number: "Bug Fixes" + icon: "" + items: + - "Reworked file caching to reduce errors for some users." + - "Fixed bug where exiting PvP could desync some users." + + - name: "v1.11.9" + tagline: "File cache improvements" + date: "September 13th 2025" + is_current: false + versions: + - number: "Bug Fixes" + icon: "" + items: + - "Identified and fixed potential file cache problems." + - "Improved cache error handling and stability." + + - name: "v1.11.8" + tagline: "Hotfix - UI and exception handling" + date: "September 12th 2025" + is_current: false + versions: + - number: "Bug Fixes" + icon: "" + items: + - "Attempted fix for NullReferenceException spam." + - "Fixed additional UI edge cases preventing loading for some users." + - "Fixed color bar UI issues." + + - name: "v1.11.7" + tagline: "Hotfix - UI loading and warnings" + date: "September 12th 2025" + is_current: false + versions: + - number: "Bug Fixes" + icon: "" + items: + - "Fixed UI not loading for some users." + - "Self warnings now behind 'Warn on loading in players exceeding performance thresholds' setting." + + - name: "v1.11.6" + tagline: "Admin panel rework and new features" + date: "September 11th 2025" + is_current: false + versions: + - number: "New Features" + icon: "" + items: + - "Reworked Syncshell Admin Page with improved styling." + - "Right-click on Server Top Bar button to disconnect from Lightless." + - "Shift+Left click on Server Top Bar button to open settings." + - "Added colors section in settings to change accent colors." + - "Added pin option from Dalamud in the UI." + - "Ability to pause syncing while in Instance/Duty." + - "Functionality to create syncshell folders." + - "Added self-threshold warning." + - number: "Bug Fixes" + icon: "" + items: + - "Fixed owners being visible in moderator list view." + - "Removed Pin/Remove/Ban buttons on Owners when viewing as moderator." + - "Fixed nameplate bug in PvP." + - "Added 1 or 3 day options for inactive check." + + - name: "Template" + tagline: "" + date: "October 15th 2025" + is_current: false + message: "Thank you for using Lightless Sync!\n\nThis update brings quality of life improvements and polish to the user experience.\nWe're committed to helping you share your character with others seamlessly.\n\nIf you have any suggestions or encounter any issues, please let us know on Discord or GitHub!\n\n- The Lightless Team" diff --git a/LightlessSync/UI/Changelog/contributors.txt b/LightlessSync/UI/Changelog/contributors.txt new file mode 100644 index 0000000..fabb456 --- /dev/null +++ b/LightlessSync/UI/Changelog/contributors.txt @@ -0,0 +1 @@ +[Add contributor names - GitHub handles, etc.] diff --git a/LightlessSync/UI/Changelog/credits.txt b/LightlessSync/UI/Changelog/credits.txt new file mode 100644 index 0000000..4e2fe79 --- /dev/null +++ b/LightlessSync/UI/Changelog/credits.txt @@ -0,0 +1,2 @@ +UI design inspired by Brio's update window (Etheirys/Brio). Thanks to their team for the great UX ideas. +Special thanks to the Dalamud team and the XIV modding ecosystem for tooling & APIs. diff --git a/LightlessSync/UI/Changelog/supporters.txt b/LightlessSync/UI/Changelog/supporters.txt new file mode 100644 index 0000000..f8a29df --- /dev/null +++ b/LightlessSync/UI/Changelog/supporters.txt @@ -0,0 +1 @@ +[Your Names Here] diff --git a/LightlessSync/UI/UpdateNotesUi.cs b/LightlessSync/UI/UpdateNotesUi.cs new file mode 100644 index 0000000..57c8ef6 --- /dev/null +++ b/LightlessSync/UI/UpdateNotesUi.cs @@ -0,0 +1,332 @@ +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(); + } +} From 04c00af92eacfaa9db6fa16aa3d23318f4c68b67 Mon Sep 17 00:00:00 2001 From: choco Date: Wed, 15 Oct 2025 16:37:56 +0200 Subject: [PATCH 02/16] check if completed intro setup process --- LightlessSync/LightlessPlugin.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/LightlessSync/LightlessPlugin.cs b/LightlessSync/LightlessPlugin.cs index 4f9b226..c431419 100644 --- a/LightlessSync/LightlessPlugin.cs +++ b/LightlessSync/LightlessPlugin.cs @@ -167,8 +167,10 @@ public class LightlessPlugin : MediatorSubscriberBase, IHostedService } else if (!string.Equals(lastSeen, currentVersion, StringComparison.Ordinal)) { - // TODO: actually check if setup is complete - Mediator.Publish(new UiToggleMessage(typeof(UpdateNotesUi))); + if (_lightlessConfigService.Current.HasValidSetup() && _serverConfigurationManager.HasValidConfig()) + { + Mediator.Publish(new UiToggleMessage(typeof(UpdateNotesUi))); + } _lightlessConfigService.Current.LastSeenVersion = currentVersion; _lightlessConfigService.Save(); } From d5c12e81c3b36472a842f38ae832110c2546e08d Mon Sep 17 00:00:00 2001 From: choco Date: Wed, 15 Oct 2025 22:59:08 +0200 Subject: [PATCH 03/16] 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() From 823dd39a9bd44d94365979ca9eeb28ebedde9c6e Mon Sep 17 00:00:00 2001 From: choco Date: Thu, 16 Oct 2025 01:29:38 +0200 Subject: [PATCH 04/16] improved particle animations, took some inspiration from brio and character + kinda pogged at their change log --- LightlessSync/LightlessSync.csproj | 3 - LightlessSync/UI/Changelog/changelog.yaml | 8 +- LightlessSync/UI/Changelog/contributors.txt | 1 - LightlessSync/UI/Changelog/credits.txt | 2 - LightlessSync/UI/Changelog/supporters.txt | 1 - LightlessSync/UI/UpdateNotesUi.cs | 566 +++++++------------- 6 files changed, 191 insertions(+), 390 deletions(-) delete mode 100644 LightlessSync/UI/Changelog/contributors.txt delete mode 100644 LightlessSync/UI/Changelog/credits.txt delete mode 100644 LightlessSync/UI/Changelog/supporters.txt diff --git a/LightlessSync/LightlessSync.csproj b/LightlessSync/LightlessSync.csproj index b51bcd7..daf6cfd 100644 --- a/LightlessSync/LightlessSync.csproj +++ b/LightlessSync/LightlessSync.csproj @@ -66,9 +66,6 @@ PreserveNewest - - - diff --git a/LightlessSync/UI/Changelog/changelog.yaml b/LightlessSync/UI/Changelog/changelog.yaml index c28a2c3..d3024c9 100644 --- a/LightlessSync/UI/Changelog/changelog.yaml +++ b/LightlessSync/UI/Changelog/changelog.yaml @@ -171,10 +171,4 @@ changelog: - "Fixed owners being visible in moderator list view." - "Removed Pin/Remove/Ban buttons on Owners when viewing as moderator." - "Fixed nameplate bug in PvP." - - "Added 1 or 3 day options for inactive check." - - - name: "Template" - tagline: "" - date: "October 15th 2025" - is_current: false - message: "Thank you for using Lightless Sync!\n\nThis update brings quality of life improvements and polish to the user experience.\nWe're committed to helping you share your character with others seamlessly.\n\nIf you have any suggestions or encounter any issues, please let us know on Discord or GitHub!\n\n- The Lightless Team" + - "Added 1 or 3 day options for inactive check." \ No newline at end of file diff --git a/LightlessSync/UI/Changelog/contributors.txt b/LightlessSync/UI/Changelog/contributors.txt deleted file mode 100644 index fabb456..0000000 --- a/LightlessSync/UI/Changelog/contributors.txt +++ /dev/null @@ -1 +0,0 @@ -[Add contributor names - GitHub handles, etc.] diff --git a/LightlessSync/UI/Changelog/credits.txt b/LightlessSync/UI/Changelog/credits.txt deleted file mode 100644 index 4e2fe79..0000000 --- a/LightlessSync/UI/Changelog/credits.txt +++ /dev/null @@ -1,2 +0,0 @@ -UI design inspired by Brio's update window (Etheirys/Brio). Thanks to their team for the great UX ideas. -Special thanks to the Dalamud team and the XIV modding ecosystem for tooling & APIs. diff --git a/LightlessSync/UI/Changelog/supporters.txt b/LightlessSync/UI/Changelog/supporters.txt deleted file mode 100644 index f8a29df..0000000 --- a/LightlessSync/UI/Changelog/supporters.txt +++ /dev/null @@ -1 +0,0 @@ -[Your Names Here] diff --git a/LightlessSync/UI/UpdateNotesUi.cs b/LightlessSync/UI/UpdateNotesUi.cs index 83ac903..37763d3 100644 --- a/LightlessSync/UI/UpdateNotesUi.cs +++ b/LightlessSync/UI/UpdateNotesUi.cs @@ -6,7 +6,6 @@ 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; @@ -16,20 +15,15 @@ using Dalamud.Interface; namespace LightlessSync.UI; +// Inspiration taken from Brio and Character Select+ (goats) 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; @@ -37,27 +31,29 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase 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; + public float Twinkle; + public float Depth; + public float Hue; } private enum ParticleType { - Star, - Moon, - Sparkle, - FastFallingStar + TwinklingStar, + ShootingStar } private readonly List _particles = []; - private float _particleTimer; - private readonly Random _particleRandom = new(); - private Particle? _largeMoon; - private float _largeMoonTimer; + private float _particleSpawnTimer; + private readonly Random _random = new(); + + private const float HeaderHeight = 150f; + private const float ParticleSpawnInterval = 0.2f; + private const int MaxParticles = 50; + private const int MaxTrailLength = 50; + private const float EdgeFadeDistance = 30f; + private const float ExtendedParticleHeight = 40f; public UpdateNotesUi(ILogger logger, LightlessMediator mediator, @@ -66,7 +62,6 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase PerformanceCollectorService performanceCollectorService) : base(logger, mediator, "Lightless Sync — Update Notes", performanceCollectorService) { - _configService = configService; _uiShared = uiShared; AllowClickthrough = false; @@ -106,20 +101,20 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase 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); + var headerEnd = headerStart + new Vector2(headerWidth, HeaderHeight); + var headerSize = new Vector2(headerWidth, HeaderHeight); + + var extendedParticleSize = new Vector2(headerWidth, HeaderHeight + ExtendedParticleHeight); DrawGradientBackground(headerStart, headerEnd); - DrawParticleEffects(headerStart, new Vector2(headerWidth, headerHeight)); DrawHeaderText(headerStart); DrawHeaderButtons(headerStart, headerWidth); + DrawBottomGradient(headerStart, headerEnd, headerWidth); - ImGui.SetCursorPosY(windowPadding.Y + headerHeight + 5); - - // Version badge with icon - ImGui.SetCursorPosX(12); + ImGui.SetCursorPosY(windowPadding.Y + HeaderHeight + 5); + ImGui.SetCursorPosX(20); using (ImRaii.PushFont(UiBuilder.IconFont)) { ImGui.TextColored(UIColors.Get("LightlessGreen"), FontAwesomeIcon.Star.ToIconString()); @@ -140,16 +135,15 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase ImGui.TextColored(new Vector4(0.65f, 0.65f, 0.75f, 1.0f), $" – {_changelog.Subline}"); } } - - ImGui.Separator(); ImGuiHelpers.ScaledDummy(3); + + DrawParticleEffects(headerStart, extendedParticleSize); } 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); @@ -162,8 +156,7 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase ImGui.GetColorU32(deepPurple) ); - // Add some static "distant stars" for depth - var random = new Random(42); // Fixed seed for consistent pattern + var random = new Random(42); for (int i = 0; i < 50; i++) { var starPos = headerStart + new Vector2( @@ -173,27 +166,44 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase var brightness = 0.3f + (float)random.NextDouble() * 0.4f; 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; - // Accent border at bottom with glow - drawList.AddLine( - new Vector2(headerStart.X, headerEnd.Y), - headerEnd, - ImGui.GetColorU32(UIColors.Get("LightlessPurple")), - 2f - ); + for (int i = 0; i < gradientHeight; i++) + { + var progress = i / gradientHeight; + var smoothProgress = progress * progress; + var r = 0.12f + (0.0f - 0.12f) * smoothProgress; + var g = 0.08f + (0.0f - 0.08f) * smoothProgress; + var b = 0.20f + (0.0f - 0.20f) * smoothProgress; + var alpha = 1f - smoothProgress; + var gradientColor = new Vector4(r, g, b, alpha); + drawList.AddLine( + new Vector2(headerStart.X, headerEnd.Y + i), + new Vector2(headerStart.X + width, headerEnd.Y + i), + ImGui.GetColorU32(gradientColor), + 1f + ); + } } private void DrawHeaderText(Vector2 headerStart) { - // Title text overlay - drawn after particles so it's on top - ImGui.SetCursorScreenPos(headerStart + new Vector2(20, 30)); + 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(20, 75)); + ImGui.SetCursorScreenPos(headerStart + new Vector2(textX, textY + 45f)); ImGui.TextColored(UIColors.Get("LightlessBlue"), "Update Notes"); } @@ -203,8 +213,6 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase 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; @@ -239,372 +247,202 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase private void DrawParticleEffects(Vector2 bannerStart, Vector2 bannerSize) { var deltaTime = ImGui.GetIO().DeltaTime; - _particleTimer += deltaTime; - _largeMoonTimer += deltaTime; + _particleSpawnTimer += deltaTime; - // Spawn new particles - if (_particleTimer > 0.3f && _particles.Count < 30) + if (_particleSpawnTimer > ParticleSpawnInterval && _particles.Count < MaxParticles) { - SpawnParticle(bannerStart, bannerSize); - _particleTimer = 0f; + SpawnParticle(bannerSize); + _particleSpawnTimer = 0f; } - // Spawn or update large moon - if (_largeMoon == null || _largeMoonTimer > 45f) + if (_random.NextDouble() < 0.003) { - SpawnLargeMoon(bannerStart, bannerSize); - _largeMoonTimer = 0f; + SpawnShootingStar(bannerSize); } 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) + var screenPos = bannerStart + particle.Position; + + if (particle.Type == ParticleType.ShootingStar && particle.Trail != null) { particle.Trail.Insert(0, particle.Position); - var maxTrailLength = particle.Type == ParticleType.FastFallingStar ? 15 : 8; - if (particle.Trail.Count > maxTrailLength) + 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; - 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) + 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; } - 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) + if (particle.Type == ParticleType.TwinklingStar) { + if (particle.Position.X < 0 || particle.Position.X > bannerSize.X) + particle.Velocity = particle.Velocity with { X = -particle.Velocity.X }; + 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) + ); + var edgeFadeY = Math.Min( + Math.Min(1f, (particle.Position.Y + EdgeFadeDistance) / EdgeFadeDistance), + 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 + ? 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++) { - 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; + 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( - particle.Trail[t - 1], - particle.Trail[t], - ImGui.GetColorU32(trailColor), - thickness + bannerStart + particle.Trail[t - 1], + bannerStart + particle.Trail[t], + ImGui.GetColorU32(cyanColor with { W = glowAlpha }), + trailWidth + 4f + ); + + drawList.AddLine( + bannerStart + particle.Trail[t - 1], + bannerStart + particle.Trail[t], + ImGui.GetColorU32(cyanColor with { W = trailAlpha }), + trailWidth ); } } - - // Draw based on particle type - switch (particle.Type) + else if (particle.Type == ParticleType.TwinklingStar) { - 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; + DrawTwinklingStar(drawList, screenPos, particle.Size, particle.Hue, finalAlpha, particle.Depth); } _particles[i] = particle; } } - private void DrawStar(ImDrawListPtr drawList, Vector2 position, float size, Vector4 color, float rotation) + private void DrawTwinklingStar(ImDrawListPtr drawList, Vector2 position, float size, float hue, float alpha, float depth) { - // 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); - } + var color = HslToRgb(hue, 1.0f, 0.85f); + color.W = alpha; - // 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)); - } + var glowColor = color with { W = alpha * 0.3f }; + drawList.AddCircleFilled(position, size * (1.2f + depth * 0.3f), ImGui.GetColorU32(glowColor)); } - private void DrawSparkle(ImDrawListPtr drawList, Vector2 position, float size, Vector4 color, float rotation) + private static Vector4 HslToRgb(float h, float s, float l) { - // Draw a 4-pointed sparkle (plus shape) - var thickness = size * 0.3f; + 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; - // Horizontal line - drawList.AddLine( - position + new Vector2(-size, 0), - position + new Vector2(size, 0), - ImGui.GetColorU32(color), - thickness - ); + 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; } - // 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)); + return new Vector4(r + m, g + m, b + m, 1.0f); } - private void SpawnParticle(Vector2 bannerStart, Vector2 bannerSize) + + private void SpawnParticle(Vector2 bannerSize) { - var typeRoll = _particleRandom.Next(100); - var particleType = typeRoll switch - { - < 35 => ParticleType.Star, - < 50 => ParticleType.Moon, - < 65 => ParticleType.FastFallingStar, - _ => ParticleType.Sparkle - }; + var position = new Vector2( + (float)_random.NextDouble() * bannerSize.X, + (float)_random.NextDouble() * bannerSize.Y + ); - Vector2 position; - Vector2 velocity; + var depthLayers = new[] { 0.5f, 1.0f, 1.5f }; + var depth = depthLayers[_random.Next(depthLayers.Length)]; - // 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 velocity = new Vector2( + ((float)_random.NextDouble() - 0.5f) * 0.05f * depth, + ((float)_random.NextDouble() - 0.5f) * 0.05f * depth + ); - var particle = new Particle + 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, 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, + Life = maxLife, + MaxLife = maxLife, + Size = size, + Type = ParticleType.TwinklingStar, Trail = null, - IsLargeMoon = true - }; + Twinkle = (float)_random.NextDouble() * MathF.PI * 2, + Depth = depth, + 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), + Velocity = new Vector2( + -50f - (float)_random.NextDouble() * 40f, + 30f + (float)_random.NextDouble() * 40f + ), + Life = maxLife, + MaxLife = maxLife, + Size = 2.5f, + Type = ParticleType.ShootingStar, + Trail = new List(), + Twinkle = 0, + Depth = 1.0f, + Hue = 270f + }); + } + private void DrawCloseButton() { @@ -653,7 +491,7 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase { var currentColor = entry.IsCurrent == true ? UIColors.Get("LightlessGreen") - : new Vector4(0.75f, 0.75f, 0.85f, 1.0f); + : new Vector4(0.95f, 0.95f, 1.0f, 1.0f); bool isOpen; var flags = entry.IsCurrent == true @@ -670,7 +508,7 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase } ImGui.SameLine(); - ImGui.TextColored(new Vector4(0.75f, 0.75f, 0.85f, 1.0f), $" — {entry.Tagline}"); + ImGui.TextColored(new Vector4(0.85f, 0.85f, 0.95f, 1.0f), $" — {entry.Tagline}"); if (!isOpen) return; @@ -711,11 +549,9 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase 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), @@ -723,7 +559,6 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase 3f ); - // Subtle glow effect var glowColor = accentColor with { W = 0.15f }; drawList.AddRect( backgroundMin, @@ -745,11 +580,6 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase 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) { @@ -767,22 +597,6 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase // 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; From f77d109d00c7e61c3cf961e1adaa33fcf861d3f7 Mon Sep 17 00:00:00 2001 From: choco Date: Thu, 16 Oct 2025 10:37:00 +0200 Subject: [PATCH 05/16] added changelog model and update notes UI with particle effects --- LightlessSync/UI/Models/Changelog.cs | 25 +++ LightlessSync/UI/UpdateNotesUi.cs | 246 ++++++++++++++------------- 2 files changed, 156 insertions(+), 115 deletions(-) create mode 100644 LightlessSync/UI/Models/Changelog.cs 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(); - } } From 92b8d4a1cd25253e4e305d577497a2d6dff306a2 Mon Sep 17 00:00:00 2001 From: choco Date: Thu, 16 Oct 2025 14:35:30 +0200 Subject: [PATCH 06/16] credits integration into the yaml and ui --- LightlessSync/UI/Changelog/changelog.yaml | 38 +++++++++- LightlessSync/UI/Models/Changelog.cs | 13 ++++ LightlessSync/UI/UpdateNotesUi.cs | 88 ++++++++++++++++++++++- 3 files changed, 137 insertions(+), 2 deletions(-) diff --git a/LightlessSync/UI/Changelog/changelog.yaml b/LightlessSync/UI/Changelog/changelog.yaml index d3024c9..a2af1f1 100644 --- a/LightlessSync/UI/Changelog/changelog.yaml +++ b/LightlessSync/UI/Changelog/changelog.yaml @@ -171,4 +171,40 @@ changelog: - "Fixed owners being visible in moderator list view." - "Removed Pin/Remove/Ban buttons on Owners when viewing as moderator." - "Fixed nameplate bug in PvP." - - "Added 1 or 3 day options for inactive check." \ No newline at end of file + - "Added 1 or 3 day options for inactive check." + +credits: + - category: "Development Team" + items: + - name: "Choco" + role: "Cringe Developer" + - name: "Additional Contributors" + role: "Community Contributors & Bug Reporters" + + - category: "Plugin Integration & IPC Support" + items: + - name: "Penumbra Team" + role: "Mod framework integration" + - name: "Glamourer Team" + role: "Customization system integration" + - name: "Customize+ Team" + role: "Body scaling integration" + - name: "Simple Heels Team" + role: "Height offset integration" + - name: "Honorific Team" + role: "Title system integration" + - name: "Moodles Team" + role: "Status effect integration" + - name: "PetNicknames Team" + role: "Pet naming integration" + - name: "Brio Team" + role: "GPose enhancement integration" + + - category: "Special Thanks" + items: + - name: "Dalamud & XIVLauncher Teams" + role: "Plugin framework and infrastructure" + - name: "Community Supporters" + role: "Testing, feedback, and financial support" + - name: "Beta Testers" + role: "Early testing and bug reporting" \ No newline at end of file diff --git a/LightlessSync/UI/Models/Changelog.cs b/LightlessSync/UI/Models/Changelog.cs index 1a69756..bf1a474 100644 --- a/LightlessSync/UI/Models/Changelog.cs +++ b/LightlessSync/UI/Models/Changelog.cs @@ -5,6 +5,7 @@ namespace LightlessSync.UI.Models public string Tagline { get; init; } = string.Empty; public string Subline { get; init; } = string.Empty; public List Changelog { get; init; } = new(); + public List? Credits { get; init; } } public class ChangelogEntry @@ -22,4 +23,16 @@ namespace LightlessSync.UI.Models public string Number { get; init; } = string.Empty; public List Items { get; init; } = new(); } + + public class CreditCategory + { + public string Category { get; init; } = string.Empty; + public List Items { get; init; } = new(); + } + + public class CreditItem + { + public string Name { get; init; } = string.Empty; + public string Role { get; init; } = string.Empty; + } } \ No newline at end of file diff --git a/LightlessSync/UI/UpdateNotesUi.cs b/LightlessSync/UI/UpdateNotesUi.cs index 558774f..8345987 100644 --- a/LightlessSync/UI/UpdateNotesUi.cs +++ b/LightlessSync/UI/UpdateNotesUi.cs @@ -93,7 +93,7 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase DrawHeader(); ImGuiHelpers.ScaledDummy(6); - DrawChangelog(); + DrawTabs(); DrawCloseButton(); } @@ -480,6 +480,92 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase } + 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("What's New")) + { + if (changelogTab) + { + _selectedTab = 0; + DrawChangelog(); + } + } + + if (_changelog.Credits != null && _changelog.Credits.Count > 0) + { + using (var creditsTab = ImRaii.TabItem("Credits")) + { + if (creditsTab) + { + _selectedTab = 1; + 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 (_changelog.Credits != null) + { + foreach (var category in _changelog.Credits) + { + DrawCreditCategory(category); + ImGuiHelpers.ScaledDummy(10); + } + } + + ImGui.PopTextWrapPos(); + ImGui.Spacing(); + } + } + + private void DrawCreditCategory(CreditCategory category) + { + DrawFeatureSection(category.Category, UIColors.Get("LightlessBlue")); + + ImGui.Indent(15f); + + foreach (var item in category.Items) + { + using (ImRaii.PushColor(ImGuiCol.Text, new Vector4(0.95f, 0.95f, 1.0f, 1.0f))) + { + ImGui.TextWrapped($"• {item.Name}"); + } + + if (!string.IsNullOrEmpty(item.Role)) + { + ImGui.SameLine(); + ImGui.TextColored(new Vector4(0.7f, 0.7f, 0.8f, 1.0f), $" — {item.Role}"); + } + + ImGuiHelpers.ScaledDummy(3); + } + + ImGui.Unindent(15f); + } + private void DrawCloseButton() { ImGuiHelpers.ScaledDummy(5); From 6d01d47c2f883158e3346292ff45c41012e69fd4 Mon Sep 17 00:00:00 2001 From: choco Date: Thu, 16 Oct 2025 22:13:40 +0200 Subject: [PATCH 07/16] broadcast notifications, action button for reconnecting once expired --- LightlessSync/Services/BroadcastService.cs | 84 +++++++++++++++++++++- 1 file changed, 81 insertions(+), 3 deletions(-) diff --git a/LightlessSync/Services/BroadcastService.cs b/LightlessSync/Services/BroadcastService.cs index 0cdd3c0..cad09dd 100644 --- a/LightlessSync/Services/BroadcastService.cs +++ b/LightlessSync/Services/BroadcastService.cs @@ -1,4 +1,8 @@ -using LightlessSync.API.Dto.Group; +using Dalamud.Interface; +using LightlessSync.LightlessConfiguration.Models; +using LightlessSync.UI; +using LightlessSync.UI.Models; +using LightlessSync.API.Dto.Group; using LightlessSync.API.Dto.User; using LightlessSync.LightlessConfiguration; using LightlessSync.Services.Mediator; @@ -140,6 +144,11 @@ public class BroadcastService : IHostedService, IMediatorSubscriber IsLightFinderAvailable = false; ApplyBroadcastDisabled(forcePublish: true); _logger.LogDebug("Cleared Lightfinder state due to disconnect."); + + _mediator.Publish(new NotificationMessage( + "Disconnected from Server", + "Your Lightfinder broadcast has been disabled due to disconnection.", + NotificationType.Warning)); } public Task StartAsync(CancellationToken cancellationToken) @@ -236,6 +245,11 @@ public class BroadcastService : IHostedService, IMediatorSubscriber { _logger.LogInformation("Auto-enabling Lightfinder broadcast after reconnect."); _mediator.Publish(new EnableBroadcastMessage(hashedCid, true)); + + _mediator.Publish(new NotificationMessage( + "Broadcast Auto-Enabled", + "Your Lightfinder broadcast has been automatically enabled.", + NotificationType.Info)); } } catch (OperationCanceledException) @@ -389,20 +403,29 @@ public class BroadcastService : IHostedService, IMediatorSubscriber - public async void ToggleBroadcast() + public async void ToggleBroadcast(bool bypassCooldown = false) { + if (!IsLightFinderAvailable) { _logger.LogWarning("ToggleBroadcast - Lightfinder is not available."); + _mediator.Publish(new NotificationMessage( + "Broadcast Unavailable", + "Lightfinder is not available on this server.", + NotificationType.Error)); return; } await RequireConnectionAsync(nameof(ToggleBroadcast), async () => { var cooldown = RemainingCooldown; - if (!_config.Current.BroadcastEnabled && cooldown is { } cd && cd > TimeSpan.Zero) + if (!bypassCooldown && !_config.Current.BroadcastEnabled && cooldown is { } cd && cd > TimeSpan.Zero) { _logger.LogWarning("Cooldown active. Must wait {Remaining}s before re-enabling.", cd.TotalSeconds); + _mediator.Publish(new NotificationMessage( + "Broadcast Cooldown", + $"Please wait {cd.TotalSeconds:F0} seconds before re-enabling broadcast.", + NotificationType.Warning)); return; } @@ -427,10 +450,19 @@ public class BroadcastService : IHostedService, IMediatorSubscriber _logger.LogDebug("Toggling broadcast. Server currently broadcasting: {ServerStatus}, setting to: {NewStatus}", isCurrentlyBroadcasting, newStatus); _mediator.Publish(new EnableBroadcastMessage(hashedCid, newStatus)); + + _mediator.Publish(new NotificationMessage( + newStatus ? "Broadcast Enabled" : "Broadcast Disabled", + newStatus ? "Your Lightfinder broadcast has been enabled." : "Your Lightfinder broadcast has been disabled.", + NotificationType.Info)); } catch (Exception ex) { _logger.LogError(ex, "Failed to determine current broadcast status for toggle"); + _mediator.Publish(new NotificationMessage( + "Broadcast Toggle Failed", + $"Failed to toggle broadcast: {ex.Message}", + NotificationType.Error)); } }).ConfigureAwait(false); } @@ -493,6 +525,7 @@ public class BroadcastService : IHostedService, IMediatorSubscriber { _logger.LogDebug("Broadcast TTL expired. Disabling broadcast locally."); ApplyBroadcastDisabled(forcePublish: true); + ShowBroadcastExpiredNotification(); } } else @@ -501,4 +534,49 @@ public class BroadcastService : IHostedService, IMediatorSubscriber } }).ConfigureAwait(false); } + + private void ShowBroadcastExpiredNotification() + { + var notification = new LightlessNotification + { + Id = "broadcast_expired", + Title = "Broadcast Expired", + Message = "Your Lightfinder broadcast has expired after 3 hours. Would you like to re-enable it?", + Type = NotificationType.PairRequest, + Duration = TimeSpan.FromSeconds(180), + Actions = new List + { + new() + { + Id = "re_enable", + Label = "Re-enable", + Icon = FontAwesomeIcon.Plus, + Color = UIColors.Get("PairBlue"), + IsPrimary = true, + OnClick = (n) => + { + _logger.LogInformation("Re-enabling broadcast from notification"); + ToggleBroadcast(bypassCooldown: true); + n.IsDismissed = true; + n.IsAnimatingOut = true; + } + }, + new() + { + Id = "close", + Label = "Close", + Icon = FontAwesomeIcon.Times, + Color = UIColors.Get("DimRed"), + OnClick = (n) => + { + _logger.LogInformation("Broadcast expiration notification dismissed"); + n.IsDismissed = true; + n.IsAnimatingOut = true; + } + } + } + }; + + _mediator.Publish(new LightlessNotificationMessage(notification)); + } } \ No newline at end of file From dccd2cdc36bdeb378e0dbfcfba40af02b67c2213 Mon Sep 17 00:00:00 2001 From: choco Date: Thu, 16 Oct 2025 22:52:46 +0200 Subject: [PATCH 08/16] changelog cleanup, credits tab --- .../{UI => }/Changelog/changelog.yaml | 38 +--------- LightlessSync/Changelog/credits.yaml | 35 +++++++++ LightlessSync/LightlessSync.csproj | 3 +- LightlessSync/UI/Models/Changelog.cs | 5 ++ LightlessSync/UI/UpdateNotesUi.cs | 76 +++++++++++-------- 5 files changed, 86 insertions(+), 71 deletions(-) rename LightlessSync/{UI => }/Changelog/changelog.yaml (84%) create mode 100644 LightlessSync/Changelog/credits.yaml diff --git a/LightlessSync/UI/Changelog/changelog.yaml b/LightlessSync/Changelog/changelog.yaml similarity index 84% rename from LightlessSync/UI/Changelog/changelog.yaml rename to LightlessSync/Changelog/changelog.yaml index a2af1f1..98134a1 100644 --- a/LightlessSync/UI/Changelog/changelog.yaml +++ b/LightlessSync/Changelog/changelog.yaml @@ -161,7 +161,6 @@ changelog: - "Right-click on Server Top Bar button to disconnect from Lightless." - "Shift+Left click on Server Top Bar button to open settings." - "Added colors section in settings to change accent colors." - - "Added pin option from Dalamud in the UI." - "Ability to pause syncing while in Instance/Duty." - "Functionality to create syncshell folders." - "Added self-threshold warning." @@ -172,39 +171,4 @@ changelog: - "Removed Pin/Remove/Ban buttons on Owners when viewing as moderator." - "Fixed nameplate bug in PvP." - "Added 1 or 3 day options for inactive check." - -credits: - - category: "Development Team" - items: - - name: "Choco" - role: "Cringe Developer" - - name: "Additional Contributors" - role: "Community Contributors & Bug Reporters" - - - category: "Plugin Integration & IPC Support" - items: - - name: "Penumbra Team" - role: "Mod framework integration" - - name: "Glamourer Team" - role: "Customization system integration" - - name: "Customize+ Team" - role: "Body scaling integration" - - name: "Simple Heels Team" - role: "Height offset integration" - - name: "Honorific Team" - role: "Title system integration" - - name: "Moodles Team" - role: "Status effect integration" - - name: "PetNicknames Team" - role: "Pet naming integration" - - name: "Brio Team" - role: "GPose enhancement integration" - - - category: "Special Thanks" - items: - - name: "Dalamud & XIVLauncher Teams" - role: "Plugin framework and infrastructure" - - name: "Community Supporters" - role: "Testing, feedback, and financial support" - - name: "Beta Testers" - role: "Early testing and bug reporting" \ No newline at end of file + - "Fixed bug where some users could not see their own syncshell folders." \ No newline at end of file diff --git a/LightlessSync/Changelog/credits.yaml b/LightlessSync/Changelog/credits.yaml new file mode 100644 index 0000000..b3b3e8c --- /dev/null +++ b/LightlessSync/Changelog/credits.yaml @@ -0,0 +1,35 @@ +credits: + - category: "Development Team" + items: + - name: "Choco" + role: "Cringe Developer" + - name: "Additional Contributors" + role: "Community Contributors & Bug Reporters" + + - category: "Plugin Integration & IPC Support" + items: + - name: "Penumbra Team" + role: "Mod framework integration" + - name: "Glamourer Team" + role: "Customization system integration" + - name: "Customize+ Team" + role: "Body scaling integration" + - name: "Simple Heels Team" + role: "Height offset integration" + - name: "Honorific Team" + role: "Title system integration" + - name: "Moodles Team" + role: "Status effect integration" + - name: "PetNicknames Team" + role: "Pet naming integration" + - name: "Brio Team" + role: "GPose enhancement integration" + + - category: "Special Thanks" + items: + - name: "Dalamud & XIVLauncher Teams" + role: "Plugin framework and infrastructure" + - name: "Community Supporters" + role: "Testing, feedback, and financial support" + - name: "Beta Testers" + role: "Early testing and bug reporting" diff --git a/LightlessSync/LightlessSync.csproj b/LightlessSync/LightlessSync.csproj index daf6cfd..b4b5288 100644 --- a/LightlessSync/LightlessSync.csproj +++ b/LightlessSync/LightlessSync.csproj @@ -65,7 +65,8 @@ PreserveNewest - + + diff --git a/LightlessSync/UI/Models/Changelog.cs b/LightlessSync/UI/Models/Changelog.cs index bf1a474..23d26c4 100644 --- a/LightlessSync/UI/Models/Changelog.cs +++ b/LightlessSync/UI/Models/Changelog.cs @@ -35,4 +35,9 @@ namespace LightlessSync.UI.Models public string Name { get; init; } = string.Empty; public string Role { get; init; } = string.Empty; } + + public class CreditsFile + { + public List Credits { get; init; } = new(); + } } \ No newline at end of file diff --git a/LightlessSync/UI/UpdateNotesUi.cs b/LightlessSync/UI/UpdateNotesUi.cs index 8345987..f25c38a 100644 --- a/LightlessSync/UI/UpdateNotesUi.cs +++ b/LightlessSync/UI/UpdateNotesUi.cs @@ -22,6 +22,7 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase private readonly UiSharedService _uiShared; private ChangelogFile _changelog = new(); + private CreditsFile _credits = new(); private bool _scrollToTop; private int _selectedTab; @@ -492,7 +493,7 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase if (!tabBar) return; - using (var changelogTab = ImRaii.TabItem("What's New")) + using (var changelogTab = ImRaii.TabItem("Changelog")) { if (changelogTab) { @@ -501,7 +502,7 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase } } - if (_changelog.Credits != null && _changelog.Credits.Count > 0) + if (_credits.Credits != null && _credits.Credits.Count > 0) { using (var creditsTab = ImRaii.TabItem("Credits")) { @@ -527,9 +528,9 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase ImGui.PushTextWrapPos(); - if (_changelog.Credits != null) + if (_credits.Credits != null) { - foreach (var category in _changelog.Credits) + foreach (var category in _credits.Credits) { DrawCreditCategory(category); ImGuiHelpers.ScaledDummy(10); @@ -545,25 +546,19 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase { DrawFeatureSection(category.Category, UIColors.Get("LightlessBlue")); - ImGui.Indent(15f); - foreach (var item in category.Items) { - using (ImRaii.PushColor(ImGuiCol.Text, new Vector4(0.95f, 0.95f, 1.0f, 1.0f))) - { - ImGui.TextWrapped($"• {item.Name}"); - } - if (!string.IsNullOrEmpty(item.Role)) { - ImGui.SameLine(); - ImGui.TextColored(new Vector4(0.7f, 0.7f, 0.8f, 1.0f), $" — {item.Role}"); + ImGui.BulletText($"{item.Name} — {item.Role}"); + } + else + { + ImGui.BulletText(item.Name); } - - ImGuiHelpers.ScaledDummy(3); } - ImGui.Unindent(15f); + ImGuiHelpers.ScaledDummy(5); } private void DrawCloseButton() @@ -604,7 +599,9 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase ImGui.PushTextWrapPos(); foreach (var entry in _changelog.Changelog) + { DrawChangelogEntry(entry); + } ImGui.PopTextWrapPos(); ImGui.Spacing(); @@ -617,7 +614,6 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase ? UIColors.Get("LightlessGreen") : new Vector4(0.95f, 0.95f, 1.0f, 1.0f); - bool isOpen; var flags = entry.IsCurrent == true ? ImGuiTreeNodeFlags.DefaultOpen : ImGuiTreeNodeFlags.None; @@ -628,15 +624,15 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase using (ImRaii.PushColor(ImGuiCol.HeaderActive, UIColors.Get("LightlessPurpleActive"))) using (ImRaii.PushColor(ImGuiCol.Text, currentColor)) { - isOpen = ImGui.CollapsingHeader($" {entry.Name} — {entry.Date} ", flags); + var 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; } - 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)) @@ -660,8 +656,6 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase ImGuiHelpers.ScaledDummy(5); } } - - ImGuiHelpers.ScaledDummy(8); } private static void DrawFeatureSection(string title, Vector4 accentColor) @@ -693,10 +687,15 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase 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.Spacing(); + ImGui.SetCursorPosY(ImGui.GetCursorPosY() + verticalOffset); ImGui.TextColored(accentColor, title); - ImGui.Spacing(); + ImGui.SetCursorPosY(backgroundMax.Y - startPos.Y + ImGui.GetCursorPosY()); } private void LoadEmbeddedResources() @@ -704,17 +703,28 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase try { var assembly = Assembly.GetExecutingAssembly(); - using var changelogStream = assembly.GetManifestResourceStream("LightlessSync.UI.Changelog.changelog.yaml"); + 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, true, 128); var yaml = reader.ReadToEnd(); - var deserializer = new DeserializerBuilder() - .WithNamingConvention(CamelCaseNamingConvention.Instance) - .IgnoreUnmatchedProperties() - .Build(); _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, true, 128); + var yaml = reader.ReadToEnd(); + _credits = deserializer.Deserialize(yaml) ?? new(); + } } catch { From 9170b5205c78d58880f2f33fccc2ec0250092dde Mon Sep 17 00:00:00 2001 From: choco Date: Thu, 16 Oct 2025 22:54:56 +0200 Subject: [PATCH 09/16] removed temp changelog on loading --- LightlessSync/LightlessPlugin.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/LightlessSync/LightlessPlugin.cs b/LightlessSync/LightlessPlugin.cs index dcc1990..617cfe3 100644 --- a/LightlessSync/LightlessPlugin.cs +++ b/LightlessSync/LightlessPlugin.cs @@ -155,12 +155,12 @@ public class LightlessPlugin : MediatorSubscriberBase, IHostedService _runtimeServiceScope.ServiceProvider.GetRequiredService(); _runtimeServiceScope.ServiceProvider.GetRequiredService(); - // TODO: move this to a better place var ver = Assembly.GetExecutingAssembly().GetName().Version; 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))); + // for testing c: + // Mediator.Publish(new UiToggleMessage(typeof(UpdateNotesUi))); if (string.IsNullOrEmpty(lastSeen)) { From 8fdff1eb18cd66be7f2320918f5dd05e3b156c93 Mon Sep 17 00:00:00 2001 From: choco Date: Thu, 16 Oct 2025 23:03:32 +0200 Subject: [PATCH 10/16] SHOWING changelog everytime till the got it button is pressed, should reappear on version updates according to the current settings --- LightlessSync/LightlessPlugin.cs | 20 ++++++-------------- LightlessSync/UI/UpdateNotesUi.cs | 8 ++++++++ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/LightlessSync/LightlessPlugin.cs b/LightlessSync/LightlessPlugin.cs index 617cfe3..9dc0f99 100644 --- a/LightlessSync/LightlessPlugin.cs +++ b/LightlessSync/LightlessPlugin.cs @@ -159,22 +159,14 @@ 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); - // for testing c: - // Mediator.Publish(new UiToggleMessage(typeof(UpdateNotesUi))); - if (string.IsNullOrEmpty(lastSeen)) + // Show update notes if version has changed and user has valid setup + if (!string.IsNullOrEmpty(lastSeen) && + !string.Equals(lastSeen, currentVersion, StringComparison.Ordinal) && + _lightlessConfigService.Current.HasValidSetup() && + _serverConfigurationManager.HasValidConfig()) { - _lightlessConfigService.Current.LastSeenVersion = currentVersion; - _lightlessConfigService.Save(); - } - else if (!string.Equals(lastSeen, currentVersion, StringComparison.Ordinal)) - { - if (_lightlessConfigService.Current.HasValidSetup() && _serverConfigurationManager.HasValidConfig()) - { - Mediator.Publish(new UiToggleMessage(typeof(UpdateNotesUi))); - } - _lightlessConfigService.Current.LastSeenVersion = currentVersion; - _lightlessConfigService.Save(); + Mediator.Publish(new UiToggleMessage(typeof(UpdateNotesUi))); } #if !DEBUG diff --git a/LightlessSync/UI/UpdateNotesUi.cs b/LightlessSync/UI/UpdateNotesUi.cs index f25c38a..ab35177 100644 --- a/LightlessSync/UI/UpdateNotesUi.cs +++ b/LightlessSync/UI/UpdateNotesUi.cs @@ -20,6 +20,7 @@ namespace LightlessSync.UI; public class UpdateNotesUi : WindowMediatorSubscriberBase { private readonly UiSharedService _uiShared; + private readonly LightlessConfigService _configService; private ChangelogFile _changelog = new(); private CreditsFile _credits = new(); @@ -65,6 +66,7 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase : base(logger, mediator, "Lightless Sync — Update Notes", performanceCollectorService) { _uiShared = uiShared; + _configService = configService; AllowClickthrough = false; AllowPinning = false; @@ -576,6 +578,12 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase { 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; } } From 2d094404df5279c138eb1e234824977b1e4468bd Mon Sep 17 00:00:00 2001 From: choco Date: Thu, 16 Oct 2025 23:21:14 +0200 Subject: [PATCH 11/16] proper version checker on plugin laoding --- LightlessSync/LightlessPlugin.cs | 36 +++++++++++++++++++------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/LightlessSync/LightlessPlugin.cs b/LightlessSync/LightlessPlugin.cs index 9dc0f99..327c4a6 100644 --- a/LightlessSync/LightlessPlugin.cs +++ b/LightlessSync/LightlessPlugin.cs @@ -9,6 +9,7 @@ using LightlessSync.UI; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Serilog; using System.Reflection; namespace LightlessSync; @@ -101,6 +102,8 @@ public class LightlessPlugin : MediatorSubscriberBase, IHostedService UIColors.Initialize(_lightlessConfigService); Mediator.StartQueueProcessing(); + + CheckVersion(); return Task.CompletedTask; } @@ -115,6 +118,24 @@ public class LightlessPlugin : MediatorSubscriberBase, IHostedService return Task.CompletedTask; } + + private void CheckVersion() + { + var ver = Assembly.GetExecutingAssembly().GetName().Version; + var currentVersion = ver == null ? string.Empty : $"{ver.Major}.{ver.Minor}.{ver.Build}"; + var lastSeen = _lightlessConfigService.Current.LastSeenVersion ?? string.Empty; + Logger.LogInformation("Last seen version: {lastSeen}, current version: {currentVersion}", lastSeen, currentVersion); + Logger.LogInformation("User has valid setup: {hasValidSetup}", _lightlessConfigService.Current.HasValidSetup()); + Logger.LogInformation("Server has valid config: {hasValidConfig}", _serverConfigurationManager.HasValidConfig()); + // Show update notes if version has changed and user has valid setup + if (!string.IsNullOrEmpty(lastSeen) && + !string.Equals(lastSeen, currentVersion, StringComparison.Ordinal) && + _lightlessConfigService.Current.HasValidSetup() && + _serverConfigurationManager.HasValidConfig()) + { + Mediator.Publish(new UiToggleMessage(typeof(UpdateNotesUi))); + } + } private void DalamudUtilOnLogIn() { @@ -154,21 +175,6 @@ public class LightlessPlugin : MediatorSubscriberBase, IHostedService _runtimeServiceScope.ServiceProvider.GetRequiredService(); _runtimeServiceScope.ServiceProvider.GetRequiredService(); _runtimeServiceScope.ServiceProvider.GetRequiredService(); - - var ver = Assembly.GetExecutingAssembly().GetName().Version; - 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); - - // Show update notes if version has changed and user has valid setup - if (!string.IsNullOrEmpty(lastSeen) && - !string.Equals(lastSeen, currentVersion, StringComparison.Ordinal) && - _lightlessConfigService.Current.HasValidSetup() && - _serverConfigurationManager.HasValidConfig()) - { - Mediator.Publish(new UiToggleMessage(typeof(UpdateNotesUi))); - } - #if !DEBUG if (_lightlessConfigService.Current.LogLevel != LogLevel.Information) { From f1af6601cc87461f9063b0e97b3764248193f25a Mon Sep 17 00:00:00 2001 From: choco Date: Thu, 16 Oct 2025 23:30:06 +0200 Subject: [PATCH 12/16] removed null check --- LightlessSync/LightlessPlugin.cs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/LightlessSync/LightlessPlugin.cs b/LightlessSync/LightlessPlugin.cs index 327c4a6..06cb3ef 100644 --- a/LightlessSync/LightlessPlugin.cs +++ b/LightlessSync/LightlessPlugin.cs @@ -128,12 +128,19 @@ public class LightlessPlugin : MediatorSubscriberBase, IHostedService Logger.LogInformation("User has valid setup: {hasValidSetup}", _lightlessConfigService.Current.HasValidSetup()); Logger.LogInformation("Server has valid config: {hasValidConfig}", _serverConfigurationManager.HasValidConfig()); // Show update notes if version has changed and user has valid setup - if (!string.IsNullOrEmpty(lastSeen) && - !string.Equals(lastSeen, currentVersion, StringComparison.Ordinal) && + if (!string.Equals(lastSeen, currentVersion, StringComparison.Ordinal) && _lightlessConfigService.Current.HasValidSetup() && _serverConfigurationManager.HasValidConfig()) { - Mediator.Publish(new UiToggleMessage(typeof(UpdateNotesUi))); + // Update the last seen version to current version + _lightlessConfigService.Current.LastSeenVersion = currentVersion; + _lightlessConfigService.Save(); + + // Only show update notes if this isn't the first run + if (!string.IsNullOrEmpty(lastSeen)) + { + Mediator.Publish(new UiToggleMessage(typeof(UpdateNotesUi))); + } } } From ea8f8e389573d63bbb52e7d5ad21d39713bf2d51 Mon Sep 17 00:00:00 2001 From: choco Date: Thu, 16 Oct 2025 23:34:10 +0200 Subject: [PATCH 13/16] last null check removal --- LightlessSync/LightlessPlugin.cs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/LightlessSync/LightlessPlugin.cs b/LightlessSync/LightlessPlugin.cs index 06cb3ef..5d74e01 100644 --- a/LightlessSync/LightlessPlugin.cs +++ b/LightlessSync/LightlessPlugin.cs @@ -132,15 +132,7 @@ public class LightlessPlugin : MediatorSubscriberBase, IHostedService _lightlessConfigService.Current.HasValidSetup() && _serverConfigurationManager.HasValidConfig()) { - // Update the last seen version to current version - _lightlessConfigService.Current.LastSeenVersion = currentVersion; - _lightlessConfigService.Save(); - - // Only show update notes if this isn't the first run - if (!string.IsNullOrEmpty(lastSeen)) - { - Mediator.Publish(new UiToggleMessage(typeof(UpdateNotesUi))); - } + Mediator.Publish(new UiToggleMessage(typeof(UpdateNotesUi))); } } From 8a3902ec2b38bd0643d0e712adb7759c99d4f461 Mon Sep 17 00:00:00 2001 From: choco Date: Thu, 16 Oct 2025 23:44:15 +0200 Subject: [PATCH 14/16] init change :) --- LightlessSync/LightlessPlugin.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/LightlessSync/LightlessPlugin.cs b/LightlessSync/LightlessPlugin.cs index 5d74e01..fe7e9a4 100644 --- a/LightlessSync/LightlessPlugin.cs +++ b/LightlessSync/LightlessPlugin.cs @@ -103,8 +103,6 @@ public class LightlessPlugin : MediatorSubscriberBase, IHostedService UIColors.Initialize(_lightlessConfigService); Mediator.StartQueueProcessing(); - CheckVersion(); - return Task.CompletedTask; } @@ -128,6 +126,7 @@ public class LightlessPlugin : MediatorSubscriberBase, IHostedService Logger.LogInformation("User has valid setup: {hasValidSetup}", _lightlessConfigService.Current.HasValidSetup()); Logger.LogInformation("Server has valid config: {hasValidConfig}", _serverConfigurationManager.HasValidConfig()); // Show update notes if version has changed and user has valid setup + if (!string.Equals(lastSeen, currentVersion, StringComparison.Ordinal) && _lightlessConfigService.Current.HasValidSetup() && _serverConfigurationManager.HasValidConfig()) @@ -174,6 +173,8 @@ public class LightlessPlugin : MediatorSubscriberBase, IHostedService _runtimeServiceScope.ServiceProvider.GetRequiredService(); _runtimeServiceScope.ServiceProvider.GetRequiredService(); _runtimeServiceScope.ServiceProvider.GetRequiredService(); + CheckVersion(); + #if !DEBUG if (_lightlessConfigService.Current.LogLevel != LogLevel.Information) { From 47b7ecd5214e313f3eb0211efeaa2879143eca94 Mon Sep 17 00:00:00 2001 From: choco Date: Fri, 17 Oct 2025 15:36:50 +0200 Subject: [PATCH 15/16] forced current changelog version to be opened on default --- LightlessSync/Changelog/changelog.yaml | 11 ++------ LightlessSync/UI/UpdateNotesUi.cs | 35 ++++++++++++++++---------- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/LightlessSync/Changelog/changelog.yaml b/LightlessSync/Changelog/changelog.yaml index 98134a1..2cdbf6a 100644 --- a/LightlessSync/Changelog/changelog.yaml +++ b/LightlessSync/Changelog/changelog.yaml @@ -4,7 +4,8 @@ changelog: - name: "v1.12.3" tagline: "FILLER" date: "October 15th 2025" - is_current: true + # be sure to set this every new version + isCurrent: true versions: - number: "New Features" icon: "" @@ -22,7 +23,6 @@ changelog: - name: "v1.12.2" tagline: "LightFinder fixes, Notifications overhaul" date: "October 12th 2025" - is_current: false versions: - number: "LightFinder" icon: "" @@ -50,7 +50,6 @@ changelog: - name: "v1.12.1" tagline: "LightFinder customization and download limiter" date: "October 8th 2025" - is_current: false versions: - number: "New Features" icon: "" @@ -69,7 +68,6 @@ changelog: - name: "v1.12.0" tagline: "LightFinder - Major feature release" date: "October 5th 2025" - is_current: false versions: - number: "Major Features" icon: "" @@ -96,7 +94,6 @@ changelog: - name: "v1.11.12" tagline: "Syncshell grouping and performance options" date: "September 16th 2025" - is_current: false versions: - number: "New Features" icon: "" @@ -118,7 +115,6 @@ changelog: - name: "v1.11.9" tagline: "File cache improvements" date: "September 13th 2025" - is_current: false versions: - number: "Bug Fixes" icon: "" @@ -129,7 +125,6 @@ changelog: - name: "v1.11.8" tagline: "Hotfix - UI and exception handling" date: "September 12th 2025" - is_current: false versions: - number: "Bug Fixes" icon: "" @@ -141,7 +136,6 @@ changelog: - name: "v1.11.7" tagline: "Hotfix - UI loading and warnings" date: "September 12th 2025" - is_current: false versions: - number: "Bug Fixes" icon: "" @@ -152,7 +146,6 @@ changelog: - name: "v1.11.6" tagline: "Admin panel rework and new features" date: "September 11th 2025" - is_current: false versions: - number: "New Features" icon: "" diff --git a/LightlessSync/UI/UpdateNotesUi.cs b/LightlessSync/UI/UpdateNotesUi.cs index ab35177..57dd173 100644 --- a/LightlessSync/UI/UpdateNotesUi.cs +++ b/LightlessSync/UI/UpdateNotesUi.cs @@ -26,6 +26,7 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase private CreditsFile _credits = new(); private bool _scrollToTop; private int _selectedTab; + private bool _hasInitializedCollapsingHeaders; private struct Particle { @@ -65,6 +66,7 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase PerformanceCollectorService performanceCollectorService) : base(logger, mediator, "Lightless Sync — Update Notes", performanceCollectorService) { + logger.LogInformation("UpdateNotesUi constructor called"); _uiShared = uiShared; _configService = configService; @@ -82,11 +84,13 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase }; LoadEmbeddedResources(); + logger.LogInformation("UpdateNotesUi constructor completed successfully"); } public override void OnOpen() { _scrollToTop = true; + _hasInitializedCollapsingHeaders = false; } protected override void DrawInternal() @@ -108,7 +112,6 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase 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); @@ -610,6 +613,8 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase { DrawChangelogEntry(entry); } + + _hasInitializedCollapsingHeaders = true; ImGui.PopTextWrapPos(); ImGui.Spacing(); @@ -618,28 +623,32 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase private void DrawChangelogEntry(ChangelogEntry entry) { - var currentColor = entry.IsCurrent == true + var isCurrent = entry.IsCurrent ?? false; + + var currentColor = isCurrent ? UIColors.Get("LightlessGreen") : new Vector4(0.95f, 0.95f, 1.0f, 1.0f); - - var flags = entry.IsCurrent == true - ? ImGuiTreeNodeFlags.DefaultOpen - : ImGuiTreeNodeFlags.None; - + + 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.HeaderHovered, UIColors.Get("LightlessPurple"))) - using (ImRaii.PushColor(ImGuiCol.HeaderActive, UIColors.Get("LightlessPurpleActive"))) using (ImRaii.PushColor(ImGuiCol.Text, currentColor)) { - var isOpen = ImGui.CollapsingHeader($" {entry.Name} — {entry.Date} ", flags); + 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; } + + if (!isOpen) + return; ImGuiHelpers.ScaledDummy(8); From 44177ab7bd03799fa388ee299d94e62ca1df1e4e Mon Sep 17 00:00:00 2001 From: choco Date: Fri, 17 Oct 2025 20:07:14 +0200 Subject: [PATCH 16/16] broadcast bypass toggle gone --- LightlessSync/Services/BroadcastService.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/LightlessSync/Services/BroadcastService.cs b/LightlessSync/Services/BroadcastService.cs index cad09dd..81e54d5 100644 --- a/LightlessSync/Services/BroadcastService.cs +++ b/LightlessSync/Services/BroadcastService.cs @@ -403,7 +403,7 @@ public class BroadcastService : IHostedService, IMediatorSubscriber - public async void ToggleBroadcast(bool bypassCooldown = false) + public async void ToggleBroadcast() { if (!IsLightFinderAvailable) @@ -419,7 +419,7 @@ public class BroadcastService : IHostedService, IMediatorSubscriber await RequireConnectionAsync(nameof(ToggleBroadcast), async () => { var cooldown = RemainingCooldown; - if (!bypassCooldown && !_config.Current.BroadcastEnabled && cooldown is { } cd && cd > TimeSpan.Zero) + if (!_config.Current.BroadcastEnabled && cooldown is { } cd && cd > TimeSpan.Zero) { _logger.LogWarning("Cooldown active. Must wait {Remaining}s before re-enabling.", cd.TotalSeconds); _mediator.Publish(new NotificationMessage( @@ -556,7 +556,7 @@ public class BroadcastService : IHostedService, IMediatorSubscriber OnClick = (n) => { _logger.LogInformation("Re-enabling broadcast from notification"); - ToggleBroadcast(bypassCooldown: true); + ToggleBroadcast(); n.IsDismissed = true; n.IsAnimatingOut = true; }