Files
LightlessClient/LightlessSync/UI/UpdateNotesUi.cs
2025-10-15 22:59:08 +02:00

809 lines
30 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
using Dalamud.Interface;
namespace LightlessSync.UI;
public class UpdateNotesUi : WindowMediatorSubscriberBase
{
private readonly LightlessConfigService _configService;
private readonly UiSharedService _uiShared;
private readonly List<string> _contributors = [];
private readonly List<string> _credits = [];
private readonly List<string> _supporters = [];
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<Vector2>? Trail;
public bool IsLargeMoon;
}
private enum ParticleType
{
Star,
Moon,
Sparkle,
FastFallingStar
}
private readonly List<Particle> _particles = [];
private float _particleTimer;
private readonly Random _particleRandom = new();
private Particle? _largeMoon;
private float _largeMoonTimer;
public UpdateNotesUi(ILogger<UpdateNotesUi> 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 | ImGuiWindowFlags.NoTitleBar;
SizeConstraints = new WindowSizeConstraints()
{
MinimumSize = new Vector2(800, 700),
MaximumSize = new Vector2(800, 700),
};
LoadEmbeddedResources();
}
public override void OnOpen()
{
_scrollToTop = true;
}
protected override void DrawInternal()
{
if (_uiShared.IsInGpose)
return;
DrawHeader();
ImGuiHelpers.ScaledDummy(6);
DrawChangelog();
DrawCloseButton();
}
private void DrawHeader()
{
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.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}");
}
}
ImGui.Separator();
ImGuiHelpers.ScaledDummy(3);
}
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);
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<Vector2>() : 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()
{
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)))
{
IsOpen = false;
}
}
}
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);
ImGui.PopTextWrapPos();
ImGui.Spacing();
}
}
private void DrawChangelogEntry(ChangelogEntry entry)
{
var currentColor = entry.IsCurrent == true
? 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} ", flags);
}
ImGui.SameLine();
ImGui.TextColored(new Vector4(0.75f, 0.75f, 0.85f, 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);
}
}
ImGuiHelpers.ScaledDummy(8);
}
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()
{
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<ChangelogFile>(yaml) ?? new();
}
}
catch
{
// Ignore - window will gracefully render with defaults
}
}
private static void ReadLines(Assembly assembly, string resourceName, List<string> 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<ChangelogEntry> 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<ChangelogVersion>? Versions { get; init; }
}
private sealed record ChangelogVersion
{
public string Number { get; init; } = string.Empty;
public List<string> Items { get; init; } = new();
}
}