Compare commits

...

20 Commits

Author SHA1 Message Date
choco
b9030549a7 pair request cleanup with refactoring 2025-10-16 14:08:37 +02:00
choco
10336a829a added re-broadcast to the notifications with action buttons, added some regular notifcations to broadcast service 2025-10-16 12:28:34 +02:00
77ff8ae372 Merge pull request 'notification-changes' (#65) from notification-changes into 1.12.3
Reviewed-on: #65
Reviewed-by: defnotken <defnotken@noreply.git.lightless-sync.org>
2025-10-14 22:43:10 +02:00
choco
cf27a67296 optional action button toggle for pair request (default on) 2025-10-14 15:07:46 +02:00
choco
d6a4595bb8 old debug line removals, better alignment performance notifs 2025-10-14 14:54:30 +02:00
choco
f202818b55 performance notifcation addition, with some regular bugfixes regarding the flexing of the notifications 2025-10-14 11:46:14 +02:00
choco
3f2e4d6640 small service cleanup 2025-10-14 09:36:10 +02:00
choco
90b483e4ea Merge remote-tracking branch 'origin/1.12.3' into notification-changes 2025-10-13 22:24:50 +02:00
choco
bcb524df52 text auto flexing on dismiss removed 2025-10-13 00:13:21 +02:00
b64bb66119 Merge pull request 'notification-changes' (#62) from notification-changes into 1.12.3
Reviewed-on: #62
2025-10-12 23:33:42 +02:00
choco
118edb9dea notif overlay flex with 1 sec delay removal 2025-10-12 23:11:18 +02:00
choco
6c0d00dc39 update intervall prevent spam better performance 2025-10-12 21:07:32 +02:00
defnotken
dae8127ac8 Merge branch '1.12.3' of https://git.lightless-sync.org/Lightless-Sync/LightlessClient into 1.12.3 2025-10-12 12:11:18 -05:00
defnotken
0635caab65 Safety checks for NullDrawObject 2025-10-12 12:09:06 -05:00
1530ac3911 Merge pull request 'notification-changes' (#59) from notification-changes into 1.12.3
Reviewed-on: #59
2025-10-12 18:08:52 +02:00
3f80467180 Merge branch '1.12.3' into notification-changes 2025-10-12 18:08:43 +02:00
defnotken
4b4e587a89 1.12.3 initial 2025-10-12 11:07:24 -05:00
choco
02c3846031 column rename for better UX c: 2025-10-12 18:02:53 +02:00
choco
a8a01b3034 added more customization to the notifs, settings improvemnts, left and right notifs, animations for sliding in and out 2025-10-12 18:00:49 +02:00
50a5046c96 1.12.2
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 51s
Reviewed-on: #52
2025-10-12 15:33:02 +02:00
17 changed files with 919 additions and 368 deletions

View File

@@ -86,6 +86,7 @@ public class LightlessConfig : ILightlessConfiguration
public NotificationLocation LightlessErrorNotification { get; set; } = NotificationLocation.ChatAndLightlessUi; public NotificationLocation LightlessErrorNotification { get; set; } = NotificationLocation.ChatAndLightlessUi;
public NotificationLocation LightlessPairRequestNotification { get; set; } = NotificationLocation.LightlessUi; public NotificationLocation LightlessPairRequestNotification { get; set; } = NotificationLocation.LightlessUi;
public NotificationLocation LightlessDownloadNotification { get; set; } = NotificationLocation.TextOverlay; public NotificationLocation LightlessDownloadNotification { get; set; } = NotificationLocation.TextOverlay;
public NotificationLocation LightlessPerformanceNotification { get; set; } = NotificationLocation.LightlessUi;
// Basic Settings // Basic Settings
public float NotificationOpacity { get; set; } = 0.95f; public float NotificationOpacity { get; set; } = 0.95f;
@@ -95,6 +96,7 @@ public class LightlessConfig : ILightlessConfiguration
public bool ShowNotificationTimestamp { get; set; } = false; public bool ShowNotificationTimestamp { get; set; } = false;
// Position & Layout // Position & Layout
public NotificationCorner NotificationCorner { get; set; } = NotificationCorner.Right;
public int NotificationOffsetY { get; set; } = 50; public int NotificationOffsetY { get; set; } = 50;
public int NotificationOffsetX { get; set; } = 0; public int NotificationOffsetX { get; set; } = 0;
public float NotificationWidth { get; set; } = 350f; public float NotificationWidth { get; set; } = 350f;
@@ -102,6 +104,7 @@ public class LightlessConfig : ILightlessConfiguration
// Animation & Effects // Animation & Effects
public float NotificationAnimationSpeed { get; set; } = 10f; public float NotificationAnimationSpeed { get; set; } = 10f;
public float NotificationSlideSpeed { get; set; } = 10f;
public float NotificationAccentBarWidth { get; set; } = 3f; public float NotificationAccentBarWidth { get; set; } = 3f;
// Duration per Type // Duration per Type
@@ -110,16 +113,19 @@ public class LightlessConfig : ILightlessConfiguration
public int ErrorNotificationDurationSeconds { get; set; } = 20; public int ErrorNotificationDurationSeconds { get; set; } = 20;
public int PairRequestDurationSeconds { get; set; } = 180; public int PairRequestDurationSeconds { get; set; } = 180;
public int DownloadNotificationDurationSeconds { get; set; } = 300; public int DownloadNotificationDurationSeconds { get; set; } = 300;
public int PerformanceNotificationDurationSeconds { get; set; } = 20;
public uint CustomInfoSoundId { get; set; } = 2; // Se2 public uint CustomInfoSoundId { get; set; } = 2; // Se2
public uint CustomWarningSoundId { get; set; } = 16; // Se15 public uint CustomWarningSoundId { get; set; } = 16; // Se15
public uint CustomErrorSoundId { get; set; } = 16; // Se15 public uint CustomErrorSoundId { get; set; } = 16; // Se15
public uint PairRequestSoundId { get; set; } = 5; // Se5 public uint PairRequestSoundId { get; set; } = 5; // Se5
public uint DownloadSoundId { get; set; } = 15; // Se14 public uint PerformanceSoundId { get; set; } = 16; // Se15
public bool DisableInfoSound { get; set; } = true; public bool DisableInfoSound { get; set; } = true;
public bool DisableWarningSound { get; set; } = true; public bool DisableWarningSound { get; set; } = true;
public bool DisableErrorSound { get; set; } = true; public bool DisableErrorSound { get; set; } = true;
public bool DisablePairRequestSound { get; set; } = true; public bool DisablePairRequestSound { get; set; } = true;
public bool DisableDownloadSound { get; set; } = true; public bool DisablePerformanceSound { get; set; } = true;
public bool ShowPerformanceNotificationActions { get; set; } = true;
public bool ShowPairRequestNotificationActions { get; set; } = true;
public bool UseFocusTarget { get; set; } = false; public bool UseFocusTarget { get; set; } = false;
public bool overrideFriendColor { get; set; } = false; public bool overrideFriendColor { get; set; } = false;
public bool overridePartyColor { get; set; } = false; public bool overridePartyColor { get; set; } = false;

View File

@@ -17,5 +17,12 @@ public enum NotificationType
Warning, Warning,
Error, Error,
PairRequest, PairRequest,
Download Download,
Performance
}
public enum NotificationCorner
{
Right,
Left
} }

View File

@@ -3,7 +3,7 @@
<PropertyGroup> <PropertyGroup>
<Authors></Authors> <Authors></Authors>
<Company></Company> <Company></Company>
<Version>1.12.2</Version> <Version>1.12.3</Version>
<Description></Description> <Description></Description>
<Copyright></Copyright> <Copyright></Copyright>
<PackageProjectUrl>https://github.com/Light-Public-Syncshells/LightlessClient</PackageProjectUrl> <PackageProjectUrl>https://github.com/Light-Public-Syncshells/LightlessClient</PackageProjectUrl>

View File

@@ -98,7 +98,19 @@ public class PlayerDataFactory
private unsafe bool CheckForNullDrawObjectUnsafe(IntPtr playerPointer) private unsafe bool CheckForNullDrawObjectUnsafe(IntPtr playerPointer)
{ {
return ((Character*)playerPointer)->GameObject.DrawObject == null; if (playerPointer == IntPtr.Zero)
return true;
var character = (Character*)playerPointer;
if (character == null)
return true;
var gameObject = &character->GameObject;
if (gameObject == null)
return true;
return gameObject->DrawObject == null;
} }
private async Task<CharacterDataFragment> CreateCharacterData(GameObjectHandler playerRelatedObject, CancellationToken ct) private async Task<CharacterDataFragment> CreateCharacterData(GameObjectHandler playerRelatedObject, CancellationToken ct)

View File

@@ -190,7 +190,8 @@ public sealed class Plugin : IDalamudPlugin
notificationManager, notificationManager,
chatGui, chatGui,
s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<LightlessMediator>(),
s.GetRequiredService<PairRequestService>())); s.GetRequiredService<PairRequestService>(),
s.GetRequiredService<BroadcastService>()));
collection.AddSingleton((s) => collection.AddSingleton((s) =>
{ {
var httpClient = new HttpClient(); var httpClient = new HttpClient();

View File

@@ -1,4 +1,4 @@
using LightlessSync.API.Dto.Group; 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;
@@ -394,6 +394,10 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
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.",
LightlessConfiguration.Models.NotificationType.Error));
return; return;
} }
@@ -403,6 +407,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.",
LightlessConfiguration.Models.NotificationType.Warning));
return; return;
} }
@@ -427,10 +435,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.",
LightlessConfiguration.Models.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 Failed",
$"Failed to toggle broadcast: {ex.Message}",
LightlessConfiguration.Models.NotificationType.Error));
} }
}).ConfigureAwait(false); }).ConfigureAwait(false);
} }
@@ -493,6 +510,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);
_mediator.Publish(new BroadcastExpiredMessage());
} }
} }
else else

View File

@@ -48,20 +48,23 @@ public record PetNamesMessage(string PetNicknamesData) : MessageBase;
public record HonorificReadyMessage : MessageBase; public record HonorificReadyMessage : MessageBase;
public record TransientResourceChangedMessage(IntPtr Address) : MessageBase; public record TransientResourceChangedMessage(IntPtr Address) : MessageBase;
public record HaltScanMessage(string Source) : MessageBase; public record HaltScanMessage(string Source) : MessageBase;
public record ResumeScanMessage(string Source) : MessageBase;
public record NotificationMessage public record NotificationMessage
(string Title, string Message, NotificationType Type, TimeSpan? TimeShownOnScreen = null) : MessageBase; (string Title, string Message, NotificationType Type, TimeSpan? TimeShownOnScreen = null) : MessageBase;
public record PerformanceNotificationMessage
(string Title, string Message, UserData UserData, bool IsPaused, string PlayerName) : MessageBase;
public record CreateCacheForObjectMessage(GameObjectHandler ObjectToCreateFor) : SameThreadMessage; public record CreateCacheForObjectMessage(GameObjectHandler ObjectToCreateFor) : SameThreadMessage;
public record ClearCacheForObjectMessage(GameObjectHandler ObjectToCreateFor) : SameThreadMessage; public record ClearCacheForObjectMessage(GameObjectHandler ObjectToCreateFor) : SameThreadMessage;
public record CharacterDataCreatedMessage(CharacterData CharacterData) : SameThreadMessage; public record CharacterDataCreatedMessage(CharacterData CharacterData) : SameThreadMessage;
public record LightlessNotificationMessage(LightlessSync.UI.Models.LightlessNotification Notification) : MessageBase; public record LightlessNotificationMessage(LightlessSync.UI.Models.LightlessNotification Notification) : MessageBase;
public record LightlessNotificationDismissMessage(string NotificationId) : MessageBase; public record LightlessNotificationDismissMessage(string NotificationId) : MessageBase;
public record ClearAllNotificationsMessage : MessageBase;
public record CharacterDataAnalyzedMessage : MessageBase; public record CharacterDataAnalyzedMessage : MessageBase;
public record PenumbraStartRedrawMessage(IntPtr Address) : MessageBase; public record PenumbraStartRedrawMessage(IntPtr Address) : MessageBase;
public record PenumbraEndRedrawMessage(IntPtr Address) : MessageBase; public record PenumbraEndRedrawMessage(IntPtr Address) : MessageBase;
public record HubReconnectingMessage(Exception? Exception) : SameThreadMessage; public record HubReconnectingMessage(Exception? Exception) : SameThreadMessage;
public record HubReconnectedMessage(string? Arg) : SameThreadMessage; public record HubReconnectedMessage(string? Arg) : SameThreadMessage;
public record HubClosedMessage(Exception? Exception) : SameThreadMessage; public record HubClosedMessage(Exception? Exception) : SameThreadMessage;
public record ResumeScanMessage(string Source) : MessageBase;
public record DownloadReadyMessage(Guid RequestId) : MessageBase; public record DownloadReadyMessage(Guid RequestId) : MessageBase;
public record DownloadStartedMessage(GameObjectHandler DownloadId, Dictionary<string, FileDownloadStatus> DownloadStatus) : MessageBase; public record DownloadStartedMessage(GameObjectHandler DownloadId, Dictionary<string, FileDownloadStatus> DownloadStatus) : MessageBase;
public record DownloadFinishedMessage(GameObjectHandler DownloadId) : MessageBase; public record DownloadFinishedMessage(GameObjectHandler DownloadId) : MessageBase;
@@ -105,6 +108,8 @@ public record EnableBroadcastMessage(string HashedCid, bool Enabled) : MessageBa
public record BroadcastStatusChangedMessage(bool Enabled, TimeSpan? Ttl) : MessageBase; public record BroadcastStatusChangedMessage(bool Enabled, TimeSpan? Ttl) : MessageBase;
public record SyncshellBroadcastsUpdatedMessage : MessageBase; public record SyncshellBroadcastsUpdatedMessage : MessageBase;
public record PairRequestsUpdatedMessage : MessageBase; public record PairRequestsUpdatedMessage : MessageBase;
public record PairRequestReceivedMessage(string SenderName, string SenderId) : MessageBase;
public record BroadcastExpiredMessage : MessageBase;
public record VisibilityChange : MessageBase; public record VisibilityChange : MessageBase;
#pragma warning restore S2094 #pragma warning restore S2094
#pragma warning restore MA0048 // File name must match type name #pragma warning restore MA0048 // File name must match type name

View File

@@ -10,9 +10,11 @@ using LightlessSync.UI.Models;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Client.UI;
using LightlessSync.API.Data;
using NotificationType = LightlessSync.LightlessConfiguration.Models.NotificationType; using NotificationType = LightlessSync.LightlessConfiguration.Models.NotificationType;
namespace LightlessSync.Services; namespace LightlessSync.Services;
public class NotificationService : DisposableMediatorSubscriberBase, IHostedService public class NotificationService : DisposableMediatorSubscriberBase, IHostedService
{ {
private readonly ILogger<NotificationService> _logger; private readonly ILogger<NotificationService> _logger;
@@ -21,6 +23,7 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
private readonly INotificationManager _notificationManager; private readonly INotificationManager _notificationManager;
private readonly IChatGui _chatGui; private readonly IChatGui _chatGui;
private readonly PairRequestService _pairRequestService; private readonly PairRequestService _pairRequestService;
private readonly BroadcastService _broadcastService;
private readonly HashSet<string> _shownPairRequestNotifications = new(); private readonly HashSet<string> _shownPairRequestNotifications = new();
public NotificationService( public NotificationService(
@@ -30,7 +33,8 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
INotificationManager notificationManager, INotificationManager notificationManager,
IChatGui chatGui, IChatGui chatGui,
LightlessMediator mediator, LightlessMediator mediator,
PairRequestService pairRequestService) : base(logger, mediator) PairRequestService pairRequestService,
BroadcastService broadcastService) : base(logger, mediator)
{ {
_logger = logger; _logger = logger;
_configService = configService; _configService = configService;
@@ -38,12 +42,16 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
_notificationManager = notificationManager; _notificationManager = notificationManager;
_chatGui = chatGui; _chatGui = chatGui;
_pairRequestService = pairRequestService; _pairRequestService = pairRequestService;
_broadcastService = broadcastService;
} }
public Task StartAsync(CancellationToken cancellationToken) public Task StartAsync(CancellationToken cancellationToken)
{ {
Mediator.Subscribe<NotificationMessage>(this, HandleNotificationMessage); Mediator.Subscribe<NotificationMessage>(this, HandleNotificationMessage);
Mediator.Subscribe<PairRequestsUpdatedMessage>(this, HandlePairRequestsUpdated); Mediator.Subscribe<PairRequestsUpdatedMessage>(this, HandlePairRequestsUpdated);
Mediator.Subscribe<PairRequestReceivedMessage>(this, HandlePairRequestReceived);
Mediator.Subscribe<PerformanceNotificationMessage>(this, HandlePerformanceNotification);
Mediator.Subscribe<BroadcastExpiredMessage>(this, HandleBroadcastExpired);
return Task.CompletedTask; return Task.CompletedTask;
} }
@@ -107,23 +115,42 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
public void ShowPairRequestNotification(string senderName, string senderId, Action onAccept, Action onDecline) public void ShowPairRequestNotification(string senderName, string senderId, Action onAccept, Action onDecline)
{ {
var notification = new LightlessNotification var location = GetNotificationLocation(NotificationType.PairRequest);
{
Id = $"pair_request_{senderId}",
Title = "Pair Request Received",
Message = $"{senderName} wants to directly pair with you.",
Type = NotificationType.PairRequest,
Duration = TimeSpan.FromSeconds(_configService.Current.PairRequestDurationSeconds),
SoundEffectId = GetPairRequestSoundId(),
Actions = CreatePairRequestActions(onAccept, onDecline)
};
if (notification.SoundEffectId.HasValue) // Show in chat if configured
if (location == NotificationLocation.Chat || location == NotificationLocation.ChatAndLightlessUi)
{ {
PlayNotificationSound(notification.SoundEffectId.Value); ShowChat(new NotificationMessage("Pair Request Received", $"{senderName} wants to directly pair with you.", NotificationType.PairRequest));
} }
Mediator.Publish(new LightlessNotificationMessage(notification)); // Show Lightless notification if configured and action buttons are enabled
if ((location == NotificationLocation.LightlessUi || location == NotificationLocation.ChatAndLightlessUi)
&& _configService.Current.UseLightlessNotifications
&& _configService.Current.ShowPairRequestNotificationActions)
{
var notification = new LightlessNotification
{
Id = $"pair_request_{senderId}",
Title = "Pair Request Received",
Message = $"{senderName} wants to directly pair with you.",
Type = NotificationType.PairRequest,
Duration = TimeSpan.FromSeconds(_configService.Current.PairRequestDurationSeconds),
SoundEffectId = GetPairRequestSoundId(),
Actions = CreatePairRequestActions(onAccept, onDecline)
};
if (notification.SoundEffectId.HasValue)
{
PlayNotificationSound(notification.SoundEffectId.Value);
}
Mediator.Publish(new LightlessNotificationMessage(notification));
}
else if (location != NotificationLocation.Nowhere && location != NotificationLocation.Chat)
{
// Fall back to regular notification without action buttons
HandleNotificationMessage(new NotificationMessage("Pair Request Received", $"{senderName} wants to directly pair with you.", NotificationType.PairRequest));
}
} }
private uint? GetPairRequestSoundId() => private uint? GetPairRequestSoundId() =>
@@ -356,6 +383,7 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
NotificationType.Error => TimeSpan.FromSeconds(_configService.Current.ErrorNotificationDurationSeconds), NotificationType.Error => TimeSpan.FromSeconds(_configService.Current.ErrorNotificationDurationSeconds),
NotificationType.PairRequest => TimeSpan.FromSeconds(_configService.Current.PairRequestDurationSeconds), NotificationType.PairRequest => TimeSpan.FromSeconds(_configService.Current.PairRequestDurationSeconds),
NotificationType.Download => TimeSpan.FromSeconds(_configService.Current.DownloadNotificationDurationSeconds), NotificationType.Download => TimeSpan.FromSeconds(_configService.Current.DownloadNotificationDurationSeconds),
NotificationType.Performance => TimeSpan.FromSeconds(_configService.Current.PerformanceNotificationDurationSeconds),
_ => TimeSpan.FromSeconds(10) _ => TimeSpan.FromSeconds(10)
}; };
@@ -371,7 +399,8 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
NotificationType.Info => _configService.Current.DisableInfoSound, NotificationType.Info => _configService.Current.DisableInfoSound,
NotificationType.Warning => _configService.Current.DisableWarningSound, NotificationType.Warning => _configService.Current.DisableWarningSound,
NotificationType.Error => _configService.Current.DisableErrorSound, NotificationType.Error => _configService.Current.DisableErrorSound,
NotificationType.Download => _configService.Current.DisableDownloadSound, NotificationType.Performance => _configService.Current.DisablePerformanceSound,
NotificationType.Download => true, // Download sounds always disabled
_ => false _ => false
}; };
@@ -380,7 +409,7 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
NotificationType.Info => _configService.Current.CustomInfoSoundId, NotificationType.Info => _configService.Current.CustomInfoSoundId,
NotificationType.Warning => _configService.Current.CustomWarningSoundId, NotificationType.Warning => _configService.Current.CustomWarningSoundId,
NotificationType.Error => _configService.Current.CustomErrorSoundId, NotificationType.Error => _configService.Current.CustomErrorSoundId,
NotificationType.Download => _configService.Current.DownloadSoundId, NotificationType.Performance => _configService.Current.PerformanceSoundId,
_ => NotificationSounds.GetDefaultSound(type) _ => NotificationSounds.GetDefaultSound(type)
}; };
@@ -418,6 +447,7 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
NotificationType.Error => _configService.Current.LightlessErrorNotification, NotificationType.Error => _configService.Current.LightlessErrorNotification,
NotificationType.PairRequest => _configService.Current.LightlessPairRequestNotification, NotificationType.PairRequest => _configService.Current.LightlessPairRequestNotification,
NotificationType.Download => _configService.Current.LightlessDownloadNotification, NotificationType.Download => _configService.Current.LightlessDownloadNotification,
NotificationType.Performance => _configService.Current.LightlessPerformanceNotification,
_ => NotificationLocation.LightlessUi _ => NotificationLocation.LightlessUi
}; };
@@ -505,6 +535,18 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
case NotificationType.Error: case NotificationType.Error:
PrintErrorChat(msg.Message); PrintErrorChat(msg.Message);
break; break;
case NotificationType.PairRequest:
PrintPairRequestChat(msg.Title, msg.Message);
break;
case NotificationType.Performance:
PrintPerformanceChat(msg.Title, msg.Message);
break;
// Download notifications don't support chat output, will be a giga spam otherwise
case NotificationType.Download:
break;
} }
} }
@@ -528,6 +570,32 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
_chatGui.Print(se.BuiltString); _chatGui.Print(se.BuiltString);
} }
private void PrintPairRequestChat(string? title, string? message)
{
SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] ")
.AddUiForeground("Pair Request: ", 541).AddUiForegroundOff()
.AddText(title ?? message ?? string.Empty);
_chatGui.Print(se.BuiltString);
}
private void PrintPerformanceChat(string? title, string? message)
{
SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] ")
.AddUiForeground("Performance: ", 508).AddUiForegroundOff()
.AddText(title ?? message ?? string.Empty);
_chatGui.Print(se.BuiltString);
}
private void HandlePairRequestReceived(PairRequestReceivedMessage msg)
{
ShowPairRequestNotification(
msg.SenderName,
msg.SenderId,
() => _pairRequestService.AcceptPairRequest(msg.SenderId, msg.SenderName),
() => _pairRequestService.DeclinePairRequest(msg.SenderId)
);
}
private void HandlePairRequestsUpdated(PairRequestsUpdatedMessage _) private void HandlePairRequestsUpdated(PairRequestsUpdatedMessage _)
{ {
var activeRequests = _pairRequestService.GetActiveRequests(); var activeRequests = _pairRequestService.GetActiveRequests();
@@ -545,17 +613,219 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
_shownPairRequestNotifications.Remove(hashedCid); _shownPairRequestNotifications.Remove(hashedCid);
} }
// Show/update notifications for all active requests // Track active requests
foreach (var request in activeRequests) foreach (var request in activeRequests)
{ {
_shownPairRequestNotifications.Add(request.HashedCid); _shownPairRequestNotifications.Add(request.HashedCid);
ShowPairRequestNotification(
request.DisplayName,
request.HashedCid,
() => _pairRequestService.AcceptPairRequest(request.HashedCid, request.DisplayName),
() => _pairRequestService.DeclinePairRequest(request.HashedCid)
);
} }
} }
}
private void HandlePerformanceNotification(PerformanceNotificationMessage msg)
{
var location = GetNotificationLocation(NotificationType.Performance);
// Show in chat if configured
if (location == NotificationLocation.Chat || location == NotificationLocation.ChatAndLightlessUi)
{
ShowChat(new NotificationMessage(msg.Title, msg.Message, NotificationType.Performance));
}
// Show Lightless notification if configured and action buttons are enabled
if ((location == NotificationLocation.LightlessUi || location == NotificationLocation.ChatAndLightlessUi)
&& _configService.Current.UseLightlessNotifications
&& _configService.Current.ShowPerformanceNotificationActions)
{
var actions = CreatePerformanceActions(msg.UserData, msg.IsPaused, msg.PlayerName);
var notification = new LightlessNotification
{
Title = msg.Title,
Message = msg.Message,
Type = NotificationType.Performance,
Duration = TimeSpan.FromSeconds(_configService.Current.PerformanceNotificationDurationSeconds),
Actions = actions,
SoundEffectId = GetSoundEffectId(NotificationType.Performance, null)
};
if (notification.SoundEffectId.HasValue)
{
PlayNotificationSound(notification.SoundEffectId.Value);
}
Mediator.Publish(new LightlessNotificationMessage(notification));
}
else if (location != NotificationLocation.Nowhere && location != NotificationLocation.Chat)
{
// Fall back to regular notification without action buttons
HandleNotificationMessage(new NotificationMessage(msg.Title, msg.Message, NotificationType.Performance));
}
}
private List<LightlessNotificationAction> CreatePerformanceActions(UserData userData, bool isPaused, string playerName)
{
var actions = new List<LightlessNotificationAction>();
if (isPaused)
{
actions.Add(new LightlessNotificationAction
{
Label = "Unpause",
Icon = FontAwesomeIcon.Play,
Color = UIColors.Get("LightlessGreen"),
IsPrimary = true,
OnClick = (notification) =>
{
try
{
Mediator.Publish(new CyclePauseMessage(userData));
DismissNotification(notification);
var displayName = GetUserDisplayName(userData, playerName);
ShowNotification(
"Player Unpaused",
$"Successfully unpaused {displayName}",
NotificationType.Info,
TimeSpan.FromSeconds(3));
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to unpause player {uid}", userData.UID);
var displayName = GetUserDisplayName(userData, playerName);
ShowNotification(
"Unpause Failed",
$"Failed to unpause {displayName}",
NotificationType.Error,
TimeSpan.FromSeconds(5));
}
}
});
}
else
{
actions.Add(new LightlessNotificationAction
{
Label = "Pause",
Icon = FontAwesomeIcon.Pause,
Color = UIColors.Get("LightlessOrange"),
IsPrimary = true,
OnClick = (notification) =>
{
try
{
Mediator.Publish(new PauseMessage(userData));
DismissNotification(notification);
var displayName = GetUserDisplayName(userData, playerName);
ShowNotification(
"Player Paused",
$"Successfully paused {displayName}",
NotificationType.Info,
TimeSpan.FromSeconds(3));
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to pause player {uid}", userData.UID);
var displayName = GetUserDisplayName(userData, playerName);
ShowNotification(
"Pause Failed",
$"Failed to pause {displayName}",
NotificationType.Error,
TimeSpan.FromSeconds(5));
}
}
});
}
// Add dismiss button
actions.Add(new LightlessNotificationAction
{
Label = "Dismiss",
Icon = FontAwesomeIcon.Times,
Color = UIColors.Get("DimRed"),
IsPrimary = false,
OnClick = (notification) =>
{
DismissNotification(notification);
}
});
return actions;
}
private string GetUserDisplayName(UserData userData, string playerName)
{
if (!string.IsNullOrEmpty(userData.Alias) && !string.Equals(userData.Alias, userData.UID, StringComparison.Ordinal))
{
return $"{playerName} ({userData.Alias})";
}
return $"{playerName} ({userData.UID})";
}
private void HandleBroadcastExpired(BroadcastExpiredMessage _)
{
var location = GetNotificationLocation(NotificationType.Warning);
if (location == NotificationLocation.Chat || location == NotificationLocation.ChatAndLightlessUi)
{
ShowChat(new NotificationMessage("Broadcast Expired", "Your Lightfinder broadcast has expired after 3 hours.", NotificationType.Warning));
}
if ((location == NotificationLocation.LightlessUi || location == NotificationLocation.ChatAndLightlessUi)
&& _configService.Current.UseLightlessNotifications)
{
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.Warning,
Duration = TimeSpan.FromSeconds(_configService.Current.WarningNotificationDurationSeconds),
SoundEffectId = GetSoundEffectId(NotificationType.Warning, null),
Actions = CreateBroadcastExpiredActions()
};
if (notification.SoundEffectId.HasValue)
{
PlayNotificationSound(notification.SoundEffectId.Value);
}
Mediator.Publish(new LightlessNotificationMessage(notification));
}
else if (location != NotificationLocation.Nowhere && location != NotificationLocation.Chat)
{
HandleNotificationMessage(new NotificationMessage("Broadcast Expired", "Your Lightfinder broadcast has expired after 3 hours.", NotificationType.Warning));
}
}
private List<LightlessNotificationAction> CreateBroadcastExpiredActions()
{
return new List<LightlessNotificationAction>
{
new()
{
Id = "re_enable",
Label = "Re-enable Broadcast",
Icon = FontAwesomeIcon.Plus,
Color = UIColors.Get("LightlessGreen"),
IsPrimary = true,
OnClick = (n) =>
{
_logger.LogInformation("Re-enabling broadcast from notification");
_broadcastService.ToggleBroadcast();
DismissNotification(n);
}
},
new()
{
Id = "close",
Label = "Close",
Icon = FontAwesomeIcon.Times,
Color = UIColors.Get("DimRed"),
OnClick = (n) =>
{
_logger.LogInformation("Broadcast expiration notification dismissed");
DismissNotification(n);
}
}
};
}
}

View File

@@ -70,6 +70,10 @@ public sealed class PairRequestService : DisposableMediatorSubscriberBase
: _dalamudUtil.RunOnFrameworkThread(() => ToDisplay(entry)).GetAwaiter().GetResult(); : _dalamudUtil.RunOnFrameworkThread(() => ToDisplay(entry)).GetAwaiter().GetResult();
Mediator.Publish(new PairRequestsUpdatedMessage()); Mediator.Publish(new PairRequestsUpdatedMessage());
var senderName = string.IsNullOrEmpty(display.DisplayName) ? "Unknown User" : display.DisplayName;
Mediator.Publish(new PairRequestReceivedMessage(senderName, display.HashedCid));
return display; return display;
} }

View File

@@ -78,23 +78,26 @@ public class PlayerPerformanceService
string warningText = string.Empty; string warningText = string.Empty;
if (exceedsTris && !exceedsVram) if (exceedsTris && !exceedsVram)
{ {
warningText = $"Player {pairHandler.Pair.PlayerName} ({pairHandler.Pair.UserData.AliasOrUID}) exceeds your configured triangle warning threshold (" + warningText = $"Player {pairHandler.Pair.PlayerName} ({pairHandler.Pair.UserData.AliasOrUID}) exceeds your configured triangle warning threshold\n" +
$"{triUsage}/{config.TrisWarningThresholdThousands * 1000} triangles)."; $"{triUsage}/{config.TrisWarningThresholdThousands * 1000} triangles";
} }
else if (!exceedsTris) else if (!exceedsTris)
{ {
warningText = $"Player {pairHandler.Pair.PlayerName} ({pairHandler.Pair.UserData.AliasOrUID}) exceeds your configured VRAM warning threshold (" + warningText = $"Player {pairHandler.Pair.PlayerName} ({pairHandler.Pair.UserData.AliasOrUID}) exceeds your configured VRAM warning threshold\n" +
$"{UiSharedService.ByteToString(vramUsage, true)}/{config.VRAMSizeWarningThresholdMiB} MiB)."; $"{UiSharedService.ByteToString(vramUsage, true)}/{config.VRAMSizeWarningThresholdMiB} MiB";
} }
else else
{ {
warningText = $"Player {pairHandler.Pair.PlayerName} ({pairHandler.Pair.UserData.AliasOrUID}) exceeds both VRAM warning threshold (" + warningText = $"Player {pairHandler.Pair.PlayerName} ({pairHandler.Pair.UserData.AliasOrUID}) exceeds both VRAM warning threshold and triangle warning threshold\n" +
$"{UiSharedService.ByteToString(vramUsage, true)}/{config.VRAMSizeWarningThresholdMiB} MiB) and " + $"{UiSharedService.ByteToString(vramUsage, true)}/{config.VRAMSizeWarningThresholdMiB} MiB and {triUsage}/{config.TrisWarningThresholdThousands * 1000} triangles";
$"triangle warning threshold ({triUsage}/{config.TrisWarningThresholdThousands * 1000} triangles).";
} }
_mediator.Publish(new NotificationMessage($"{pairHandler.Pair.PlayerName} ({pairHandler.Pair.UserData.AliasOrUID}) exceeds performance threshold(s)", _mediator.Publish(new PerformanceNotificationMessage(
warningText, LightlessConfiguration.Models.NotificationType.Warning)); $"{pairHandler.Pair.PlayerName} ({pairHandler.Pair.UserData.AliasOrUID}) exceeds performance threshold(s)",
warningText,
pairHandler.Pair.UserData,
pairHandler.Pair.IsPaused,
pairHandler.Pair.PlayerName));
} }
return true; return true;
@@ -138,11 +141,15 @@ public class PlayerPerformanceService
if (CheckForThreshold(config.AutoPausePlayersExceedingThresholds, config.TrisAutoPauseThresholdThousands * 1000, if (CheckForThreshold(config.AutoPausePlayersExceedingThresholds, config.TrisAutoPauseThresholdThousands * 1000,
triUsage, config.AutoPausePlayersWithPreferredPermissionsExceedingThresholds, isPrefPerm)) triUsage, config.AutoPausePlayersWithPreferredPermissionsExceedingThresholds, isPrefPerm))
{ {
_mediator.Publish(new NotificationMessage($"{pair.PlayerName} ({pair.UserData.AliasOrUID}) automatically paused", var message = $"Player {pair.PlayerName} ({pair.UserData.AliasOrUID}) exceeded your configured triangle auto pause threshold and has been automatically paused\n" +
$"Player {pair.PlayerName} ({pair.UserData.AliasOrUID}) exceeded your configured triangle auto pause threshold (" + $"{triUsage}/{config.TrisAutoPauseThresholdThousands * 1000} triangles";
$"{triUsage}/{config.TrisAutoPauseThresholdThousands * 1000} triangles)" +
$" and has been automatically paused.", _mediator.Publish(new PerformanceNotificationMessage(
LightlessConfiguration.Models.NotificationType.Warning)); $"{pair.PlayerName} ({pair.UserData.AliasOrUID}) automatically paused",
message,
pair.UserData,
true,
pair.PlayerName));
_mediator.Publish(new EventMessage(new Event(pair.PlayerName, pair.UserData, nameof(PlayerPerformanceService), EventSeverity.Warning, _mediator.Publish(new EventMessage(new Event(pair.PlayerName, pair.UserData, nameof(PlayerPerformanceService), EventSeverity.Warning,
$"Exceeds triangle threshold: automatically paused ({triUsage}/{config.TrisAutoPauseThresholdThousands * 1000} triangles)"))); $"Exceeds triangle threshold: automatically paused ({triUsage}/{config.TrisAutoPauseThresholdThousands * 1000} triangles)")));
@@ -214,11 +221,15 @@ public class PlayerPerformanceService
if (CheckForThreshold(config.AutoPausePlayersExceedingThresholds, config.VRAMSizeAutoPauseThresholdMiB * 1024 * 1024, if (CheckForThreshold(config.AutoPausePlayersExceedingThresholds, config.VRAMSizeAutoPauseThresholdMiB * 1024 * 1024,
vramUsage, config.AutoPausePlayersWithPreferredPermissionsExceedingThresholds, isPrefPerm)) vramUsage, config.AutoPausePlayersWithPreferredPermissionsExceedingThresholds, isPrefPerm))
{ {
_mediator.Publish(new NotificationMessage($"{pair.PlayerName} ({pair.UserData.AliasOrUID}) automatically paused", var message = $"Player {pair.PlayerName} ({pair.UserData.AliasOrUID}) exceeded your configured VRAM auto pause threshold and has been automatically paused\n" +
$"Player {pair.PlayerName} ({pair.UserData.AliasOrUID}) exceeded your configured VRAM auto pause threshold (" + $"{UiSharedService.ByteToString(vramUsage, addSuffix: true)}/{config.VRAMSizeAutoPauseThresholdMiB}MiB";
$"{UiSharedService.ByteToString(vramUsage, addSuffix: true)}/{config.VRAMSizeAutoPauseThresholdMiB}MiB)" +
$" and has been automatically paused.", _mediator.Publish(new PerformanceNotificationMessage(
LightlessConfiguration.Models.NotificationType.Warning)); $"{pair.PlayerName} ({pair.UserData.AliasOrUID}) automatically paused",
message,
pair.UserData,
true,
pair.PlayerName));
_mediator.Publish(new PauseMessage(pair.UserData)); _mediator.Publish(new PauseMessage(pair.UserData));

View File

@@ -24,6 +24,7 @@ public class DownloadUi : WindowMediatorSubscriberBase
private readonly ConcurrentDictionary<GameObjectHandler, bool> _uploadingPlayers = new(); private readonly ConcurrentDictionary<GameObjectHandler, bool> _uploadingPlayers = new();
private readonly NotificationService _notificationService; private readonly NotificationService _notificationService;
private bool _notificationDismissed = true; private bool _notificationDismissed = true;
private int _lastDownloadStateHash = 0;
public DownloadUi(ILogger<DownloadUi> logger, DalamudUtilService dalamudUtilService, LightlessConfigService configService, public DownloadUi(ILogger<DownloadUi> logger, DalamudUtilService dalamudUtilService, LightlessConfigService configService,
PairProcessingLimiter pairProcessingLimiter, FileUploadManager fileTransferManager, LightlessMediator mediator, UiSharedService uiShared, PairProcessingLimiter pairProcessingLimiter, FileUploadManager fileTransferManager, LightlessMediator mediator, UiSharedService uiShared,
@@ -65,7 +66,7 @@ public class DownloadUi : WindowMediatorSubscriberBase
_currentDownloads.TryRemove(msg.DownloadId, out _); _currentDownloads.TryRemove(msg.DownloadId, out _);
if (!_currentDownloads.Any()) if (!_currentDownloads.Any())
{ {
_notificationService.DismissPairDownloadNotification(); Mediator.Publish(new LightlessNotificationDismissMessage("pair_download_progress"));
} }
}); });
Mediator.Subscribe<GposeStartMessage>(this, (_) => IsOpen = false); Mediator.Subscribe<GposeStartMessage>(this, (_) => IsOpen = false);
@@ -116,7 +117,7 @@ public class DownloadUi : WindowMediatorSubscriberBase
} }
catch catch
{ {
// ignore errors thrown from UI _logger.LogDebug("Error drawing upload progress");
} }
try try
@@ -131,17 +132,19 @@ public class DownloadUi : WindowMediatorSubscriberBase
// Use notification system // Use notification system
if (_currentDownloads.Any()) if (_currentDownloads.Any())
{ {
UpdateDownloadNotification(limiterSnapshot); UpdateDownloadNotificationIfChanged(limiterSnapshot);
_notificationDismissed = false; _notificationDismissed = false;
} }
else if (!_notificationDismissed) else if (!_notificationDismissed)
{ {
_notificationService.DismissPairDownloadNotification(); Mediator.Publish(new LightlessNotificationDismissMessage("pair_download_progress"));
_notificationDismissed = true; _notificationDismissed = true;
_lastDownloadStateHash = 0;
} }
} }
else else
{ {
// Use text overlay
if (limiterSnapshot.IsEnabled) if (limiterSnapshot.IsEnabled)
{ {
var queueColor = limiterSnapshot.Waiting > 0 ? ImGuiColors.DalamudYellow : ImGuiColors.DalamudGrey; var queueColor = limiterSnapshot.Waiting > 0 ? ImGuiColors.DalamudYellow : ImGuiColors.DalamudGrey;
@@ -183,7 +186,7 @@ public class DownloadUi : WindowMediatorSubscriberBase
} }
catch catch
{ {
// ignore errors thrown from UI _logger.LogDebug("Error drawing download progress");
} }
} }
@@ -255,7 +258,7 @@ public class DownloadUi : WindowMediatorSubscriberBase
} }
catch catch
{ {
// ignore errors thrown on UI _logger.LogDebug("Error drawing upload progress");
} }
} }
} }
@@ -298,20 +301,34 @@ public class DownloadUi : WindowMediatorSubscriberBase
}; };
} }
private void UpdateDownloadNotification(PairProcessingLimiterSnapshot limiterSnapshot) private void UpdateDownloadNotificationIfChanged(PairProcessingLimiterSnapshot limiterSnapshot)
{ {
var downloadStatus = new List<(string playerName, float progress, string status)>(); var downloadStatus = new List<(string playerName, float progress, string status)>(_currentDownloads.Count);
var hashCode = new HashCode();
foreach (var item in _currentDownloads.ToList()) foreach (var item in _currentDownloads)
{ {
var dlSlot = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.WaitingForSlot); var dlSlot = 0;
var dlQueue = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.WaitingForQueue); var dlQueue = 0;
var dlProg = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.Downloading); var dlProg = 0;
var dlDecomp = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.Decompressing); var dlDecomp = 0;
var totalFiles = item.Value.Sum(c => c.Value.TotalFiles); long totalBytes = 0;
var transferredFiles = item.Value.Sum(c => c.Value.TransferredFiles); long transferredBytes = 0;
var totalBytes = item.Value.Sum(c => c.Value.TotalBytes);
var transferredBytes = item.Value.Sum(c => c.Value.TransferredBytes); // Single pass through the dictionary to count everything - avoid multiple LINQ iterations
foreach (var entry in item.Value)
{
var fileStatus = entry.Value;
switch (fileStatus.DownloadStatus)
{
case DownloadStatus.WaitingForSlot: dlSlot++; break;
case DownloadStatus.WaitingForQueue: dlQueue++; break;
case DownloadStatus.Downloading: dlProg++; break;
case DownloadStatus.Decompressing: dlDecomp++; break;
}
totalBytes += fileStatus.TotalBytes;
transferredBytes += fileStatus.TransferredBytes;
}
var progress = totalBytes > 0 ? (float)transferredBytes / totalBytes : 0f; var progress = totalBytes > 0 ? (float)transferredBytes / totalBytes : 0f;
@@ -323,13 +340,27 @@ public class DownloadUi : WindowMediatorSubscriberBase
else status = "completed"; else status = "completed";
downloadStatus.Add((item.Key.Name, progress, status)); downloadStatus.Add((item.Key.Name, progress, status));
// Build hash from meaningful state
hashCode.Add(item.Key.Name);
hashCode.Add(transferredBytes);
hashCode.Add(totalBytes);
hashCode.Add(status);
} }
// Pass queue waiting count separately, show notification if there are downloads or queue items
var queueWaiting = limiterSnapshot.IsEnabled ? limiterSnapshot.Waiting : 0; var queueWaiting = limiterSnapshot.IsEnabled ? limiterSnapshot.Waiting : 0;
if (downloadStatus.Any() || queueWaiting > 0) hashCode.Add(queueWaiting);
var currentHash = hashCode.ToHashCode();
// Only update notification if state has actually changed
if (currentHash != _lastDownloadStateHash)
{ {
_notificationService.ShowPairDownloadNotification(downloadStatus, queueWaiting); _lastDownloadStateHash = currentHash;
if (downloadStatus.Count > 0 || queueWaiting > 0)
{
_notificationService.ShowPairDownloadNotification(downloadStatus, queueWaiting);
}
} }
} }

View File

@@ -22,10 +22,16 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase
private const float WindowPaddingOffset = 6f; private const float WindowPaddingOffset = 6f;
private const float SlideAnimationDistance = 100f; private const float SlideAnimationDistance = 100f;
private const float OutAnimationSpeedMultiplier = 0.7f; private const float OutAnimationSpeedMultiplier = 0.7f;
private const float ContentPaddingX = 10f;
private const float ContentPaddingY = 6f;
private const float TitleMessageSpacing = 4f;
private const float ActionButtonSpacing = 8f;
private readonly List<LightlessNotification> _notifications = new(); private readonly List<LightlessNotification> _notifications = new();
private readonly object _notificationLock = new(); private readonly object _notificationLock = new();
private readonly LightlessConfigService _configService; private readonly LightlessConfigService _configService;
private readonly Dictionary<string, float> _notificationYOffsets = new();
private readonly Dictionary<string, float> _notificationTargetYOffsets = new();
public LightlessNotificationUI(ILogger<LightlessNotificationUI> logger, LightlessMediator mediator, PerformanceCollectorService performanceCollector, LightlessConfigService configService) public LightlessNotificationUI(ILogger<LightlessNotificationUI> logger, LightlessMediator mediator, PerformanceCollectorService performanceCollector, LightlessConfigService configService)
: base(logger, mediator, "Lightless Notifications##LightlessNotifications", performanceCollector) : base(logger, mediator, "Lightless Notifications##LightlessNotifications", performanceCollector)
@@ -49,12 +55,11 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase
Mediator.Subscribe<LightlessNotificationMessage>(this, HandleNotificationMessage); Mediator.Subscribe<LightlessNotificationMessage>(this, HandleNotificationMessage);
Mediator.Subscribe<LightlessNotificationDismissMessage>(this, HandleNotificationDismissMessage); Mediator.Subscribe<LightlessNotificationDismissMessage>(this, HandleNotificationDismissMessage);
Mediator.Subscribe<ClearAllNotificationsMessage>(this, HandleClearAllNotifications);
} }
private void HandleNotificationMessage(LightlessNotificationMessage message) => private void HandleNotificationMessage(LightlessNotificationMessage message) => AddNotification(message.Notification);
AddNotification(message.Notification); private void HandleNotificationDismissMessage(LightlessNotificationDismissMessage message) => RemoveNotification(message.NotificationId);
private void HandleClearAllNotifications(ClearAllNotificationsMessage message) => ClearAllNotifications();
private void HandleNotificationDismissMessage(LightlessNotificationDismissMessage message) =>
RemoveNotification(message.NotificationId);
public void AddNotification(LightlessNotification notification) public void AddNotification(LightlessNotification notification)
{ {
@@ -96,12 +101,26 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase
} }
} }
public void ClearAllNotifications()
{
lock (_notificationLock)
{
foreach (var notification in _notifications)
{
StartOutAnimation(notification);
}
}
}
private void StartOutAnimation(LightlessNotification notification) private void StartOutAnimation(LightlessNotification notification)
{ {
notification.IsAnimatingOut = true; notification.IsAnimatingOut = true;
notification.IsAnimatingIn = false; notification.IsAnimatingIn = false;
} }
private bool ShouldRemoveNotification(LightlessNotification notification) =>
notification.IsAnimatingOut && notification.AnimationProgress <= 0.01f;
protected override void DrawInternal() protected override void DrawInternal()
{ {
ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, Vector2.Zero); ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, Vector2.Zero);
@@ -118,7 +137,11 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase
} }
var viewport = ImGui.GetMainViewport(); var viewport = ImGui.GetMainViewport();
// Window auto-resizes based on content (AlwaysAutoResize flag)
Position = CalculateWindowPosition(viewport); Position = CalculateWindowPosition(viewport);
PositionCondition = ImGuiCond.Always;
DrawAllNotifications(); DrawAllNotifications();
} }
@@ -127,24 +150,32 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase
private Vector2 CalculateWindowPosition(ImGuiViewportPtr viewport) private Vector2 CalculateWindowPosition(ImGuiViewportPtr viewport)
{ {
var x = viewport.WorkPos.X + viewport.WorkSize.X - var corner = _configService.Current.NotificationCorner;
_configService.Current.NotificationWidth - var offsetX = _configService.Current.NotificationOffsetX;
_configService.Current.NotificationOffsetX - var width = _configService.Current.NotificationWidth;
WindowPaddingOffset;
var y = viewport.WorkPos.Y + _configService.Current.NotificationOffsetY; float posX = corner == NotificationCorner.Left
return new Vector2(x, y); ? viewport.WorkPos.X + offsetX - WindowPaddingOffset
: viewport.WorkPos.X + viewport.WorkSize.X - width - offsetX - WindowPaddingOffset;
return new Vector2(posX, viewport.WorkPos.Y);
} }
private void DrawAllNotifications() private void DrawAllNotifications()
{ {
var offsetY = _configService.Current.NotificationOffsetY;
var startY = ImGui.GetCursorPosY() + offsetY;
for (int i = 0; i < _notifications.Count; i++) for (int i = 0; i < _notifications.Count; i++)
{ {
DrawNotification(_notifications[i], i); var notification = _notifications[i];
if (i < _notifications.Count - 1) if (_notificationYOffsets.TryGetValue(notification.Id, out var yOffset))
{ {
ImGui.Dummy(new Vector2(0, _configService.Current.NotificationSpacing)); ImGui.SetCursorPosY(startY + yOffset);
} }
DrawNotification(notification, i);
} }
} }
@@ -174,18 +205,65 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase
private void UpdateAnimationsAndRemoveExpired(float deltaTime) private void UpdateAnimationsAndRemoveExpired(float deltaTime)
{ {
UpdateTargetYPositions();
for (int i = _notifications.Count - 1; i >= 0; i--) for (int i = _notifications.Count - 1; i >= 0; i--)
{ {
var notification = _notifications[i]; var notification = _notifications[i];
UpdateNotificationAnimation(notification, deltaTime); UpdateNotificationAnimation(notification, deltaTime);
UpdateNotificationYOffset(notification, deltaTime);
if (ShouldRemoveNotification(notification)) if (ShouldRemoveNotification(notification))
{ {
_notifications.RemoveAt(i); _notifications.RemoveAt(i);
_notificationYOffsets.Remove(notification.Id);
_notificationTargetYOffsets.Remove(notification.Id);
} }
} }
} }
private void UpdateTargetYPositions()
{
float currentY = 0f;
for (int i = 0; i < _notifications.Count; i++)
{
var notification = _notifications[i];
if (!_notificationTargetYOffsets.ContainsKey(notification.Id))
{
_notificationTargetYOffsets[notification.Id] = currentY;
_notificationYOffsets[notification.Id] = currentY;
}
else
{
_notificationTargetYOffsets[notification.Id] = currentY;
}
currentY += CalculateNotificationHeight(notification) + _configService.Current.NotificationSpacing;
}
}
private void UpdateNotificationYOffset(LightlessNotification notification, float deltaTime)
{
if (!_notificationYOffsets.ContainsKey(notification.Id) || !_notificationTargetYOffsets.ContainsKey(notification.Id))
return;
var current = _notificationYOffsets[notification.Id];
var target = _notificationTargetYOffsets[notification.Id];
var diff = target - current;
if (Math.Abs(diff) < 0.5f)
{
_notificationYOffsets[notification.Id] = target;
}
else
{
var speed = _configService.Current.NotificationSlideSpeed;
_notificationYOffsets[notification.Id] = current + (diff * deltaTime * speed);
}
}
private void UpdateNotificationAnimation(LightlessNotification notification, float deltaTime) private void UpdateNotificationAnimation(LightlessNotification notification, float deltaTime)
{ {
if (notification.IsAnimatingIn && notification.AnimationProgress < 1f) if (notification.IsAnimatingIn && notification.AnimationProgress < 1f)
@@ -209,20 +287,24 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase
} }
} }
private bool ShouldRemoveNotification(LightlessNotification notification) => private Vector2 CalculateSlideOffset(float alpha)
notification.IsAnimatingOut && notification.AnimationProgress <= 0.01f; {
var distance = (1f - alpha) * SlideAnimationDistance;
var corner = _configService.Current.NotificationCorner;
return corner == NotificationCorner.Left ? new Vector2(-distance, 0) : new Vector2(distance, 0);
}
private void DrawNotification(LightlessNotification notification, int index) private void DrawNotification(LightlessNotification notification, int index)
{ {
var alpha = notification.AnimationProgress; var alpha = notification.AnimationProgress;
if (alpha <= 0f) return; if (alpha <= 0f) return;
var slideOffset = (1f - alpha) * SlideAnimationDistance; var slideOffset = CalculateSlideOffset(alpha);
var originalCursorPos = ImGui.GetCursorPos(); var originalCursorPos = ImGui.GetCursorPos();
ImGui.SetCursorPosX(originalCursorPos.X + slideOffset); ImGui.SetCursorPos(originalCursorPos + slideOffset);
var notificationHeight = CalculateNotificationHeight(notification); var notificationHeight = CalculateNotificationHeight(notification);
var notificationWidth = _configService.Current.NotificationWidth - slideOffset; var notificationWidth = _configService.Current.NotificationWidth;
ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, Vector2.Zero); ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, Vector2.Zero);
@@ -308,15 +390,28 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase
private void DrawAccentBar(ImDrawListPtr drawList, Vector2 windowPos, Vector2 windowSize, Vector4 accentColor) private void DrawAccentBar(ImDrawListPtr drawList, Vector2 windowPos, Vector2 windowSize, Vector4 accentColor)
{ {
var accentWidth = _configService.Current.NotificationAccentBarWidth; var accentWidth = _configService.Current.NotificationAccentBarWidth;
if (accentWidth > 0f) if (accentWidth <= 0f) return;
var corner = _configService.Current.NotificationCorner;
Vector2 accentStart, accentEnd;
if (corner == NotificationCorner.Left)
{ {
drawList.AddRectFilled( accentStart = windowPos + new Vector2(windowSize.X - accentWidth, 0);
windowPos, accentEnd = windowPos + windowSize;
windowPos + new Vector2(accentWidth, windowSize.Y),
ImGui.ColorConvertFloat4ToU32(accentColor),
3f
);
} }
else
{
accentStart = windowPos;
accentEnd = windowPos + new Vector2(accentWidth, windowSize.Y);
}
drawList.AddRectFilled(
accentStart,
accentEnd,
ImGui.ColorConvertFloat4ToU32(accentColor),
3f
);
} }
private void DrawDurationProgressBar(LightlessNotification notification, float alpha, Vector2 windowPos, Vector2 windowSize, ImDrawListPtr drawList) private void DrawDurationProgressBar(LightlessNotification notification, float alpha, Vector2 windowPos, Vector2 windowSize, ImDrawListPtr drawList)
@@ -371,82 +466,113 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase
private void DrawNotificationText(LightlessNotification notification, float alpha) private void DrawNotificationText(LightlessNotification notification, float alpha)
{ {
var padding = new Vector2(10f, 6f); var contentPos = new Vector2(ContentPaddingX, ContentPaddingY);
var contentPos = new Vector2(padding.X, padding.Y);
var windowSize = ImGui.GetWindowSize(); var windowSize = ImGui.GetWindowSize();
var contentSize = new Vector2(windowSize.X - padding.X, windowSize.Y - padding.Y * 2); var contentWidth = CalculateContentWidth(windowSize.X);
ImGui.SetCursorPos(contentPos); ImGui.SetCursorPos(contentPos);
var titleHeight = DrawTitle(notification, contentSize.X, alpha); var titleHeight = DrawTitle(notification, contentWidth, alpha);
DrawMessage(notification, contentPos, contentSize.X, titleHeight, alpha); DrawMessage(notification, contentPos, contentWidth, titleHeight, alpha);
if (notification.Actions.Count > 0) if (HasActions(notification))
{ {
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + ImGui.GetStyle().ItemSpacing.Y); PositionActionsAtBottom(windowSize.Y);
ImGui.SetCursorPosX(contentPos.X); DrawNotificationActions(notification, contentWidth, alpha);
DrawNotificationActions(notification, contentSize.X, alpha);
} }
} }
private float CalculateContentWidth(float windowWidth) =>
windowWidth - (ContentPaddingX * 2);
private bool HasActions(LightlessNotification notification) =>
notification.Actions.Count > 0;
private void PositionActionsAtBottom(float windowHeight)
{
var actionHeight = ImGui.GetFrameHeight();
var bottomY = windowHeight - ContentPaddingY - actionHeight;
ImGui.SetCursorPosY(bottomY);
ImGui.SetCursorPosX(ContentPaddingX);
}
private float DrawTitle(LightlessNotification notification, float contentWidth, float alpha) private float DrawTitle(LightlessNotification notification, float contentWidth, float alpha)
{ {
using (ImRaii.PushColor(ImGuiCol.Text, new Vector4(1f, 1f, 1f, alpha))) var titleColor = new Vector4(1f, 1f, 1f, alpha);
var titleText = FormatTitleText(notification);
using (ImRaii.PushColor(ImGuiCol.Text, titleColor))
{ {
ImGui.PushTextWrapPos(ImGui.GetCursorPosX() + contentWidth); return DrawWrappedText(titleText, contentWidth);
var titleStartY = ImGui.GetCursorPosY();
var titleText = _configService.Current.ShowNotificationTimestamp
? $"[{notification.CreatedAt.ToLocalTime():HH:mm:ss}] {notification.Title}"
: notification.Title;
ImGui.TextWrapped(titleText);
var titleHeight = ImGui.GetCursorPosY() - titleStartY;
ImGui.PopTextWrapPos();
return titleHeight;
} }
} }
private string FormatTitleText(LightlessNotification notification)
{
if (!_configService.Current.ShowNotificationTimestamp)
return notification.Title;
var timestamp = notification.CreatedAt.ToLocalTime().ToString("HH:mm:ss");
return $"[{timestamp}] {notification.Title}";
}
private float DrawWrappedText(string text, float wrapWidth)
{
ImGui.PushTextWrapPos(ImGui.GetCursorPosX() + wrapWidth);
var startY = ImGui.GetCursorPosY();
ImGui.TextWrapped(text);
var height = ImGui.GetCursorPosY() - startY;
ImGui.PopTextWrapPos();
return height;
}
private void DrawMessage(LightlessNotification notification, Vector2 contentPos, float contentWidth, float titleHeight, float alpha) private void DrawMessage(LightlessNotification notification, Vector2 contentPos, float contentWidth, float titleHeight, float alpha)
{ {
if (string.IsNullOrEmpty(notification.Message)) return; if (string.IsNullOrEmpty(notification.Message)) return;
ImGui.SetCursorPos(contentPos + new Vector2(0f, titleHeight + 4f)); var messagePos = contentPos + new Vector2(0f, titleHeight + TitleMessageSpacing);
ImGui.PushTextWrapPos(ImGui.GetCursorPosX() + contentWidth); var messageColor = new Vector4(0.9f, 0.9f, 0.9f, alpha);
using (ImRaii.PushColor(ImGuiCol.Text, new Vector4(0.9f, 0.9f, 0.9f, alpha)))
ImGui.SetCursorPos(messagePos);
using (ImRaii.PushColor(ImGuiCol.Text, messageColor))
{ {
ImGui.TextWrapped(notification.Message); DrawWrappedText(notification.Message, contentWidth);
} }
ImGui.PopTextWrapPos();
} }
private void DrawNotificationActions(LightlessNotification notification, float availableWidth, float alpha) private void DrawNotificationActions(LightlessNotification notification, float availableWidth, float alpha)
{ {
var buttonSpacing = 8f; var buttonWidth = CalculateActionButtonWidth(notification.Actions.Count, availableWidth);
var rightPadding = 10f;
var usableWidth = availableWidth - rightPadding;
var totalSpacing = (notification.Actions.Count - 1) * buttonSpacing;
var buttonWidth = (usableWidth - totalSpacing) / notification.Actions.Count;
_logger.LogDebug("Drawing {ActionCount} notification actions, buttonWidth: {ButtonWidth}, availableWidth: {AvailableWidth}", _logger.LogDebug("Drawing {ActionCount} notification actions, buttonWidth: {ButtonWidth}, availableWidth: {AvailableWidth}",
notification.Actions.Count, buttonWidth, availableWidth); notification.Actions.Count, buttonWidth, availableWidth);
var startCursorPos = ImGui.GetCursorPos(); var startX = ImGui.GetCursorPosX();
for (int i = 0; i < notification.Actions.Count; i++) for (int i = 0; i < notification.Actions.Count; i++)
{ {
var action = notification.Actions[i];
if (i > 0) if (i > 0)
{ {
ImGui.SameLine(); ImGui.SameLine();
var currentX = startCursorPos.X + i * (buttonWidth + buttonSpacing); PositionActionButton(i, startX, buttonWidth);
ImGui.SetCursorPosX(currentX);
} }
DrawActionButton(action, notification, alpha, buttonWidth); DrawActionButton(notification.Actions[i], notification, alpha, buttonWidth);
} }
} }
private float CalculateActionButtonWidth(int actionCount, float availableWidth)
{
var totalSpacing = (actionCount - 1) * ActionButtonSpacing;
return (availableWidth - totalSpacing) / actionCount;
}
private void PositionActionButton(int index, float startX, float buttonWidth)
{
var xPosition = startX + index * (buttonWidth + ActionButtonSpacing);
ImGui.SetCursorPosX(xPosition);
}
private void DrawActionButton(LightlessNotificationAction action, LightlessNotification notification, float alpha, float buttonWidth) private void DrawActionButton(LightlessNotificationAction action, LightlessNotification notification, float alpha, float buttonWidth)
{ {
_logger.LogDebug("Drawing action button: {ActionId} - {ActionLabel}, width: {ButtonWidth}", action.Id, action.Label, buttonWidth); _logger.LogDebug("Drawing action button: {ActionId} - {ActionLabel}, width: {ButtonWidth}", action.Id, action.Label, buttonWidth);
@@ -543,7 +669,7 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase
private float CalculateNotificationHeight(LightlessNotification notification) private float CalculateNotificationHeight(LightlessNotification notification)
{ {
var contentWidth = _configService.Current.NotificationWidth - 35f; var contentWidth = CalculateContentWidth(_configService.Current.NotificationWidth);
var height = 12f; var height = 12f;
height += CalculateTitleHeight(notification, contentWidth); height += CalculateTitleHeight(notification, contentWidth);
@@ -590,6 +716,8 @@ public class LightlessNotificationUI : WindowMediatorSubscriberBase
NotificationType.Error => UIColors.Get("DimRed"), NotificationType.Error => UIColors.Get("DimRed"),
NotificationType.PairRequest => UIColors.Get("LightlessBlue"), NotificationType.PairRequest => UIColors.Get("LightlessBlue"),
NotificationType.Download => UIColors.Get("LightlessGreen"), NotificationType.Download => UIColors.Get("LightlessGreen"),
NotificationType.Performance => UIColors.Get("LightlessOrange"),
_ => UIColors.Get("LightlessPurple") _ => UIColors.Get("LightlessPurple")
}; };
} }

View File

@@ -1990,7 +1990,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
("LightlessBlue", "Secondary Blue", "Secondary title colors, visable pairs"), ("LightlessBlue", "Secondary Blue", "Secondary title colors, visable pairs"),
("LightlessGreen", "Success Green", "Join buttons and success messages"), ("LightlessGreen", "Success Green", "Join buttons and success messages"),
("LightlessYellow", "Warning Yellow", "Warning colors"), ("LightlessYellow", "Warning Yellow", "Warning colors"),
("LightlessYellow2", "Warning Yellow (Alt)", "Warning colors"), ("LightlessOrange", "Performance Orange", "Performance notifications and warnings"),
("PairBlue", "Syncshell Blue", "Syncshell headers, toggle highlights, and moderator actions"), ("PairBlue", "Syncshell Blue", "Syncshell headers, toggle highlights, and moderator actions"),
("DimRed", "Error Red", "Error and offline colors") ("DimRed", "Error Red", "Error and offline colors")
}; };
@@ -3493,69 +3493,192 @@ public class SettingsUi : WindowMediatorSubscriberBase
if (useLightlessNotifications) if (useLightlessNotifications)
{ {
// Lightless notification locations // Lightless notification locations
ImGui.Indent();
var lightlessLocations = GetLightlessNotificationLocations(); var lightlessLocations = GetLightlessNotificationLocations();
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("Info Notifications:");
ImGui.SameLine();
ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale);
_uiShared.DrawCombo("###enhanced_info", lightlessLocations, GetNotificationLocationLabel, (location) =>
{
_configService.Current.LightlessInfoNotification = location;
_configService.Save();
}, _configService.Current.LightlessInfoNotification);
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("Warning Notifications:");
ImGui.SameLine();
ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale);
_uiShared.DrawCombo("###enhanced_warning", lightlessLocations, GetNotificationLocationLabel,
(location) =>
{
_configService.Current.LightlessWarningNotification = location;
_configService.Save();
}, _configService.Current.LightlessWarningNotification);
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("Error Notifications:");
ImGui.SameLine();
ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale);
_uiShared.DrawCombo("###enhanced_error", lightlessLocations, GetNotificationLocationLabel, (location) =>
{
_configService.Current.LightlessErrorNotification = location;
_configService.Save();
}, _configService.Current.LightlessErrorNotification);
ImGuiHelpers.ScaledDummy(3);
_uiShared.DrawHelpText("Special notification types:");
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("Pair Request Notifications:");
ImGui.SameLine();
ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale);
_uiShared.DrawCombo("###enhanced_pairrequest", lightlessLocations, GetNotificationLocationLabel,
(location) =>
{
_configService.Current.LightlessPairRequestNotification = location;
_configService.Save();
}, _configService.Current.LightlessPairRequestNotification);
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("Download Progress Notifications:");
ImGui.SameLine();
ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale);
var downloadLocations = GetDownloadNotificationLocations(); var downloadLocations = GetDownloadNotificationLocations();
_uiShared.DrawCombo("###enhanced_download", downloadLocations, GetNotificationLocationLabel,
(location) => if (ImGui.BeginTable("##NotificationLocationTable", 3, ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit))
{
ImGui.TableSetupColumn("Notification Type", ImGuiTableColumnFlags.WidthFixed, 200f * ImGuiHelpers.GlobalScale);
ImGui.TableSetupColumn("Location", ImGuiTableColumnFlags.WidthStretch);
ImGui.TableSetupColumn("Test", ImGuiTableColumnFlags.WidthFixed, 40f * ImGuiHelpers.GlobalScale);
ImGui.TableHeadersRow();
ImGui.TableNextRow();
ImGui.TableSetColumnIndex(0);
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("Info Notifications");
ImGui.TableSetColumnIndex(1);
ImGui.SetNextItemWidth(-1);
_uiShared.DrawCombo("###enhanced_info", lightlessLocations, GetNotificationLocationLabel, (location) =>
{ {
_configService.Current.LightlessDownloadNotification = location; _configService.Current.LightlessInfoNotification = location;
_configService.Save(); _configService.Save();
}, _configService.Current.LightlessDownloadNotification); }, _configService.Current.LightlessInfoNotification);
ImGui.TableSetColumnIndex(2);
var availableWidth = ImGui.GetContentRegionAvail().X;
using (ImRaii.PushFont(UiBuilder.IconFont))
{
if (ImGui.Button($"{FontAwesomeIcon.Play.ToIconString()}##test_info", new Vector2(availableWidth, 0)))
{
Mediator.Publish(new NotificationMessage("Test Info",
"This is a test info notification to let you know Chocola is cute :3", NotificationType.Info));
}
}
UiSharedService.AttachToolTip("Test info notification");
ImGui.TableNextRow();
ImGui.TableSetColumnIndex(0);
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("Warning Notifications");
ImGui.TableSetColumnIndex(1);
ImGui.SetNextItemWidth(-1);
_uiShared.DrawCombo("###enhanced_warning", lightlessLocations, GetNotificationLocationLabel,
(location) =>
{
_configService.Current.LightlessWarningNotification = location;
_configService.Save();
}, _configService.Current.LightlessWarningNotification);
ImGui.TableSetColumnIndex(2);
availableWidth = ImGui.GetContentRegionAvail().X;
using (ImRaii.PushFont(UiBuilder.IconFont))
{
if (ImGui.Button($"{FontAwesomeIcon.Play.ToIconString()}##test_warning", new Vector2(availableWidth, 0)))
{
Mediator.Publish(new NotificationMessage("Test Warning", "This is a test warning notification!",
NotificationType.Warning));
}
}
UiSharedService.AttachToolTip("Test warning notification");
ImGui.Unindent(); ImGui.TableNextRow();
ImGui.TableSetColumnIndex(0);
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("Error Notifications");
ImGui.TableSetColumnIndex(1);
ImGui.SetNextItemWidth(-1);
_uiShared.DrawCombo("###enhanced_error", lightlessLocations, GetNotificationLocationLabel, (location) =>
{
_configService.Current.LightlessErrorNotification = location;
_configService.Save();
}, _configService.Current.LightlessErrorNotification);
ImGui.TableSetColumnIndex(2);
availableWidth = ImGui.GetContentRegionAvail().X;
using (ImRaii.PushFont(UiBuilder.IconFont))
{
if (ImGui.Button($"{FontAwesomeIcon.Play.ToIconString()}##test_error", new Vector2(availableWidth, 0)))
{
Mediator.Publish(new NotificationMessage("Test Error", "This is a test error notification!",
NotificationType.Error));
}
}
UiSharedService.AttachToolTip("Test error notification");
ImGui.TableNextRow();
ImGui.TableSetColumnIndex(0);
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("Pair Request Notifications");
ImGui.TableSetColumnIndex(1);
ImGui.SetNextItemWidth(-1);
_uiShared.DrawCombo("###enhanced_pairrequest", lightlessLocations, GetNotificationLocationLabel,
(location) =>
{
_configService.Current.LightlessPairRequestNotification = location;
_configService.Save();
}, _configService.Current.LightlessPairRequestNotification);
ImGui.TableSetColumnIndex(2);
availableWidth = ImGui.GetContentRegionAvail().X;
using (ImRaii.PushFont(UiBuilder.IconFont))
{
if (ImGui.Button($"{FontAwesomeIcon.Play.ToIconString()}##test_pair", new Vector2(availableWidth, 0)))
{
_lightlessNotificationService.ShowPairRequestNotification(
"Test User",
"test-uid-123",
() =>
{
Mediator.Publish(new NotificationMessage("Accepted", "You accepted the test pair request.",
NotificationType.Info));
},
() =>
{
Mediator.Publish(new NotificationMessage("Declined", "You declined the test pair request.",
NotificationType.Info));
}
);
}
}
UiSharedService.AttachToolTip("Test pair request notification");
ImGui.TableNextRow();
ImGui.TableSetColumnIndex(0);
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("Download Progress Notifications");
ImGui.TableSetColumnIndex(1);
ImGui.SetNextItemWidth(-1);
_uiShared.DrawCombo("###enhanced_download", downloadLocations, GetNotificationLocationLabel,
(location) =>
{
_configService.Current.LightlessDownloadNotification = location;
_configService.Save();
}, _configService.Current.LightlessDownloadNotification);
ImGui.TableSetColumnIndex(2);
availableWidth = ImGui.GetContentRegionAvail().X;
using (ImRaii.PushFont(UiBuilder.IconFont))
{
if (ImGui.Button($"{FontAwesomeIcon.Play.ToIconString()}##test_download", new Vector2(availableWidth, 0)))
{
_lightlessNotificationService.ShowPairDownloadNotification(
new List<(string playerName, float progress, string status)>
{
("Player One", 0.35f, "downloading"),
("Player Two", 0.75f, "downloading"),
("Player Three", 1.0f, "downloading")
},
queueWaiting: 2
);
}
}
UiSharedService.AttachToolTip("Test download progress notification");
ImGui.TableNextRow();
ImGui.TableSetColumnIndex(0);
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("Performance Notifications");
ImGui.TableSetColumnIndex(1);
ImGui.SetNextItemWidth(-1);
_uiShared.DrawCombo("###enhanced_performance", lightlessLocations, GetNotificationLocationLabel,
(location) =>
{
_configService.Current.LightlessPerformanceNotification = location;
_configService.Save();
}, _configService.Current.LightlessPerformanceNotification);
ImGui.TableSetColumnIndex(2);
availableWidth = ImGui.GetContentRegionAvail().X;
using (ImRaii.PushFont(UiBuilder.IconFont))
{
if (ImGui.Button($"{FontAwesomeIcon.Play.ToIconString()}##test_performance", new Vector2(availableWidth, 0)))
{
var testUserData = new UserData("TEST123", "TestUser", false, false, false, null, null);
Mediator.Publish(new PerformanceNotificationMessage(
"Test Player (TestUser) exceeds performance threshold(s)",
"Player Test Player (TestUser) exceeds your configured VRAM warning threshold\n500 MB/300 MB",
testUserData,
false,
"Test Player"
));
}
}
UiSharedService.AttachToolTip("Test performance notification");
ImGui.EndTable();
}
ImGuiHelpers.ScaledDummy(5);
if (_uiShared.IconTextButton(FontAwesomeIcon.Trash, "Clear All Notifications"))
{
Mediator.Publish(new ClearAllNotificationsMessage());
}
_uiShared.DrawHelpText("Dismiss all active notifications immediately.");
} }
else else
{ {
@@ -3602,73 +3725,6 @@ public class SettingsUi : WindowMediatorSubscriberBase
ImGui.Separator(); ImGui.Separator();
if (useLightlessNotifications) if (useLightlessNotifications)
{ {
if (_uiShared.MediumTreeNode("Test Notifications", UIColors.Get("LightlessPurple")))
{
ImGui.Indent();
// Test notification buttons
if (_uiShared.IconTextButton(FontAwesomeIcon.Bell, "Test Info"))
{
Mediator.Publish(new NotificationMessage("Test Info",
"This is a test info notification to let you know Chocola is cute :3", NotificationType.Info));
}
ImGui.SameLine();
if (_uiShared.IconTextButton(FontAwesomeIcon.ExclamationTriangle, "Test Warning"))
{
Mediator.Publish(new NotificationMessage("Test Warning", "This is a test warning notification!",
NotificationType.Warning));
}
ImGui.SameLine();
if (_uiShared.IconTextButton(FontAwesomeIcon.ExclamationCircle, "Test Error"))
{
Mediator.Publish(new NotificationMessage("Test Error", "This is a test error notification!",
NotificationType.Error));
}
ImGuiHelpers.ScaledDummy(3);
if (_uiShared.IconTextButton(FontAwesomeIcon.UserPlus, "Test Pair Request"))
{
_lightlessNotificationService.ShowPairRequestNotification(
"Test User",
"test-uid-123",
() =>
{
Mediator.Publish(new NotificationMessage("Accepted", "You accepted the test pair request.",
NotificationType.Info));
},
() =>
{
Mediator.Publish(new NotificationMessage("Declined", "You declined the test pair request.",
NotificationType.Info));
}
);
}
ImGui.SameLine();
if (_uiShared.IconTextButton(FontAwesomeIcon.Download, "Test Download Progress"))
{
_lightlessNotificationService.ShowPairDownloadNotification(
new List<(string playerName, float progress, string status)>
{
("Player One", 0.35f, "downloading"),
("Player Two", 0.75f, "downloading"),
("Player Three", 1.0f, "downloading")
},
queueWaiting: 2
);
}
_uiShared.DrawHelpText("Preview how notifications will appear with your current settings.");
ImGui.Unindent();
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f);
ImGui.TreePop();
}
ImGui.Separator();
if (_uiShared.MediumTreeNode("Basic Settings", UIColors.Get("LightlessPurple"))) if (_uiShared.MediumTreeNode("Basic Settings", UIColors.Get("LightlessPurple")))
{ {
int maxNotifications = _configService.Current.MaxSimultaneousNotifications; int maxNotifications = _configService.Current.MaxSimultaneousNotifications;
@@ -3768,10 +3824,28 @@ public class SettingsUi : WindowMediatorSubscriberBase
ImGui.Spacing(); ImGui.Spacing();
ImGui.TextUnformatted("Position"); ImGui.TextUnformatted("Position");
int offsetY = _configService.Current.NotificationOffsetY; var currentCorner = _configService.Current.NotificationCorner;
if (ImGui.SliderInt("Vertical Offset", ref offsetY, 0, 500)) if (ImGui.BeginCombo("Notification Position", GetNotificationCornerLabel(currentCorner)))
{ {
_configService.Current.NotificationOffsetY = Math.Clamp(offsetY, 0, 500); foreach (NotificationCorner corner in Enum.GetValues(typeof(NotificationCorner)))
{
bool isSelected = currentCorner == corner;
if (ImGui.Selectable(GetNotificationCornerLabel(corner), isSelected))
{
_configService.Current.NotificationCorner = corner;
_configService.Save();
}
if (isSelected)
ImGui.SetItemDefaultFocus();
}
ImGui.EndCombo();
}
_uiShared.DrawHelpText("Choose which corner of the screen notifications appear in.");
int offsetY = _configService.Current.NotificationOffsetY;
if (ImGui.SliderInt("Vertical Offset", ref offsetY, 0, 1000))
{
_configService.Current.NotificationOffsetY = Math.Clamp(offsetY, 0, 1000);
_configService.Save(); _configService.Save();
} }
if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) if (ImGui.IsItemClicked(ImGuiMouseButton.Right))
@@ -3781,7 +3855,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
} }
if (ImGui.IsItemHovered()) if (ImGui.IsItemHovered())
ImGui.SetTooltip("Right click to reset to default (50)."); ImGui.SetTooltip("Right click to reset to default (50).");
_uiShared.DrawHelpText("Move notifications down from the top-right corner."); _uiShared.DrawHelpText("Distance from the top edge of the screen.");
int offsetX = _configService.Current.NotificationOffsetX; int offsetX = _configService.Current.NotificationOffsetX;
if (ImGui.SliderInt("Horizontal Offset", ref offsetX, 0, 500)) if (ImGui.SliderInt("Horizontal Offset", ref offsetX, 0, 500))
@@ -3802,9 +3876,9 @@ public class SettingsUi : WindowMediatorSubscriberBase
ImGui.TextUnformatted("Animation Settings"); ImGui.TextUnformatted("Animation Settings");
float animSpeed = _configService.Current.NotificationAnimationSpeed; float animSpeed = _configService.Current.NotificationAnimationSpeed;
if (ImGui.SliderFloat("Animation Speed", ref animSpeed, 1f, 30f, "%.1f")) if (ImGui.SliderFloat("Animation Speed", ref animSpeed, 1f, 20f, "%.1f"))
{ {
_configService.Current.NotificationAnimationSpeed = Math.Clamp(animSpeed, 1f, 30f); _configService.Current.NotificationAnimationSpeed = Math.Clamp(animSpeed, 1f, 20f);
_configService.Save(); _configService.Save();
} }
if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) if (ImGui.IsItemClicked(ImGuiMouseButton.Right))
@@ -3816,6 +3890,21 @@ public class SettingsUi : WindowMediatorSubscriberBase
ImGui.SetTooltip("Right click to reset to default (10)."); ImGui.SetTooltip("Right click to reset to default (10).");
_uiShared.DrawHelpText("How fast notifications slide in/out. Higher = faster."); _uiShared.DrawHelpText("How fast notifications slide in/out. Higher = faster.");
float slideSpeed = _configService.Current.NotificationSlideSpeed;
if (ImGui.SliderFloat("Slide Speed", ref slideSpeed, 1f, 20f, "%.1f"))
{
_configService.Current.NotificationSlideSpeed = Math.Clamp(slideSpeed, 1f, 20f);
_configService.Save();
}
if (ImGui.IsItemClicked(ImGuiMouseButton.Right))
{
_configService.Current.NotificationSlideSpeed = 10f;
_configService.Save();
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip("Right click to reset to default (10).");
_uiShared.DrawHelpText("How fast notifications slide into position when others disappear. Higher = faster.");
ImGui.Spacing(); ImGui.Spacing();
ImGui.TextUnformatted("Visual Effects"); ImGui.TextUnformatted("Visual Effects");
@@ -3915,6 +4004,20 @@ public class SettingsUi : WindowMediatorSubscriberBase
if (ImGui.IsItemHovered()) if (ImGui.IsItemHovered())
ImGui.SetTooltip("Right click to reset to default (300)."); ImGui.SetTooltip("Right click to reset to default (300).");
int performanceDuration = _configService.Current.PerformanceNotificationDurationSeconds;
if (ImGui.SliderInt("Performance Duration (seconds)", ref performanceDuration, 5, 60))
{
_configService.Current.PerformanceNotificationDurationSeconds = Math.Clamp(performanceDuration, 5, 60);
_configService.Save();
}
if (ImGui.IsItemClicked(ImGuiMouseButton.Right))
{
_configService.Current.PerformanceNotificationDurationSeconds = 20;
_configService.Save();
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip("Right click to reset to default (20).");
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); _uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f);
ImGui.TreePop(); ImGui.TreePop();
} }
@@ -3982,6 +4085,38 @@ public class SettingsUi : WindowMediatorSubscriberBase
ImGui.TreePop(); ImGui.TreePop();
} }
if (_uiShared.MediumTreeNode("Pair Request Notifications", UIColors.Get("PairBlue")))
{
var showPairRequestActions = _configService.Current.ShowPairRequestNotificationActions;
if (ImGui.Checkbox("Show action buttons on pair requests", ref showPairRequestActions))
{
_configService.Current.ShowPairRequestNotificationActions = showPairRequestActions;
_configService.Save();
}
_uiShared.DrawHelpText(
"When you receive a pair request, show Accept/Decline buttons in the notification.");
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f);
ImGui.TreePop();
}
if (_uiShared.MediumTreeNode("Performance Notifications", UIColors.Get("LightlessOrange")))
{
var showPerformanceActions = _configService.Current.ShowPerformanceNotificationActions;
if (ImGui.Checkbox("Show action buttons on performance warnings", ref showPerformanceActions))
{
_configService.Current.ShowPerformanceNotificationActions = showPerformanceActions;
_configService.Save();
}
_uiShared.DrawHelpText(
"When a player exceeds performance thresholds or is auto-paused, show Pause/Unpause buttons in the notification.");
_uiShared.ColoredSeparator(UIColors.Get("LightlessOrange"), 1.5f);
ImGui.TreePop();
}
if (_uiShared.MediumTreeNode("System Notifications", UIColors.Get("LightlessYellow"))) if (_uiShared.MediumTreeNode("System Notifications", UIColors.Get("LightlessYellow")))
{ {
var disableOptionalPluginWarnings = _configService.Current.DisableOptionalPluginWarnings; var disableOptionalPluginWarnings = _configService.Current.DisableOptionalPluginWarnings;
@@ -3999,6 +4134,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
ImGui.Separator(); ImGui.Separator();
// Location descriptions removed - information is now inline with each setting // Location descriptions removed - information is now inline with each setting
} }
} }
@@ -4014,8 +4150,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
{ {
return new[] return new[]
{ {
NotificationLocation.LightlessUi, NotificationLocation.ChatAndLightlessUi, NotificationLocation.LightlessUi, NotificationLocation.TextOverlay, NotificationLocation.Nowhere
NotificationLocation.TextOverlay, NotificationLocation.Nowhere
}; };
} }
@@ -4043,6 +4178,16 @@ public class SettingsUi : WindowMediatorSubscriberBase
}; };
} }
private string GetNotificationCornerLabel(NotificationCorner corner)
{
return corner switch
{
NotificationCorner.Right => "Right",
NotificationCorner.Left => "Left",
_ => corner.ToString()
};
}
private void DrawSoundTable() private void DrawSoundTable()
{ {
var soundEffects = new[] var soundEffects = new[]
@@ -4068,7 +4213,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
("Warning", 1, _configService.Current.CustomWarningSoundId, _configService.Current.DisableWarningSound, 16u), ("Warning", 1, _configService.Current.CustomWarningSoundId, _configService.Current.DisableWarningSound, 16u),
("Error", 2, _configService.Current.CustomErrorSoundId, _configService.Current.DisableErrorSound, 16u), ("Error", 2, _configService.Current.CustomErrorSoundId, _configService.Current.DisableErrorSound, 16u),
("Pair Request", 3, _configService.Current.PairRequestSoundId, _configService.Current.DisablePairRequestSound, 5u), ("Pair Request", 3, _configService.Current.PairRequestSoundId, _configService.Current.DisablePairRequestSound, 5u),
("Download", 4, _configService.Current.DownloadSoundId, _configService.Current.DisableDownloadSound, 15u) ("Performance", 4, _configService.Current.PerformanceSoundId, _configService.Current.DisablePerformanceSound, 16u)
}; };
foreach (var (typeName, typeIndex, currentSoundId, isDisabled, defaultSoundId) in soundTypes) foreach (var (typeName, typeIndex, currentSoundId, isDisabled, defaultSoundId) in soundTypes)
@@ -4087,7 +4232,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
var currentIndex = Array.FindIndex(soundEffects, s => s.Item1 == currentSoundId); var currentIndex = Array.FindIndex(soundEffects, s => s.Item1 == currentSoundId);
if (currentIndex == -1) currentIndex = 1; if (currentIndex == -1) currentIndex = 1;
ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale); ImGui.SetNextItemWidth(-1);
if (ImGui.Combo($"##sound_{typeIndex}", ref currentIndex, if (ImGui.Combo($"##sound_{typeIndex}", ref currentIndex,
soundEffects.Select(s => s.Item2).ToArray(), soundEffects.Length)) soundEffects.Select(s => s.Item2).ToArray(), soundEffects.Length))
{ {
@@ -4098,7 +4243,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
case 1: _configService.Current.CustomWarningSoundId = newSoundId; break; case 1: _configService.Current.CustomWarningSoundId = newSoundId; break;
case 2: _configService.Current.CustomErrorSoundId = newSoundId; break; case 2: _configService.Current.CustomErrorSoundId = newSoundId; break;
case 3: _configService.Current.PairRequestSoundId = newSoundId; break; case 3: _configService.Current.PairRequestSoundId = newSoundId; break;
case 4: _configService.Current.DownloadSoundId = newSoundId; break; case 4: _configService.Current.PerformanceSoundId = newSoundId; break;
} }
_configService.Save(); _configService.Save();
@@ -4152,7 +4297,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
case 1: _configService.Current.DisableWarningSound = newDisabled; break; case 1: _configService.Current.DisableWarningSound = newDisabled; break;
case 2: _configService.Current.DisableErrorSound = newDisabled; break; case 2: _configService.Current.DisableErrorSound = newDisabled; break;
case 3: _configService.Current.DisablePairRequestSound = newDisabled; break; case 3: _configService.Current.DisablePairRequestSound = newDisabled; break;
case 4: _configService.Current.DisableDownloadSound = newDisabled; break; case 4: _configService.Current.DisablePerformanceSound = newDisabled; break;
} }
_configService.Save(); _configService.Save();
} }
@@ -4178,7 +4323,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
case 1: _configService.Current.CustomWarningSoundId = defaultSoundId; break; case 1: _configService.Current.CustomWarningSoundId = defaultSoundId; break;
case 2: _configService.Current.CustomErrorSoundId = defaultSoundId; break; case 2: _configService.Current.CustomErrorSoundId = defaultSoundId; break;
case 3: _configService.Current.PairRequestSoundId = defaultSoundId; break; case 3: _configService.Current.PairRequestSoundId = defaultSoundId; break;
case 4: _configService.Current.DownloadSoundId = defaultSoundId; break; case 4: _configService.Current.PerformanceSoundId = defaultSoundId; break;
} }
_configService.Save(); _configService.Save();
} }

View File

@@ -88,7 +88,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
ImGuiHelpers.ScaledDummy(0.5f); ImGuiHelpers.ScaledDummy(0.5f);
ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 10.0f); ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 10.0f);
ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("LightlessYellow2")); ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("PairBlue"));
if (ImGui.Button("Open Lightfinder", new Vector2(200 * ImGuiHelpers.GlobalScale, 0))) if (ImGui.Button("Open Lightfinder", new Vector2(200 * ImGuiHelpers.GlobalScale, 0)))
{ {

View File

@@ -196,82 +196,6 @@ public class TopTabMenu
if (TabSelection != SelectedTab.None) ImGuiHelpers.ScaledDummy(3f); if (TabSelection != SelectedTab.None) ImGuiHelpers.ScaledDummy(3f);
#if DEBUG
if (ImGui.Button("Test Pair Request"))
{
_lightlessNotificationService.ShowPairRequestNotification(
"Debug User",
"debug-user-id",
onAccept: () =>
{
_lightlessMediator.Publish(new NotificationMessage(
"Pair Accepted",
"Debug pair request was accepted!",
NotificationType.Info,
TimeSpan.FromSeconds(3)));
},
onDecline: () =>
{
_lightlessMediator.Publish(new NotificationMessage(
"Pair Declined",
"Debug pair request was declined.",
NotificationType.Warning,
TimeSpan.FromSeconds(3)));
}
);
}
ImGui.SameLine();
if (ImGui.Button("Test Info"))
{
_lightlessMediator.Publish(new NotificationMessage(
"Information",
"This is a test ifno notification with some longer text to see how it wraps. This is a test ifno notification with some longer text to see how it wraps. This is a test ifno notification with some longer text to see how it wraps. This is a test ifno notification with some longer text to see how it wraps.",
NotificationType.Info,
TimeSpan.FromSeconds(5)));
}
ImGui.SameLine();
if (ImGui.Button("Test Warning"))
{
_lightlessMediator.Publish(new NotificationMessage(
"Warning",
"This is a test warning notification.",
NotificationType.Warning,
TimeSpan.FromSeconds(7)));
}
ImGui.SameLine();
if (ImGui.Button("Test Error"))
{
_lightlessMediator.Publish(new NotificationMessage(
"Error",
"This is a test error notification erp police",
NotificationType.Error,
TimeSpan.FromSeconds(10)));
}
if (ImGui.Button("Test Download Progress"))
{
var downloadStatus = new List<(string playerName, float progress, string status)>
{
("Mauwmauw Nekochan", 0.85f, "downloading"),
("Raelynn Kitsune", 0.34f, "downloading"),
("Jaina Elraeth", 0.67f, "downloading"),
("Vaelstra Bloodthorn", 0.19f, "downloading"),
("Lydia Hera Moondrop", 0.86f, "downloading"),
("C'liina Star", 1.0f, "completed")
};
_lightlessNotificationService.ShowPairDownloadNotification(downloadStatus);
}
ImGui.SameLine();
if (ImGui.Button("Dismiss Download"))
{
_lightlessNotificationService.DismissPairDownloadNotification();
}
#endif
DrawIncomingPairRequests(availableWidth); DrawIncomingPairRequests(availableWidth);
ImGui.Separator(); ImGui.Separator();

View File

@@ -11,21 +11,17 @@ namespace LightlessSync.UI
{ "LightlessPurple", "#ad8af5" }, { "LightlessPurple", "#ad8af5" },
{ "LightlessPurpleActive", "#be9eff" }, { "LightlessPurpleActive", "#be9eff" },
{ "LightlessPurpleDefault", "#9375d1" }, { "LightlessPurpleDefault", "#9375d1" },
{ "ButtonDefault", "#323232" }, { "ButtonDefault", "#323232" },
{ "FullBlack", "#000000" }, { "FullBlack", "#000000" },
{ "LightlessBlue", "#a6c2ff" }, { "LightlessBlue", "#a6c2ff" },
{ "LightlessYellow", "#ffe97a" }, { "LightlessYellow", "#ffe97a" },
{ "LightlessYellow2", "#cfbd63" },
{ "LightlessGreen", "#7cd68a" }, { "LightlessGreen", "#7cd68a" },
{ "LightlessOrange", "#ffb366" },
{ "PairBlue", "#88a2db" }, { "PairBlue", "#88a2db" },
{ "DimRed", "#d44444" }, { "DimRed", "#d44444" },
{ "LightlessAdminText", "#ffd663" }, { "LightlessAdminText", "#ffd663" },
{ "LightlessAdminGlow", "#b09343" }, { "LightlessAdminGlow", "#b09343" },
{ "LightlessModeratorText", "#94ffda" }, { "LightlessModeratorText", "#94ffda" },
{ "LightlessModeratorGlow", "#599c84" },
{ "Lightfinder", "#ad8af5" }, { "Lightfinder", "#ad8af5" },
{ "LightfinderEdge", "#000000" }, { "LightfinderEdge", "#000000" },

View File

@@ -110,14 +110,7 @@ public partial class ApiController
if (dto == null) if (dto == null)
return Task.CompletedTask; return Task.CompletedTask;
var request = _pairRequestService.RegisterIncomingRequest(dto.myHashedCid, dto.message ?? string.Empty); _pairRequestService.RegisterIncomingRequest(dto.myHashedCid, dto.message ?? string.Empty);
var senderName = string.IsNullOrEmpty(request.DisplayName) ? "Unknown User" : request.DisplayName;
_lightlessNotificationService.ShowPairRequestNotification(
senderName,
request.HashedCid,
onAccept: () => _pairRequestService.AcceptPairRequest(request.HashedCid, senderName),
onDecline: () => _pairRequestService.DeclinePairRequest(request.HashedCid));
return Task.CompletedTask; return Task.CompletedTask;
} }