768 lines
26 KiB
C#
768 lines
26 KiB
C#
using Dalamud.Bindings.ImGui;
|
||
using Dalamud.Interface.Utility;
|
||
using Dalamud.Interface.Utility.Raii;
|
||
using Dalamud.Utility;
|
||
using LightlessSync.LightlessConfiguration;
|
||
using LightlessSync.Services;
|
||
using LightlessSync.Services.Mediator;
|
||
using Microsoft.Extensions.Logging;
|
||
using System.Numerics;
|
||
using System.Reflection;
|
||
using System.Text;
|
||
using YamlDotNet.Serialization;
|
||
using YamlDotNet.Serialization.NamingConventions;
|
||
using Dalamud.Interface;
|
||
using LightlessSync.UI.Models;
|
||
|
||
namespace LightlessSync.UI;
|
||
|
||
// Inspiration taken from Brio and Character Select+ (goats)
|
||
public class UpdateNotesUi : WindowMediatorSubscriberBase
|
||
{
|
||
private readonly UiSharedService _uiShared;
|
||
private readonly LightlessConfigService _configService;
|
||
|
||
private ChangelogFile _changelog = new();
|
||
private CreditsFile _credits = new();
|
||
private bool _scrollToTop;
|
||
private int _selectedTab;
|
||
private bool _hasInitializedCollapsingHeaders;
|
||
|
||
private struct Particle
|
||
{
|
||
public Vector2 Position;
|
||
public Vector2 Velocity;
|
||
public float Life;
|
||
public float MaxLife;
|
||
public float Size;
|
||
public ParticleType Type;
|
||
public List<Vector2>? Trail;
|
||
public float Twinkle;
|
||
public float Depth;
|
||
public float Hue;
|
||
}
|
||
|
||
private enum ParticleType
|
||
{
|
||
TwinklingStar,
|
||
ShootingStar
|
||
}
|
||
|
||
private readonly List<Particle> _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;
|
||
private const int _maxTrailLength = 50;
|
||
private const float _edgeFadeDistance = 30f;
|
||
private const float _extendedParticleHeight = 40f;
|
||
|
||
public UpdateNotesUi(ILogger<UpdateNotesUi> logger,
|
||
LightlessMediator mediator,
|
||
UiSharedService uiShared,
|
||
LightlessConfigService configService,
|
||
PerformanceCollectorService performanceCollectorService)
|
||
: base(logger, mediator, "Lightless Sync — Update Notes", performanceCollectorService)
|
||
{
|
||
logger.LogInformation("UpdateNotesUi constructor called");
|
||
_uiShared = uiShared;
|
||
_configService = configService;
|
||
|
||
AllowClickthrough = false;
|
||
AllowPinning = false;
|
||
RespectCloseHotkey = true;
|
||
ShowCloseButton = true;
|
||
|
||
Flags = ImGuiWindowFlags.NoSavedSettings | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoCollapse |
|
||
ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoMove;
|
||
|
||
SizeConstraints = new WindowSizeConstraints()
|
||
{
|
||
MinimumSize = new Vector2(800, 700), MaximumSize = new Vector2(800, 700),
|
||
};
|
||
|
||
PositionCondition = ImGuiCond.Always;
|
||
|
||
LoadEmbeddedResources();
|
||
logger.LogInformation("UpdateNotesUi constructor completed successfully");
|
||
}
|
||
|
||
public override void OnOpen()
|
||
{
|
||
_scrollToTop = true;
|
||
_hasInitializedCollapsingHeaders = false;
|
||
}
|
||
|
||
private void CenterWindow()
|
||
{
|
||
var viewport = ImGui.GetMainViewport();
|
||
var center = viewport.GetCenter();
|
||
var windowSize = new Vector2(800f * ImGuiHelpers.GlobalScale, 700f * ImGuiHelpers.GlobalScale);
|
||
Position = center - windowSize / 2f;
|
||
}
|
||
|
||
protected override void DrawInternal()
|
||
{
|
||
if (_uiShared.IsInGpose)
|
||
return;
|
||
|
||
CenterWindow();
|
||
DrawHeader();
|
||
ImGuiHelpers.ScaledDummy(6);
|
||
DrawTabs();
|
||
DrawCloseButton();
|
||
}
|
||
|
||
private void DrawHeader()
|
||
{
|
||
var 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 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,
|
||
ImGui.GetColorU32(darkPurple),
|
||
ImGui.GetColorU32(darkPurple),
|
||
ImGui.GetColorU32(deepPurple),
|
||
ImGui.GetColorU32(deepPurple)
|
||
);
|
||
|
||
var random = new Random(42);
|
||
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)));
|
||
}
|
||
}
|
||
|
||
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;
|
||
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)
|
||
{
|
||
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);
|
||
var spacing = 8f * ImGuiHelpers.GlobalScale;
|
||
var rightPadding = 15f * ImGuiHelpers.GlobalScale;
|
||
var topPadding = 15f * ImGuiHelpers.GlobalScale;
|
||
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;
|
||
_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)
|
||
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++)
|
||
{
|
||
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],
|
||
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
|
||
);
|
||
}
|
||
}
|
||
else if (particle.Type == ParticleType.TwinklingStar)
|
||
{
|
||
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)
|
||
{
|
||
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;
|
||
}
|
||
|
||
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,
|
||
Velocity = velocity,
|
||
Life = maxLife,
|
||
MaxLife = maxLife,
|
||
Size = size,
|
||
Type = ParticleType.TwinklingStar,
|
||
Trail = null,
|
||
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<Vector2>(),
|
||
Twinkle = 0,
|
||
Depth = 1.0f,
|
||
Hue = 270f
|
||
});
|
||
}
|
||
|
||
|
||
private void DrawTabs()
|
||
{
|
||
using (ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 6f))
|
||
using (ImRaii.PushColor(ImGuiCol.Tab, UIColors.Get("ButtonDefault")))
|
||
using (ImRaii.PushColor(ImGuiCol.TabHovered, UIColors.Get("LightlessPurple")))
|
||
using (ImRaii.PushColor(ImGuiCol.TabActive, UIColors.Get("LightlessPurpleActive")))
|
||
{
|
||
using (var tabBar = ImRaii.TabBar("###ll_tabs", ImGuiTabBarFlags.None))
|
||
{
|
||
if (!tabBar)
|
||
return;
|
||
|
||
using (var changelogTab = ImRaii.TabItem("Changelog"))
|
||
{
|
||
if (changelogTab)
|
||
{
|
||
_selectedTab = 0;
|
||
DrawChangelog();
|
||
}
|
||
}
|
||
|
||
if (_credits.Credits != null && _credits.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 (_credits.Credits != null)
|
||
{
|
||
foreach (var category in _credits.Credits)
|
||
{
|
||
DrawCreditCategory(category);
|
||
ImGuiHelpers.ScaledDummy(10);
|
||
}
|
||
}
|
||
|
||
ImGui.PopTextWrapPos();
|
||
ImGui.Spacing();
|
||
}
|
||
}
|
||
|
||
private void DrawCreditCategory(CreditCategory category)
|
||
{
|
||
DrawFeatureSection(category.Category, UIColors.Get("LightlessBlue"));
|
||
|
||
foreach (var item in category.Items)
|
||
{
|
||
if (!string.IsNullOrEmpty(item.Role))
|
||
{
|
||
ImGui.BulletText($"{item.Name} — {item.Role}");
|
||
}
|
||
else
|
||
{
|
||
ImGui.BulletText(item.Name);
|
||
}
|
||
}
|
||
|
||
ImGuiHelpers.ScaledDummy(5);
|
||
}
|
||
|
||
private void DrawCloseButton()
|
||
{
|
||
ImGuiHelpers.ScaledDummy(5);
|
||
|
||
var closeWidth = 200f * ImGuiHelpers.GlobalScale;
|
||
var closeHeight = 35f * ImGuiHelpers.GlobalScale;
|
||
ImGui.SetCursorPosX((ImGui.GetWindowSize().X - closeWidth) / 2);
|
||
|
||
using (ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 8f))
|
||
using (ImRaii.PushColor(ImGuiCol.Button, UIColors.Get("LightlessPurple")))
|
||
using (ImRaii.PushColor(ImGuiCol.ButtonHovered, UIColors.Get("LightlessPurpleActive")))
|
||
using (ImRaii.PushColor(ImGuiCol.ButtonActive, UIColors.Get("ButtonDefault")))
|
||
{
|
||
if (ImGui.Button("Got it!", new Vector2(closeWidth, closeHeight)))
|
||
{
|
||
// Update last seen version when user acknowledges the update notes
|
||
var ver = Assembly.GetExecutingAssembly().GetName().Version;
|
||
var currentVersion = ver == null ? string.Empty : $"{ver.Major}.{ver.Minor}.{ver.Build}";
|
||
_configService.Current.LastSeenVersion = currentVersion;
|
||
_configService.Save();
|
||
|
||
IsOpen = false;
|
||
}
|
||
|
||
if (ImGui.IsItemHovered())
|
||
{
|
||
ImGui.SetTooltip("You can view this window again in the settings (title menu)");
|
||
}
|
||
}
|
||
}
|
||
|
||
private void DrawChangelog()
|
||
{
|
||
using (ImRaii.PushStyle(ImGuiStyleVar.ChildRounding, 6f))
|
||
using (var child = ImRaii.Child("###ll_changelog", new Vector2(0, ImGui.GetContentRegionAvail().Y - 60), false,
|
||
ImGuiWindowFlags.AlwaysVerticalScrollbar))
|
||
{
|
||
if (!child)
|
||
return;
|
||
|
||
if (_scrollToTop)
|
||
{
|
||
_scrollToTop = false;
|
||
ImGui.SetScrollHereY(0);
|
||
}
|
||
|
||
ImGui.PushTextWrapPos();
|
||
|
||
foreach (var entry in _changelog.Changelog)
|
||
{
|
||
DrawChangelogEntry(entry);
|
||
}
|
||
|
||
_hasInitializedCollapsingHeaders = true;
|
||
|
||
ImGui.PopTextWrapPos();
|
||
ImGui.Spacing();
|
||
}
|
||
}
|
||
|
||
private void DrawChangelogEntry(ChangelogEntry entry)
|
||
{
|
||
var isCurrent = entry.IsCurrent ?? false;
|
||
|
||
var currentColor = isCurrent
|
||
? UIColors.Get("LightlessGreen")
|
||
: new Vector4(0.95f, 0.95f, 1.0f, 1.0f);
|
||
|
||
var flags = isCurrent ? ImGuiTreeNodeFlags.DefaultOpen : ImGuiTreeNodeFlags.None;
|
||
|
||
if (!_hasInitializedCollapsingHeaders)
|
||
{
|
||
ImGui.SetNextItemOpen(isCurrent, ImGuiCond.Always);
|
||
}
|
||
|
||
bool isOpen;
|
||
using (ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 4f))
|
||
using (ImRaii.PushColor(ImGuiCol.Header, UIColors.Get("ButtonDefault")))
|
||
using (ImRaii.PushColor(ImGuiCol.Text, currentColor))
|
||
{
|
||
isOpen = ImGui.CollapsingHeader($" {entry.Name} — {entry.Date} ", flags);
|
||
|
||
ImGui.SameLine();
|
||
ImGui.TextColored(new Vector4(0.85f, 0.85f, 0.95f, 1.0f), $" — {entry.Tagline}");
|
||
}
|
||
|
||
if (!isOpen)
|
||
return;
|
||
|
||
ImGuiHelpers.ScaledDummy(8);
|
||
|
||
if (!string.IsNullOrEmpty(entry.Message))
|
||
{
|
||
ImGui.TextWrapped(entry.Message);
|
||
ImGuiHelpers.ScaledDummy(8);
|
||
return;
|
||
}
|
||
|
||
if (entry.Versions != null)
|
||
{
|
||
foreach (var version in entry.Versions)
|
||
{
|
||
DrawFeatureSection(version.Number, UIColors.Get("LightlessGreen"));
|
||
|
||
foreach (var item in version.Items)
|
||
{
|
||
ImGui.BulletText(item);
|
||
}
|
||
|
||
ImGuiHelpers.ScaledDummy(5);
|
||
}
|
||
}
|
||
}
|
||
|
||
private static void DrawFeatureSection(string title, Vector4 accentColor)
|
||
{
|
||
var drawList = ImGui.GetWindowDrawList();
|
||
var startPos = ImGui.GetCursorScreenPos();
|
||
var availableWidth = ImGui.GetContentRegionAvail().X;
|
||
|
||
var backgroundMin = startPos + new Vector2(-8, -4);
|
||
var backgroundMax = startPos + new Vector2(availableWidth + 8, 28);
|
||
|
||
var bgColor = new Vector4(0.12f, 0.12f, 0.15f, 0.7f);
|
||
drawList.AddRectFilled(backgroundMin, backgroundMax, ImGui.GetColorU32(bgColor), 6f);
|
||
|
||
drawList.AddRectFilled(
|
||
backgroundMin,
|
||
backgroundMin + new Vector2(4, backgroundMax.Y - backgroundMin.Y),
|
||
ImGui.GetColorU32(accentColor),
|
||
3f
|
||
);
|
||
|
||
var glowColor = accentColor with { W = 0.15f };
|
||
drawList.AddRect(
|
||
backgroundMin,
|
||
backgroundMax,
|
||
ImGui.GetColorU32(glowColor),
|
||
6f,
|
||
ImDrawFlags.None,
|
||
1.5f
|
||
);
|
||
|
||
// Calculate vertical centering
|
||
var textSize = ImGui.CalcTextSize(title);
|
||
var boxHeight = backgroundMax.Y - backgroundMin.Y;
|
||
var verticalOffset = (boxHeight - textSize.Y) / 5f;
|
||
|
||
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + 8);
|
||
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + verticalOffset);
|
||
ImGui.TextColored(accentColor, title);
|
||
ImGui.SetCursorPosY(backgroundMax.Y - startPos.Y + ImGui.GetCursorPosY());
|
||
}
|
||
|
||
private void LoadEmbeddedResources()
|
||
{
|
||
try
|
||
{
|
||
var assembly = Assembly.GetExecutingAssembly();
|
||
var deserializer = new DeserializerBuilder()
|
||
.WithNamingConvention(CamelCaseNamingConvention.Instance)
|
||
.IgnoreUnmatchedProperties()
|
||
.Build();
|
||
|
||
// Load changelog
|
||
using var changelogStream = assembly.GetManifestResourceStream("LightlessSync.Changelog.changelog.yaml");
|
||
if (changelogStream != null)
|
||
{
|
||
using var reader = new StreamReader(changelogStream, Encoding.UTF8, true, 128);
|
||
var yaml = reader.ReadToEnd();
|
||
_changelog = deserializer.Deserialize<ChangelogFile>(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<CreditsFile>(yaml) ?? new();
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
// Ignore - window will gracefully render with defaults
|
||
}
|
||
}
|
||
}
|