Merge pull request 'patch-notes' (#66) from patch-notes into 1.12.3
Reviewed-on: #66
This commit was merged in pull request #66.
This commit is contained in:
167
LightlessSync/Changelog/changelog.yaml
Normal file
167
LightlessSync/Changelog/changelog.yaml
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
tagline: "Lightless Sync v1.12.3"
|
||||||
|
subline: "FILLER"
|
||||||
|
changelog:
|
||||||
|
- name: "v1.12.3"
|
||||||
|
tagline: "FILLER"
|
||||||
|
date: "October 15th 2025"
|
||||||
|
# be sure to set this every new version
|
||||||
|
isCurrent: 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"
|
||||||
|
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"
|
||||||
|
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"
|
||||||
|
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"
|
||||||
|
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"
|
||||||
|
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"
|
||||||
|
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"
|
||||||
|
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"
|
||||||
|
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."
|
||||||
|
- "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."
|
||||||
|
- "Fixed bug where some users could not see their own syncshell folders."
|
||||||
35
LightlessSync/Changelog/credits.yaml
Normal file
35
LightlessSync/Changelog/credits.yaml
Normal file
@@ -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"
|
||||||
@@ -147,4 +147,5 @@ public class LightlessConfig : ILightlessConfiguration
|
|||||||
public DateTime BroadcastTtl { get; set; } = DateTime.MinValue;
|
public DateTime BroadcastTtl { get; set; } = DateTime.MinValue;
|
||||||
public bool SyncshellFinderEnabled { get; set; } = false;
|
public bool SyncshellFinderEnabled { get; set; } = false;
|
||||||
public string? SelectedFinderSyncshell { get; set; } = null;
|
public string? SelectedFinderSyncshell { get; set; } = null;
|
||||||
|
public string LastSeenVersion { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ using LightlessSync.UI;
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Serilog;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
|
||||||
namespace LightlessSync;
|
namespace LightlessSync;
|
||||||
@@ -116,6 +117,24 @@ public class LightlessPlugin : MediatorSubscriberBase, IHostedService
|
|||||||
return Task.CompletedTask;
|
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.Equals(lastSeen, currentVersion, StringComparison.Ordinal) &&
|
||||||
|
_lightlessConfigService.Current.HasValidSetup() &&
|
||||||
|
_serverConfigurationManager.HasValidConfig())
|
||||||
|
{
|
||||||
|
Mediator.Publish(new UiToggleMessage(typeof(UpdateNotesUi)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void DalamudUtilOnLogIn()
|
private void DalamudUtilOnLogIn()
|
||||||
{
|
{
|
||||||
Logger?.LogDebug("Client login");
|
Logger?.LogDebug("Client login");
|
||||||
@@ -154,6 +173,7 @@ public class LightlessPlugin : MediatorSubscriberBase, IHostedService
|
|||||||
_runtimeServiceScope.ServiceProvider.GetRequiredService<VisibleUserDataDistributor>();
|
_runtimeServiceScope.ServiceProvider.GetRequiredService<VisibleUserDataDistributor>();
|
||||||
_runtimeServiceScope.ServiceProvider.GetRequiredService<NotificationService>();
|
_runtimeServiceScope.ServiceProvider.GetRequiredService<NotificationService>();
|
||||||
_runtimeServiceScope.ServiceProvider.GetRequiredService<NameplateService>();
|
_runtimeServiceScope.ServiceProvider.GetRequiredService<NameplateService>();
|
||||||
|
CheckVersion();
|
||||||
|
|
||||||
#if !DEBUG
|
#if !DEBUG
|
||||||
if (_lightlessConfigService.Current.LogLevel != LogLevel.Information)
|
if (_lightlessConfigService.Current.LogLevel != LogLevel.Information)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<Project Sdk="Dalamud.NET.Sdk/13.1.0">
|
<Project Sdk="Dalamud.NET.Sdk/13.1.0">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors></Authors>
|
<Authors></Authors>
|
||||||
@@ -46,6 +46,7 @@
|
|||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.7.0" />
|
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.7.0" />
|
||||||
|
<PackageReference Include="YamlDotNet" Version="16.3.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
@@ -64,6 +65,8 @@
|
|||||||
<None Update="images\icon.png">
|
<None Update="images\icon.png">
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
</None>
|
</None>
|
||||||
|
<EmbeddedResource Include="Changelog\changelog.yaml" />
|
||||||
|
<EmbeddedResource Include="Changelog\credits.yaml" />
|
||||||
<EmbeddedResource Include="Localization\de.json" />
|
<EmbeddedResource Include="Localization\de.json" />
|
||||||
<EmbeddedResource Include="Localization\fr.json" />
|
<EmbeddedResource Include="Localization\fr.json" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -246,6 +246,7 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
collection.AddScoped<WindowMediatorSubscriberBase, CreateSyncshellUI>();
|
collection.AddScoped<WindowMediatorSubscriberBase, CreateSyncshellUI>();
|
||||||
collection.AddScoped<WindowMediatorSubscriberBase, EventViewerUI>();
|
collection.AddScoped<WindowMediatorSubscriberBase, EventViewerUI>();
|
||||||
collection.AddScoped<WindowMediatorSubscriberBase, CharaDataHubUi>();
|
collection.AddScoped<WindowMediatorSubscriberBase, CharaDataHubUi>();
|
||||||
|
collection.AddScoped<WindowMediatorSubscriberBase, UpdateNotesUi>();
|
||||||
|
|
||||||
collection.AddScoped<WindowMediatorSubscriberBase, EditProfileUi>((s) => new EditProfileUi(s.GetRequiredService<ILogger<EditProfileUi>>(),
|
collection.AddScoped<WindowMediatorSubscriberBase, EditProfileUi>((s) => new EditProfileUi(s.GetRequiredService<ILogger<EditProfileUi>>(),
|
||||||
s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<ApiController>(), s.GetRequiredService<UiSharedService>(), s.GetRequiredService<FileDialogManager>(),
|
s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<ApiController>(), s.GetRequiredService<UiSharedService>(), s.GetRequiredService<FileDialogManager>(),
|
||||||
|
|||||||
@@ -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.API.Dto.User;
|
||||||
using LightlessSync.LightlessConfiguration;
|
using LightlessSync.LightlessConfiguration;
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
@@ -140,6 +144,11 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
|
|||||||
IsLightFinderAvailable = false;
|
IsLightFinderAvailable = false;
|
||||||
ApplyBroadcastDisabled(forcePublish: true);
|
ApplyBroadcastDisabled(forcePublish: true);
|
||||||
_logger.LogDebug("Cleared Lightfinder state due to disconnect.");
|
_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)
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
@@ -236,6 +245,11 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
|
|||||||
{
|
{
|
||||||
_logger.LogInformation("Auto-enabling Lightfinder broadcast after reconnect.");
|
_logger.LogInformation("Auto-enabling Lightfinder broadcast after reconnect.");
|
||||||
_mediator.Publish(new EnableBroadcastMessage(hashedCid, true));
|
_mediator.Publish(new EnableBroadcastMessage(hashedCid, true));
|
||||||
|
|
||||||
|
_mediator.Publish(new NotificationMessage(
|
||||||
|
"Broadcast Auto-Enabled",
|
||||||
|
"Your Lightfinder broadcast has been automatically enabled.",
|
||||||
|
NotificationType.Info));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
@@ -391,9 +405,14 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
|
|||||||
|
|
||||||
public async void ToggleBroadcast()
|
public async void ToggleBroadcast()
|
||||||
{
|
{
|
||||||
|
|
||||||
if (!IsLightFinderAvailable)
|
if (!IsLightFinderAvailable)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("ToggleBroadcast - Lightfinder is not available.");
|
_logger.LogWarning("ToggleBroadcast - Lightfinder is not available.");
|
||||||
|
_mediator.Publish(new NotificationMessage(
|
||||||
|
"Broadcast Unavailable",
|
||||||
|
"Lightfinder is not available on this server.",
|
||||||
|
NotificationType.Error));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -403,6 +422,10 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
|
|||||||
if (!_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);
|
_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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -427,10 +450,19 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
|
|||||||
_logger.LogDebug("Toggling broadcast. Server currently broadcasting: {ServerStatus}, setting to: {NewStatus}", isCurrentlyBroadcasting, newStatus);
|
_logger.LogDebug("Toggling broadcast. Server currently broadcasting: {ServerStatus}, setting to: {NewStatus}", isCurrentlyBroadcasting, newStatus);
|
||||||
|
|
||||||
_mediator.Publish(new EnableBroadcastMessage(hashedCid, 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)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Failed to determine current broadcast status for toggle");
|
_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);
|
}).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
@@ -493,6 +525,7 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
|
|||||||
{
|
{
|
||||||
_logger.LogDebug("Broadcast TTL expired. Disabling broadcast locally.");
|
_logger.LogDebug("Broadcast TTL expired. Disabling broadcast locally.");
|
||||||
ApplyBroadcastDisabled(forcePublish: true);
|
ApplyBroadcastDisabled(forcePublish: true);
|
||||||
|
ShowBroadcastExpiredNotification();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -501,4 +534,49 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
|
|||||||
}
|
}
|
||||||
}).ConfigureAwait(false);
|
}).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<LightlessNotificationAction>
|
||||||
|
{
|
||||||
|
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();
|
||||||
|
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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
43
LightlessSync/UI/Models/Changelog.cs
Normal file
43
LightlessSync/UI/Models/Changelog.cs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
namespace LightlessSync.UI.Models
|
||||||
|
{
|
||||||
|
public class ChangelogFile
|
||||||
|
{
|
||||||
|
public string Tagline { get; init; } = string.Empty;
|
||||||
|
public string Subline { get; init; } = string.Empty;
|
||||||
|
public List<ChangelogEntry> Changelog { get; init; } = new();
|
||||||
|
public List<CreditCategory>? Credits { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
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<ChangelogVersion>? Versions { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ChangelogVersion
|
||||||
|
{
|
||||||
|
public string Number { get; init; } = string.Empty;
|
||||||
|
public List<string> Items { get; init; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CreditCategory
|
||||||
|
{
|
||||||
|
public string Category { get; init; } = string.Empty;
|
||||||
|
public List<CreditItem> Items { get; init; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CreditItem
|
||||||
|
{
|
||||||
|
public string Name { get; init; } = string.Empty;
|
||||||
|
public string Role { get; init; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CreditsFile
|
||||||
|
{
|
||||||
|
public List<CreditCategory> Credits { get; init; } = new();
|
||||||
|
}
|
||||||
|
}
|
||||||
751
LightlessSync/UI/UpdateNotesUi.cs
Normal file
751
LightlessSync/UI/UpdateNotesUi.cs
Normal file
@@ -0,0 +1,751 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
SizeConstraints = new WindowSizeConstraints()
|
||||||
|
{
|
||||||
|
MinimumSize = new Vector2(800, 700), MaximumSize = new Vector2(800, 700),
|
||||||
|
};
|
||||||
|
|
||||||
|
LoadEmbeddedResources();
|
||||||
|
logger.LogInformation("UpdateNotesUi constructor completed successfully");
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnOpen()
|
||||||
|
{
|
||||||
|
_scrollToTop = true;
|
||||||
|
_hasInitializedCollapsingHeaders = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void DrawInternal()
|
||||||
|
{
|
||||||
|
if (_uiShared.IsInGpose)
|
||||||
|
return;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user