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(); + } +}