Merge pull request 'pair-notifs-ui' (#57) from pair-notifs-ui into 1.12.2

Reviewed-on: #57
Reviewed-by: defnotken <defnotken@noreply.git.lightless-sync.org>
This commit was merged in pull request #57.
This commit is contained in:
2025-10-11 22:19:21 +02:00
16 changed files with 2735 additions and 431 deletions

View File

@@ -63,6 +63,7 @@ public class LightlessConfig : ILightlessConfiguration
public bool ShowOnlineNotificationsOnlyForNamedPairs { get; set; } = false;
public bool ShowTransferBars { get; set; } = true;
public bool ShowTransferWindow { get; set; } = false;
public bool UseNotificationsForDownloads { get; set; } = true;
public bool ShowUploading { get; set; } = true;
public bool ShowUploadingBigText { get; set; } = true;
public bool ShowVisibleUsersSeparately { get; set; } = true;
@@ -76,6 +77,49 @@ public class LightlessConfig : ILightlessConfiguration
public bool AutoPopulateEmptyNotesFromCharaName { get; set; } = false;
public int Version { get; set; } = 1;
public NotificationLocation WarningNotification { get; set; } = NotificationLocation.Both;
// Lightless Notification Configuration
public bool UseLightlessNotifications { get; set; } = true;
public bool ShowNotificationProgress { get; set; } = true;
public NotificationLocation LightlessInfoNotification { get; set; } = NotificationLocation.LightlessUi;
public NotificationLocation LightlessWarningNotification { get; set; } = NotificationLocation.LightlessUi;
public NotificationLocation LightlessErrorNotification { get; set; } = NotificationLocation.ChatAndLightlessUi;
public NotificationLocation LightlessPairRequestNotification { get; set; } = NotificationLocation.LightlessUi;
public NotificationLocation LightlessDownloadNotification { get; set; } = NotificationLocation.TextOverlay;
// Basic Settings
public float NotificationOpacity { get; set; } = 0.95f;
public int MaxSimultaneousNotifications { get; set; } = 5;
public bool AutoDismissOnAction { get; set; } = true;
public bool DismissNotificationOnClick { get; set; } = false;
public bool ShowNotificationTimestamp { get; set; } = false;
// Position & Layout
public int NotificationOffsetY { get; set; } = 50;
public int NotificationOffsetX { get; set; } = 0;
public float NotificationWidth { get; set; } = 350f;
public float NotificationSpacing { get; set; } = 8f;
// Animation & Effects
public float NotificationAnimationSpeed { get; set; } = 10f;
public float NotificationAccentBarWidth { get; set; } = 3f;
// Duration per Type
public int InfoNotificationDurationSeconds { get; set; } = 10;
public int WarningNotificationDurationSeconds { get; set; } = 15;
public int ErrorNotificationDurationSeconds { get; set; } = 20;
public int PairRequestDurationSeconds { get; set; } = 180;
public int DownloadNotificationDurationSeconds { get; set; } = 300;
public uint CustomInfoSoundId { get; set; } = 2; // Se2
public uint CustomWarningSoundId { get; set; } = 16; // Se15
public uint CustomErrorSoundId { get; set; } = 16; // Se15
public uint PairRequestSoundId { get; set; } = 5; // Se5
public uint DownloadSoundId { get; set; } = 15; // Se14
public bool DisableInfoSound { get; set; } = false;
public bool DisableWarningSound { get; set; } = false;
public bool DisableErrorSound { get; set; } = false;
public bool DisablePairRequestSound { get; set; } = false;
public bool DisableDownloadSound { get; set; } = true; // Disabled by default (annoying)
public bool UseFocusTarget { get; set; } = false;
public bool overrideFriendColor { get; set; } = false;
public bool overridePartyColor { get; set; } = false;

View File

@@ -1,16 +1,21 @@
namespace LightlessSync.LightlessConfiguration.Models;
namespace LightlessSync.LightlessConfiguration.Models;
public enum NotificationLocation
{
Nowhere,
Chat,
Toast,
Both
Both,
LightlessUi,
ChatAndLightlessUi,
TextOverlay,
}
public enum NotificationType
{
Info,
Warning,
Error
Error,
PairRequest,
Download
}

View File

@@ -182,9 +182,13 @@ public sealed class Plugin : IDalamudPlugin
s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<IpcCallerPenumbra>(), s.GetRequiredService<IpcCallerGlamourer>(),
s.GetRequiredService<IpcCallerCustomize>(), s.GetRequiredService<IpcCallerHeels>(), s.GetRequiredService<IpcCallerHonorific>(),
s.GetRequiredService<IpcCallerMoodles>(), s.GetRequiredService<IpcCallerPetNames>(), s.GetRequiredService<IpcCallerBrio>()));
collection.AddSingleton((s) => new NotificationService(s.GetRequiredService<ILogger<NotificationService>>(),
s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<DalamudUtilService>(),
notificationManager, chatGui, s.GetRequiredService<LightlessConfigService>()));
collection.AddSingleton((s) => new NotificationService(
s.GetRequiredService<ILogger<NotificationService>>(),
s.GetRequiredService<LightlessConfigService>(),
s.GetRequiredService<DalamudUtilService>(),
notificationManager,
chatGui,
s.GetRequiredService<LightlessMediator>()));
collection.AddSingleton((s) =>
{
var httpClient = new HttpClient();
@@ -248,6 +252,12 @@ public sealed class Plugin : IDalamudPlugin
collection.AddScoped<WindowMediatorSubscriberBase, BroadcastUI>((s) => new BroadcastUI(s.GetRequiredService<ILogger<BroadcastUI>>(), s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<PerformanceCollectorService>(), s.GetRequiredService<BroadcastService>(), s.GetRequiredService<LightlessConfigService>(), s.GetRequiredService<UiSharedService>(), s.GetRequiredService<ApiController>(), s.GetRequiredService<BroadcastScannerService>()));
collection.AddScoped<WindowMediatorSubscriberBase, SyncshellFinderUI>((s) => new SyncshellFinderUI(s.GetRequiredService<ILogger<SyncshellFinderUI>>(), s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<PerformanceCollectorService>(), s.GetRequiredService<BroadcastService>(), s.GetRequiredService<UiSharedService>(), s.GetRequiredService<ApiController>(), s.GetRequiredService<BroadcastScannerService>(), s.GetRequiredService<PairManager>(), s.GetRequiredService<DalamudUtilService>()));
collection.AddScoped<IPopupHandler, BanUserPopupHandler>();
collection.AddScoped<WindowMediatorSubscriberBase, LightlessNotificationUI>((s) =>
new LightlessNotificationUI(
s.GetRequiredService<ILogger<LightlessNotificationUI>>(),
s.GetRequiredService<LightlessMediator>(),
s.GetRequiredService<PerformanceCollectorService>(),
s.GetRequiredService<LightlessConfigService>()));
collection.AddScoped<IPopupHandler, CensusPopupHandler>();
collection.AddScoped<CacheCreationService>();
collection.AddScoped<PlayerDataFactory>();
@@ -255,7 +265,9 @@ public sealed class Plugin : IDalamudPlugin
collection.AddScoped((s) => new UiService(s.GetRequiredService<ILogger<UiService>>(), pluginInterface.UiBuilder, s.GetRequiredService<LightlessConfigService>(),
s.GetRequiredService<WindowSystem>(), s.GetServices<WindowMediatorSubscriberBase>(),
s.GetRequiredService<UiFactory>(),
s.GetRequiredService<FileDialogManager>(), s.GetRequiredService<LightlessMediator>()));
s.GetRequiredService<FileDialogManager>(),
s.GetRequiredService<LightlessMediator>(),
s.GetRequiredService<NotificationService>()));
collection.AddScoped((s) => new CommandManagerService(commandManager, s.GetRequiredService<PerformanceCollectorService>(),
s.GetRequiredService<ServerConfigurationManager>(), s.GetRequiredService<CacheMonitor>(), s.GetRequiredService<ApiController>(),
s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<LightlessConfigService>()));

View File

@@ -1,4 +1,4 @@
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Game.ClientState.Objects.Types;
using LightlessSync.API.Data;
using LightlessSync.API.Dto;
using LightlessSync.API.Dto.CharaData;
@@ -54,6 +54,8 @@ public record NotificationMessage
public record CreateCacheForObjectMessage(GameObjectHandler ObjectToCreateFor) : SameThreadMessage;
public record ClearCacheForObjectMessage(GameObjectHandler ObjectToCreateFor) : SameThreadMessage;
public record CharacterDataCreatedMessage(CharacterData CharacterData) : SameThreadMessage;
public record LightlessNotificationMessage(LightlessSync.UI.Models.LightlessNotification Notification) : MessageBase;
public record LightlessNotificationDismissMessage(string NotificationId) : MessageBase;
public record CharacterDataAnalyzedMessage : MessageBase;
public record PenumbraStartRedrawMessage(IntPtr Address) : MessageBase;
public record PenumbraEndRedrawMessage(IntPtr Address) : MessageBase;

View File

@@ -1,42 +1,492 @@
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Interface;
using Dalamud.Interface.ImGuiNotification;
using Dalamud.Plugin.Services;
using LightlessSync.LightlessConfiguration;
using LightlessSync.LightlessConfiguration.Models;
using LightlessSync.Services.Mediator;
using LightlessSync.UI;
using LightlessSync.UI.Models;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using FFXIVClientStructs.FFXIV.Client.UI;
using NotificationType = LightlessSync.LightlessConfiguration.Models.NotificationType;
namespace LightlessSync.Services;
public class NotificationService : DisposableMediatorSubscriberBase, IHostedService
{
private readonly ILogger<NotificationService> _logger;
private readonly LightlessConfigService _configService;
private readonly DalamudUtilService _dalamudUtilService;
private readonly INotificationManager _notificationManager;
private readonly IChatGui _chatGui;
private readonly LightlessConfigService _configurationService;
public NotificationService(ILogger<NotificationService> logger, LightlessMediator mediator,
public NotificationService(
ILogger<NotificationService> logger,
LightlessConfigService configService,
DalamudUtilService dalamudUtilService,
INotificationManager notificationManager,
IChatGui chatGui, LightlessConfigService configurationService) : base(logger, mediator)
IChatGui chatGui,
LightlessMediator mediator) : base(logger, mediator)
{
_logger = logger;
_configService = configService;
_dalamudUtilService = dalamudUtilService;
_notificationManager = notificationManager;
_chatGui = chatGui;
_configurationService = configurationService;
}
public Task StartAsync(CancellationToken cancellationToken)
{
Mediator.Subscribe<NotificationMessage>(this, ShowNotification);
Mediator.Subscribe<NotificationMessage>(this, HandleNotificationMessage);
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public void ShowNotification(string title, string message, NotificationType type = NotificationType.Info,
TimeSpan? duration = null, List<LightlessNotificationAction>? actions = null, uint? soundEffectId = null)
{
return Task.CompletedTask;
var notification = CreateNotification(title, message, type, duration, actions, soundEffectId);
if (_configService.Current.AutoDismissOnAction && notification.Actions.Any())
{
WrapActionsWithAutoDismiss(notification);
}
if (notification.SoundEffectId.HasValue)
{
PlayNotificationSound(notification.SoundEffectId.Value);
}
Mediator.Publish(new LightlessNotificationMessage(notification));
}
private LightlessNotification CreateNotification(string title, string message, NotificationType type,
TimeSpan? duration, List<LightlessNotificationAction>? actions, uint? soundEffectId)
{
return new LightlessNotification
{
Title = title,
Message = message,
Type = type,
Duration = duration ?? GetDefaultDurationForType(type),
Actions = actions ?? new List<LightlessNotificationAction>(),
SoundEffectId = GetSoundEffectId(type, soundEffectId),
ShowProgress = _configService.Current.ShowNotificationProgress,
CreatedAt = DateTime.UtcNow
};
}
private void WrapActionsWithAutoDismiss(LightlessNotification notification)
{
foreach (var action in notification.Actions)
{
var originalOnClick = action.OnClick;
action.OnClick = (n) =>
{
originalOnClick(n);
if (_configService.Current.AutoDismissOnAction)
{
DismissNotification(n);
}
};
}
}
private void DismissNotification(LightlessNotification notification)
{
notification.IsDismissed = true;
notification.IsAnimatingOut = true;
}
public void ShowPairRequestNotification(string senderName, string senderId, Action onAccept, Action onDecline)
{
var notification = new LightlessNotification
{
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));
}
private uint? GetPairRequestSoundId() =>
!_configService.Current.DisablePairRequestSound ? _configService.Current.PairRequestSoundId : null;
private List<LightlessNotificationAction> CreatePairRequestActions(Action onAccept, Action onDecline)
{
return new List<LightlessNotificationAction>
{
new()
{
Id = "accept",
Label = "Accept",
Icon = FontAwesomeIcon.Check,
Color = UIColors.Get("LightlessGreen"),
IsPrimary = true,
OnClick = (n) =>
{
_logger.LogInformation("Pair request accepted");
onAccept();
DismissNotification(n);
}
},
new()
{
Id = "decline",
Label = "Decline",
Icon = FontAwesomeIcon.Times,
Color = UIColors.Get("DimRed"),
IsDestructive = true,
OnClick = (n) =>
{
_logger.LogInformation("Pair request declined");
onDecline();
DismissNotification(n);
}
}
};
}
public void ShowDownloadCompleteNotification(string fileName, int fileCount, Action? onOpenFolder = null)
{
var notification = new LightlessNotification
{
Title = "Download Complete",
Message = FormatDownloadCompleteMessage(fileName, fileCount),
Type = NotificationType.Info,
Duration = TimeSpan.FromSeconds(8),
Actions = CreateDownloadCompleteActions(onOpenFolder),
SoundEffectId = NotificationSounds.DownloadComplete
};
if (notification.SoundEffectId.HasValue)
{
PlayNotificationSound(notification.SoundEffectId.Value);
}
Mediator.Publish(new LightlessNotificationMessage(notification));
}
private string FormatDownloadCompleteMessage(string fileName, int fileCount) =>
fileCount > 1
? $"Downloaded {fileCount} files successfully."
: $"Downloaded {fileName} successfully.";
private List<LightlessNotificationAction> CreateDownloadCompleteActions(Action? onOpenFolder)
{
var actions = new List<LightlessNotificationAction>();
if (onOpenFolder != null)
{
actions.Add(new LightlessNotificationAction
{
Id = "open_folder",
Label = "Open Folder",
Icon = FontAwesomeIcon.FolderOpen,
Color = UIColors.Get("LightlessBlue"),
OnClick = (n) =>
{
onOpenFolder();
DismissNotification(n);
}
});
}
return actions;
}
public void ShowErrorNotification(string title, string message, Exception? exception = null, Action? onRetry = null, Action? onViewLog = null)
{
var notification = new LightlessNotification
{
Title = title,
Message = FormatErrorMessage(message, exception),
Type = NotificationType.Error,
Duration = TimeSpan.FromSeconds(15),
Actions = CreateErrorActions(onRetry, onViewLog),
SoundEffectId = NotificationSounds.Error
};
if (notification.SoundEffectId.HasValue)
{
PlayNotificationSound(notification.SoundEffectId.Value);
}
Mediator.Publish(new LightlessNotificationMessage(notification));
}
private string FormatErrorMessage(string message, Exception? exception) =>
exception != null ? $"{message}\n\nError: {exception.Message}" : message;
private List<LightlessNotificationAction> CreateErrorActions(Action? onRetry, Action? onViewLog)
{
var actions = new List<LightlessNotificationAction>();
if (onRetry != null)
{
actions.Add(new LightlessNotificationAction
{
Id = "retry",
Label = "Retry",
Icon = FontAwesomeIcon.Redo,
Color = UIColors.Get("LightlessBlue"),
OnClick = (n) =>
{
onRetry();
DismissNotification(n);
}
});
}
if (onViewLog != null)
{
actions.Add(new LightlessNotificationAction
{
Id = "view_log",
Label = "View Log",
Icon = FontAwesomeIcon.FileAlt,
Color = UIColors.Get("LightlessYellow"),
OnClick = (n) => onViewLog()
});
}
return actions;
}
public void ShowPairDownloadNotification(List<(string playerName, float progress, string status)> downloadStatus, int queueWaiting = 0)
{
var userDownloads = downloadStatus.Where(x => x.playerName != "Pair Queue").ToList();
var totalProgress = userDownloads.Count > 0 ? userDownloads.Average(x => x.progress) : 0f;
var message = BuildPairDownloadMessage(userDownloads, queueWaiting);
var notification = new LightlessNotification
{
Id = "pair_download_progress",
Title = "Downloading Pair Data",
Message = message,
Type = NotificationType.Download,
Duration = TimeSpan.FromSeconds(_configService.Current.DownloadNotificationDurationSeconds),
ShowProgress = true,
Progress = totalProgress
};
Mediator.Publish(new LightlessNotificationMessage(notification));
if (AreAllDownloadsCompleted(userDownloads))
{
DismissPairDownloadNotification();
}
}
private string BuildPairDownloadMessage(List<(string playerName, float progress, string status)> userDownloads, int queueWaiting)
{
var messageParts = new List<string>();
if (queueWaiting > 0)
{
messageParts.Add($"Queue: {queueWaiting} waiting");
}
if (userDownloads.Count > 0)
{
var completedCount = userDownloads.Count(x => x.progress >= 1.0f);
messageParts.Add($"Progress: {completedCount}/{userDownloads.Count} completed");
}
var activeDownloadLines = BuildActiveDownloadLines(userDownloads);
if (!string.IsNullOrEmpty(activeDownloadLines))
{
messageParts.Add(activeDownloadLines);
}
return string.Join("\n", messageParts);
}
private string BuildActiveDownloadLines(List<(string playerName, float progress, string status)> userDownloads)
{
var activeDownloads = userDownloads
.Where(x => x.progress < 1.0f)
.Take(_configService.Current.MaxConcurrentPairApplications);
if (!activeDownloads.Any()) return string.Empty;
return string.Join("\n", activeDownloads.Select(x => $"• {x.playerName}: {FormatDownloadStatus(x)}"));
}
private string FormatDownloadStatus((string playerName, float progress, string status) download) => download.status switch
{
"downloading" => $"{download.progress:P0}",
"decompressing" => "decompressing",
"queued" => "queued",
"waiting" => "waiting for slot",
_ => download.status
};
private bool AreAllDownloadsCompleted(List<(string playerName, float progress, string status)> userDownloads) =>
userDownloads.Any() && userDownloads.All(x => x.progress >= 1.0f);
public void DismissPairDownloadNotification() =>
Mediator.Publish(new LightlessNotificationDismissMessage("pair_download_progress"));
private TimeSpan GetDefaultDurationForType(NotificationType type) => type switch
{
NotificationType.Info => TimeSpan.FromSeconds(_configService.Current.InfoNotificationDurationSeconds),
NotificationType.Warning => TimeSpan.FromSeconds(_configService.Current.WarningNotificationDurationSeconds),
NotificationType.Error => TimeSpan.FromSeconds(_configService.Current.ErrorNotificationDurationSeconds),
NotificationType.PairRequest => TimeSpan.FromSeconds(_configService.Current.PairRequestDurationSeconds),
NotificationType.Download => TimeSpan.FromSeconds(_configService.Current.DownloadNotificationDurationSeconds),
_ => TimeSpan.FromSeconds(10)
};
private uint? GetSoundEffectId(NotificationType type, uint? overrideSoundId)
{
if (overrideSoundId.HasValue) return overrideSoundId;
if (IsSoundDisabledForType(type)) return null;
return GetConfiguredSoundForType(type);
}
private bool IsSoundDisabledForType(NotificationType type) => type switch
{
NotificationType.Info => _configService.Current.DisableInfoSound,
NotificationType.Warning => _configService.Current.DisableWarningSound,
NotificationType.Error => _configService.Current.DisableErrorSound,
NotificationType.Download => _configService.Current.DisableDownloadSound,
_ => false
};
private uint GetConfiguredSoundForType(NotificationType type) => type switch
{
NotificationType.Info => _configService.Current.CustomInfoSoundId,
NotificationType.Warning => _configService.Current.CustomWarningSoundId,
NotificationType.Error => _configService.Current.CustomErrorSoundId,
NotificationType.Download => _configService.Current.DownloadSoundId,
_ => NotificationSounds.GetDefaultSound(type)
};
private void PlayNotificationSound(uint soundEffectId)
{
try
{
UIGlobals.PlayChatSoundEffect(soundEffectId);
_logger.LogDebug("Played notification sound effect {SoundId} via ChatGui", soundEffectId);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to play notification sound effect {SoundId}", soundEffectId);
}
}
private void HandleNotificationMessage(NotificationMessage msg)
{
_logger.LogInformation("{msg}", msg.ToString());
if (!_dalamudUtilService.IsLoggedIn) return;
var location = GetNotificationLocation(msg.Type);
ShowNotificationLocationBased(msg, location);
}
private NotificationLocation GetNotificationLocation(NotificationType type) =>
_configService.Current.UseLightlessNotifications
? GetLightlessNotificationLocation(type)
: GetClassicNotificationLocation(type);
private NotificationLocation GetLightlessNotificationLocation(NotificationType type) => type switch
{
NotificationType.Info => _configService.Current.LightlessInfoNotification,
NotificationType.Warning => _configService.Current.LightlessWarningNotification,
NotificationType.Error => _configService.Current.LightlessErrorNotification,
NotificationType.PairRequest => _configService.Current.LightlessPairRequestNotification,
NotificationType.Download => _configService.Current.LightlessDownloadNotification,
_ => NotificationLocation.LightlessUi
};
private NotificationLocation GetClassicNotificationLocation(NotificationType type) => type switch
{
NotificationType.Info => _configService.Current.InfoNotification,
NotificationType.Warning => _configService.Current.WarningNotification,
NotificationType.Error => _configService.Current.ErrorNotification,
NotificationType.PairRequest => NotificationLocation.Toast,
NotificationType.Download => NotificationLocation.Toast,
_ => NotificationLocation.Nowhere
};
private void ShowNotificationLocationBased(NotificationMessage msg, NotificationLocation location)
{
switch (location)
{
case NotificationLocation.Toast:
ShowToast(msg);
break;
case NotificationLocation.Chat:
ShowChat(msg);
break;
case NotificationLocation.Both:
ShowToast(msg);
ShowChat(msg);
break;
case NotificationLocation.LightlessUi:
ShowLightlessNotification(msg);
break;
case NotificationLocation.ChatAndLightlessUi:
ShowChat(msg);
ShowLightlessNotification(msg);
break;
case NotificationLocation.Nowhere:
break;
}
}
private void ShowLightlessNotification(NotificationMessage msg)
{
var duration = msg.TimeShownOnScreen ?? GetDefaultDurationForType(msg.Type);
ShowNotification(msg.Title ?? "Lightless Sync", msg.Message ?? string.Empty, msg.Type, duration, null, null);
}
private void ShowToast(NotificationMessage msg)
{
var dalamudType = ConvertToDalamudNotificationType(msg.Type);
_notificationManager.AddNotification(new Notification()
{
Content = msg.Message ?? string.Empty,
Title = msg.Title,
Type = dalamudType,
Minimized = false,
InitialDuration = msg.TimeShownOnScreen ?? TimeSpan.FromSeconds(3)
});
}
private Dalamud.Interface.ImGuiNotification.NotificationType ConvertToDalamudNotificationType(NotificationType type) => type switch
{
NotificationType.Error => Dalamud.Interface.ImGuiNotification.NotificationType.Error,
NotificationType.Warning => Dalamud.Interface.ImGuiNotification.NotificationType.Warning,
_ => Dalamud.Interface.ImGuiNotification.NotificationType.Info
};
private void ShowChat(NotificationMessage msg)
{
switch (msg.Type)
{
case NotificationType.Info:
PrintInfoChat(msg.Message);
break;
case NotificationType.Warning:
PrintWarnChat(msg.Message);
break;
case NotificationType.Error:
PrintErrorChat(msg.Message);
break;
}
}
private void PrintErrorChat(string? message)
@@ -57,85 +507,4 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
_chatGui.Print(se.BuiltString);
}
private void ShowChat(NotificationMessage msg)
{
switch (msg.Type)
{
case NotificationType.Info:
PrintInfoChat(msg.Message);
break;
case NotificationType.Warning:
PrintWarnChat(msg.Message);
break;
case NotificationType.Error:
PrintErrorChat(msg.Message);
break;
}
}
private void ShowNotification(NotificationMessage msg)
{
Logger.LogInformation("{msg}", msg.ToString());
if (!_dalamudUtilService.IsLoggedIn) return;
switch (msg.Type)
{
case NotificationType.Info:
ShowNotificationLocationBased(msg, _configurationService.Current.InfoNotification);
break;
case NotificationType.Warning:
ShowNotificationLocationBased(msg, _configurationService.Current.WarningNotification);
break;
case NotificationType.Error:
ShowNotificationLocationBased(msg, _configurationService.Current.ErrorNotification);
break;
}
}
private void ShowNotificationLocationBased(NotificationMessage msg, NotificationLocation location)
{
switch (location)
{
case NotificationLocation.Toast:
ShowToast(msg);
break;
case NotificationLocation.Chat:
ShowChat(msg);
break;
case NotificationLocation.Both:
ShowToast(msg);
ShowChat(msg);
break;
case NotificationLocation.Nowhere:
break;
}
}
private void ShowToast(NotificationMessage msg)
{
Dalamud.Interface.ImGuiNotification.NotificationType dalamudType = msg.Type switch
{
NotificationType.Error => Dalamud.Interface.ImGuiNotification.NotificationType.Error,
NotificationType.Warning => Dalamud.Interface.ImGuiNotification.NotificationType.Warning,
NotificationType.Info => Dalamud.Interface.ImGuiNotification.NotificationType.Info,
_ => Dalamud.Interface.ImGuiNotification.NotificationType.Info
};
_notificationManager.AddNotification(new Notification()
{
Content = msg.Message ?? string.Empty,
Title = msg.Title,
Type = dalamudType,
Minimized = false,
InitialDuration = msg.TimeShownOnScreen ?? TimeSpan.FromSeconds(3)
});
}
}

View File

@@ -1,6 +1,8 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using LightlessSync.LightlessConfiguration.Models;
using LightlessSync.PlayerData.Pairs;
using LightlessSync.Services.Mediator;
using Microsoft.Extensions.Logging;
@@ -11,16 +13,18 @@ public sealed class PairRequestService : DisposableMediatorSubscriberBase
{
private readonly DalamudUtilService _dalamudUtil;
private readonly PairManager _pairManager;
private readonly Lazy<WebAPI.ApiController> _apiController;
private readonly object _syncRoot = new();
private readonly List<PairRequestEntry> _requests = [];
private static readonly TimeSpan Expiration = TimeSpan.FromMinutes(5);
public PairRequestService(ILogger<PairRequestService> logger, LightlessMediator mediator, DalamudUtilService dalamudUtil, PairManager pairManager)
public PairRequestService(ILogger<PairRequestService> logger, LightlessMediator mediator, DalamudUtilService dalamudUtil, PairManager pairManager, Lazy<WebAPI.ApiController> apiController)
: base(logger, mediator)
{
_dalamudUtil = dalamudUtil;
_pairManager = pairManager;
_apiController = apiController;
Mediator.Subscribe<PriorityFrameworkUpdateMessage>(this, _ =>
{
@@ -183,6 +187,41 @@ public sealed class PairRequestService : DisposableMediatorSubscriberBase
return _requests.RemoveAll(r => now - r.ReceivedAt > Expiration) > 0;
}
public void AcceptPairRequest(string hashedCid)
{
_ = Task.Run(async () =>
{
try
{
await _apiController.Value.TryPairWithContentId(hashedCid).ConfigureAwait(false);
RemoveRequest(hashedCid);
var display = ResolveDisplayName(hashedCid);
var displayText = string.IsNullOrEmpty(display) ? hashedCid : display;
Mediator.Publish(new NotificationMessage(
"Pair request accepted",
$"Sent a pair request back to {displayText}.",
NotificationType.Info,
TimeSpan.FromSeconds(3)));
}
catch (Exception ex)
{
Logger.LogError(ex, "Failed to accept pair request for {HashedCid}", hashedCid);
Mediator.Publish(new NotificationMessage(
"Failed to Accept Pair Request",
ex.Message,
NotificationType.Error,
TimeSpan.FromSeconds(5)));
}
});
}
public void DeclinePairRequest(string hashedCid)
{
RemoveRequest(hashedCid);
Logger.LogDebug("Declined pair request from {HashedCid}", hashedCid);
}
private record struct PairRequestEntry(string HashedCid, string MessageTemplate, DateTime ReceivedAt);
public readonly record struct PairRequestDisplay(string HashedCid, string DisplayName, string Message, DateTime ReceivedAt);

View File

@@ -1,4 +1,4 @@
using Dalamud.Interface;
using Dalamud.Interface;
using Dalamud.Interface.ImGuiFileDialog;
using Dalamud.Interface.Windowing;
using LightlessSync.LightlessConfiguration;
@@ -22,7 +22,8 @@ public sealed class UiService : DisposableMediatorSubscriberBase
LightlessConfigService lightlessConfigService, WindowSystem windowSystem,
IEnumerable<WindowMediatorSubscriberBase> windows,
UiFactory uiFactory, FileDialogManager fileDialogManager,
LightlessMediator lightlessMediator) : base(logger, lightlessMediator)
LightlessMediator lightlessMediator,
NotificationService notificationService) : base(logger, lightlessMediator)
{
_logger = logger;
_logger.LogTrace("Creating {type}", GetType().Name);

View File

@@ -1,4 +1,4 @@
using System;
using System;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Utility;
@@ -87,7 +87,7 @@ public class CompactUi : WindowMediatorSubscriberBase
IpcManager ipcManager,
BroadcastService broadcastService,
CharacterAnalyzer characterAnalyzer,
PlayerPerformanceConfigService playerPerformanceConfig, PairRequestService pairRequestService, DalamudUtilService dalamudUtilService) : base(logger, mediator, "###LightlessSyncMainUI", performanceCollectorService)
PlayerPerformanceConfigService playerPerformanceConfig, PairRequestService pairRequestService, DalamudUtilService dalamudUtilService, NotificationService lightlessNotificationService) : base(logger, mediator, "###LightlessSyncMainUI", performanceCollectorService)
{
_uiSharedService = uiShared;
_configService = configService;
@@ -105,7 +105,7 @@ public class CompactUi : WindowMediatorSubscriberBase
_renamePairTagUi = renameTagUi;
_ipcManager = ipcManager;
_broadcastService = broadcastService;
_tabMenu = new TopTabMenu(Mediator, _apiController, _pairManager, _uiSharedService, pairRequestService, dalamudUtilService);
_tabMenu = new TopTabMenu(Mediator, _apiController, _pairManager, _uiSharedService, pairRequestService, dalamudUtilService, lightlessNotificationService);
AllowPinning = true;
AllowClickthrough = false;

View File

@@ -1,7 +1,7 @@
using System;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Colors;
using LightlessSync.LightlessConfiguration;
using LightlessSync.LightlessConfiguration.Models;
using LightlessSync.PlayerData.Handlers;
using LightlessSync.Services;
using LightlessSync.Services.Mediator;
@@ -22,9 +22,12 @@ public class DownloadUi : WindowMediatorSubscriberBase
private readonly UiSharedService _uiShared;
private readonly PairProcessingLimiter _pairProcessingLimiter;
private readonly ConcurrentDictionary<GameObjectHandler, bool> _uploadingPlayers = new();
private readonly NotificationService _notificationService;
private bool _notificationDismissed = true;
public DownloadUi(ILogger<DownloadUi> logger, DalamudUtilService dalamudUtilService, LightlessConfigService configService,
PairProcessingLimiter pairProcessingLimiter, FileUploadManager fileTransferManager, LightlessMediator mediator, UiSharedService uiShared, PerformanceCollectorService performanceCollectorService)
PairProcessingLimiter pairProcessingLimiter, FileUploadManager fileTransferManager, LightlessMediator mediator, UiSharedService uiShared,
PerformanceCollectorService performanceCollectorService, NotificationService notificationService)
: base(logger, mediator, "Lightless Sync Downloads", performanceCollectorService)
{
_dalamudUtilService = dalamudUtilService;
@@ -32,6 +35,7 @@ public class DownloadUi : WindowMediatorSubscriberBase
_pairProcessingLimiter = pairProcessingLimiter;
_fileTransferManager = fileTransferManager;
_uiShared = uiShared;
_notificationService = notificationService;
SizeConstraints = new WindowSizeConstraints()
{
@@ -56,7 +60,14 @@ public class DownloadUi : WindowMediatorSubscriberBase
IsOpen = true;
Mediator.Subscribe<DownloadStartedMessage>(this, (msg) => _currentDownloads[msg.DownloadId] = msg.DownloadStatus);
Mediator.Subscribe<DownloadFinishedMessage>(this, (msg) => _currentDownloads.TryRemove(msg.DownloadId, out _));
Mediator.Subscribe<DownloadFinishedMessage>(this, (msg) =>
{
_currentDownloads.TryRemove(msg.DownloadId, out _);
if (!_currentDownloads.Any())
{
_notificationService.DismissPairDownloadNotification();
}
});
Mediator.Subscribe<GposeStartMessage>(this, (_) => IsOpen = false);
Mediator.Subscribe<GposeEndMessage>(this, (_) => IsOpen = true);
Mediator.Subscribe<PlayerUploadingMessage>(this, (msg) =>
@@ -77,19 +88,7 @@ public class DownloadUi : WindowMediatorSubscriberBase
if (_configService.Current.ShowTransferWindow)
{
var limiterSnapshot = _pairProcessingLimiter.GetSnapshot();
if (limiterSnapshot.IsEnabled)
{
var queueColor = limiterSnapshot.Waiting > 0 ? ImGuiColors.DalamudYellow : ImGuiColors.DalamudGrey;
var queueText = $"Pair queue {limiterSnapshot.InFlight}/{limiterSnapshot.Limit}";
queueText += limiterSnapshot.Waiting > 0 ? $" ({limiterSnapshot.Waiting} waiting, {limiterSnapshot.Remaining} free)" : $" ({limiterSnapshot.Remaining} free)";
UiSharedService.DrawOutlinedFont(queueText, queueColor, new Vector4(0, 0, 0, 255), 1);
ImGui.NewLine();
}
else
{
UiSharedService.DrawOutlinedFont("Pair apply limiter disabled", ImGuiColors.DalamudGrey, new Vector4(0, 0, 0, 255), 1);
ImGui.NewLine();
}
try
{
if (_fileTransferManager.IsUploading)
@@ -122,28 +121,64 @@ public class DownloadUi : WindowMediatorSubscriberBase
try
{
foreach (var item in _currentDownloads.ToList())
// Check if download notifications are enabled (not set to TextOverlay)
var useNotifications = _configService.Current.UseLightlessNotifications
? _configService.Current.LightlessDownloadNotification != NotificationLocation.TextOverlay
: _configService.Current.UseNotificationsForDownloads;
if (useNotifications)
{
var dlSlot = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.WaitingForSlot);
var dlQueue = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.WaitingForQueue);
var dlProg = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.Downloading);
var dlDecomp = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.Decompressing);
var totalFiles = item.Value.Sum(c => c.Value.TotalFiles);
var transferredFiles = item.Value.Sum(c => c.Value.TransferredFiles);
var totalBytes = item.Value.Sum(c => c.Value.TotalBytes);
var transferredBytes = item.Value.Sum(c => c.Value.TransferredBytes);
// Use notification system
if (_currentDownloads.Any())
{
UpdateDownloadNotification(limiterSnapshot);
_notificationDismissed = false;
}
else if (!_notificationDismissed)
{
_notificationService.DismissPairDownloadNotification();
_notificationDismissed = true;
}
}
else
{
if (limiterSnapshot.IsEnabled)
{
var queueColor = limiterSnapshot.Waiting > 0 ? ImGuiColors.DalamudYellow : ImGuiColors.DalamudGrey;
var queueText = $"Pair queue {limiterSnapshot.InFlight}/{limiterSnapshot.Limit}";
queueText += limiterSnapshot.Waiting > 0 ? $" ({limiterSnapshot.Waiting} waiting, {limiterSnapshot.Remaining} free)" : $" ({limiterSnapshot.Remaining} free)";
UiSharedService.DrawOutlinedFont(queueText, queueColor, new Vector4(0, 0, 0, 255), 1);
ImGui.NewLine();
}
else
{
UiSharedService.DrawOutlinedFont("Pair apply limiter disabled", ImGuiColors.DalamudGrey, new Vector4(0, 0, 0, 255), 1);
ImGui.NewLine();
}
foreach (var item in _currentDownloads.ToList())
{
var dlSlot = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.WaitingForSlot);
var dlQueue = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.WaitingForQueue);
var dlProg = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.Downloading);
var dlDecomp = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.Decompressing);
var totalFiles = item.Value.Sum(c => c.Value.TotalFiles);
var transferredFiles = item.Value.Sum(c => c.Value.TransferredFiles);
var totalBytes = item.Value.Sum(c => c.Value.TotalBytes);
var transferredBytes = item.Value.Sum(c => c.Value.TransferredBytes);
UiSharedService.DrawOutlinedFont($"▼", ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1);
ImGui.SameLine();
var xDistance = ImGui.GetCursorPosX();
UiSharedService.DrawOutlinedFont(
$"{item.Key.Name} [W:{dlSlot}/Q:{dlQueue}/P:{dlProg}/D:{dlDecomp}]",
ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1);
ImGui.NewLine();
ImGui.SameLine(xDistance);
UiSharedService.DrawOutlinedFont(
$"{transferredFiles}/{totalFiles} ({UiSharedService.ByteToString(transferredBytes, addSuffix: false)}/{UiSharedService.ByteToString(totalBytes)})",
ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1);
UiSharedService.DrawOutlinedFont($"▼", ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1);
ImGui.SameLine();
var xDistance = ImGui.GetCursorPosX();
UiSharedService.DrawOutlinedFont(
$"{item.Key.Name} [W:{dlSlot}/Q:{dlQueue}/P:{dlProg}/D:{dlDecomp}]",
ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1);
ImGui.NewLine();
ImGui.SameLine(xDistance);
UiSharedService.DrawOutlinedFont(
$"{transferredFiles}/{totalFiles} ({UiSharedService.ByteToString(transferredBytes, addSuffix: false)}/{UiSharedService.ByteToString(totalBytes)})",
ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1);
}
}
}
catch
@@ -262,4 +297,40 @@ public class DownloadUi : WindowMediatorSubscriberBase
MaximumSize = new Vector2(300, maxHeight),
};
}
private void UpdateDownloadNotification(PairProcessingLimiterSnapshot limiterSnapshot)
{
var downloadStatus = new List<(string playerName, float progress, string status)>();
foreach (var item in _currentDownloads.ToList())
{
var dlSlot = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.WaitingForSlot);
var dlQueue = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.WaitingForQueue);
var dlProg = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.Downloading);
var dlDecomp = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.Decompressing);
var totalFiles = item.Value.Sum(c => c.Value.TotalFiles);
var transferredFiles = item.Value.Sum(c => c.Value.TransferredFiles);
var totalBytes = item.Value.Sum(c => c.Value.TotalBytes);
var transferredBytes = item.Value.Sum(c => c.Value.TransferredBytes);
var progress = totalBytes > 0 ? (float)transferredBytes / totalBytes : 0f;
string status;
if (dlDecomp > 0) status = "decompressing";
else if (dlProg > 0) status = "downloading";
else if (dlQueue > 0) status = "queued";
else if (dlSlot > 0) status = "waiting";
else status = "completed";
downloadStatus.Add((item.Key.Name, progress, status));
}
// Pass queue waiting count separately, show notification if there are downloads or queue items
var queueWaiting = limiterSnapshot.IsEnabled ? limiterSnapshot.Waiting : 0;
if (downloadStatus.Any() || queueWaiting > 0)
{
_notificationService.ShowPairDownloadNotification(downloadStatus, queueWaiting);
}
}
}

View File

@@ -0,0 +1,598 @@
using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Windowing;
using LightlessSync.LightlessConfiguration;
using LightlessSync.LightlessConfiguration.Models;
using LightlessSync.Services;
using LightlessSync.Services.Mediator;
using LightlessSync.UI.Models;
using Microsoft.Extensions.Logging;
using System.Numerics;
using Dalamud.Bindings.ImGui;
namespace LightlessSync.UI;
public class LightlessNotificationUI : WindowMediatorSubscriberBase
{
private const float NotificationMinHeight = 60f;
private const float NotificationMaxHeight = 250f;
private const float WindowPaddingOffset = 6f;
private const float SlideAnimationDistance = 100f;
private const float OutAnimationSpeedMultiplier = 0.7f;
private readonly List<LightlessNotification> _notifications = new();
private readonly object _notificationLock = new();
private readonly LightlessConfigService _configService;
public LightlessNotificationUI(ILogger<LightlessNotificationUI> logger, LightlessMediator mediator, PerformanceCollectorService performanceCollector, LightlessConfigService configService)
: base(logger, mediator, "Lightless Notifications##LightlessNotifications", performanceCollector)
{
_configService = configService;
Flags = ImGuiWindowFlags.NoDecoration |
ImGuiWindowFlags.NoMove |
ImGuiWindowFlags.NoResize |
ImGuiWindowFlags.NoSavedSettings |
ImGuiWindowFlags.NoFocusOnAppearing |
ImGuiWindowFlags.NoNav |
ImGuiWindowFlags.NoBackground |
ImGuiWindowFlags.NoCollapse |
ImGuiWindowFlags.AlwaysAutoResize;
PositionCondition = ImGuiCond.Always;
Size = new Vector2(_configService.Current.NotificationWidth, 100);
SizeCondition = ImGuiCond.FirstUseEver;
IsOpen = false;
RespectCloseHotkey = false;
DisableWindowSounds = true;
Mediator.Subscribe<LightlessNotificationMessage>(this, HandleNotificationMessage);
Mediator.Subscribe<LightlessNotificationDismissMessage>(this, HandleNotificationDismissMessage);
}
private void HandleNotificationMessage(LightlessNotificationMessage message) =>
AddNotification(message.Notification);
private void HandleNotificationDismissMessage(LightlessNotificationDismissMessage message) =>
RemoveNotification(message.NotificationId);
public void AddNotification(LightlessNotification notification)
{
lock (_notificationLock)
{
var existingNotification = _notifications.FirstOrDefault(n => n.Id == notification.Id);
if (existingNotification != null)
{
UpdateExistingNotification(existingNotification, notification);
}
else
{
_notifications.Add(notification);
_logger.LogDebug("Added new notification: {Title}", notification.Title);
}
if (!IsOpen) IsOpen = true;
}
}
private void UpdateExistingNotification(LightlessNotification existing, LightlessNotification updated)
{
existing.Message = updated.Message;
existing.Progress = updated.Progress;
existing.ShowProgress = updated.ShowProgress;
existing.Title = updated.Title;
_logger.LogDebug("Updated existing notification: {Title}", updated.Title);
}
public void RemoveNotification(string id)
{
lock (_notificationLock)
{
var notification = _notifications.FirstOrDefault(n => n.Id == id);
if (notification != null)
{
StartOutAnimation(notification);
}
}
}
private void StartOutAnimation(LightlessNotification notification)
{
notification.IsAnimatingOut = true;
notification.IsAnimatingIn = false;
}
protected override void DrawInternal()
{
ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, Vector2.Zero);
lock (_notificationLock)
{
UpdateNotifications();
if (_notifications.Count == 0)
{
ImGui.PopStyleVar();
IsOpen = false;
return;
}
var viewport = ImGui.GetMainViewport();
Position = CalculateWindowPosition(viewport);
DrawAllNotifications();
}
ImGui.PopStyleVar();
}
private Vector2 CalculateWindowPosition(ImGuiViewportPtr viewport)
{
var x = viewport.WorkPos.X + viewport.WorkSize.X -
_configService.Current.NotificationWidth -
_configService.Current.NotificationOffsetX -
WindowPaddingOffset;
var y = viewport.WorkPos.Y + _configService.Current.NotificationOffsetY;
return new Vector2(x, y);
}
private void DrawAllNotifications()
{
for (int i = 0; i < _notifications.Count; i++)
{
DrawNotification(_notifications[i], i);
if (i < _notifications.Count - 1)
{
ImGui.Dummy(new Vector2(0, _configService.Current.NotificationSpacing));
}
}
}
private void UpdateNotifications()
{
var deltaTime = ImGui.GetIO().DeltaTime;
EnforceMaxNotificationLimit();
UpdateAnimationsAndRemoveExpired(deltaTime);
}
private void EnforceMaxNotificationLimit()
{
var maxNotifications = _configService.Current.MaxSimultaneousNotifications;
while (_notifications.Count(n => !n.IsAnimatingOut) > maxNotifications)
{
var oldestNotification = _notifications
.Where(n => !n.IsAnimatingOut)
.OrderBy(n => n.CreatedAt)
.FirstOrDefault();
if (oldestNotification != null)
{
StartOutAnimation(oldestNotification);
}
}
}
private void UpdateAnimationsAndRemoveExpired(float deltaTime)
{
for (int i = _notifications.Count - 1; i >= 0; i--)
{
var notification = _notifications[i];
UpdateNotificationAnimation(notification, deltaTime);
if (ShouldRemoveNotification(notification))
{
_notifications.RemoveAt(i);
}
}
}
private void UpdateNotificationAnimation(LightlessNotification notification, float deltaTime)
{
if (notification.IsAnimatingIn && notification.AnimationProgress < 1f)
{
notification.AnimationProgress = Math.Min(1f,
notification.AnimationProgress + deltaTime * _configService.Current.NotificationAnimationSpeed);
}
else if (notification.IsAnimatingOut && notification.AnimationProgress > 0f)
{
notification.AnimationProgress = Math.Max(0f,
notification.AnimationProgress - deltaTime * _configService.Current.NotificationAnimationSpeed * OutAnimationSpeedMultiplier);
}
else if (!notification.IsAnimatingOut && !notification.IsDismissed)
{
notification.IsAnimatingIn = false;
if (notification.IsExpired)
{
StartOutAnimation(notification);
}
}
}
private bool ShouldRemoveNotification(LightlessNotification notification) =>
notification.IsAnimatingOut && notification.AnimationProgress <= 0.01f;
private void DrawNotification(LightlessNotification notification, int index)
{
var alpha = notification.AnimationProgress;
if (alpha <= 0f) return;
var slideOffset = (1f - alpha) * SlideAnimationDistance;
var originalCursorPos = ImGui.GetCursorPos();
ImGui.SetCursorPosX(originalCursorPos.X + slideOffset);
var notificationHeight = CalculateNotificationHeight(notification);
var notificationWidth = _configService.Current.NotificationWidth - slideOffset;
ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, Vector2.Zero);
using var child = ImRaii.Child($"notification_{notification.Id}",
new Vector2(notificationWidth, notificationHeight),
false, ImGuiWindowFlags.NoScrollbar);
if (child.Success)
{
DrawNotificationContent(notification, alpha);
}
ImGui.PopStyleVar();
}
private void DrawNotificationContent(LightlessNotification notification, float alpha)
{
var drawList = ImGui.GetWindowDrawList();
var windowPos = ImGui.GetWindowPos();
var windowSize = ImGui.GetWindowSize();
var bgColor = CalculateBackgroundColor(alpha, ImGui.IsWindowHovered());
var accentColor = GetNotificationAccentColor(notification.Type);
accentColor.W *= alpha;
DrawShadow(drawList, windowPos, windowSize, alpha);
HandleClickToDismiss(notification);
DrawBackground(drawList, windowPos, windowSize, bgColor);
DrawAccentBar(drawList, windowPos, windowSize, accentColor);
DrawDurationProgressBar(notification, alpha, windowPos, windowSize, drawList);
DrawNotificationText(notification, alpha);
}
private Vector4 CalculateBackgroundColor(float alpha, bool isHovered)
{
var baseOpacity = _configService.Current.NotificationOpacity;
var finalOpacity = baseOpacity * alpha;
var bgColor = new Vector4(30f/255f, 30f/255f, 30f/255f, finalOpacity);
if (isHovered)
{
bgColor *= 1.1f;
bgColor.W = Math.Min(bgColor.W, 0.98f);
}
return bgColor;
}
private void DrawShadow(ImDrawListPtr drawList, Vector2 windowPos, Vector2 windowSize, float alpha)
{
var shadowOffset = new Vector2(1f, 1f);
var shadowColor = new Vector4(0f, 0f, 0f, 0.4f * alpha);
drawList.AddRectFilled(
windowPos + shadowOffset,
windowPos + windowSize + shadowOffset,
ImGui.ColorConvertFloat4ToU32(shadowColor),
3f
);
}
private void HandleClickToDismiss(LightlessNotification notification)
{
if (ImGui.IsWindowHovered() &&
_configService.Current.DismissNotificationOnClick &&
!notification.Actions.Any() &&
ImGui.IsMouseClicked(ImGuiMouseButton.Left))
{
notification.IsDismissed = true;
StartOutAnimation(notification);
}
}
private void DrawBackground(ImDrawListPtr drawList, Vector2 windowPos, Vector2 windowSize, Vector4 bgColor)
{
drawList.AddRectFilled(
windowPos,
windowPos + windowSize,
ImGui.ColorConvertFloat4ToU32(bgColor),
3f
);
}
private void DrawAccentBar(ImDrawListPtr drawList, Vector2 windowPos, Vector2 windowSize, Vector4 accentColor)
{
var accentWidth = _configService.Current.NotificationAccentBarWidth;
if (accentWidth > 0f)
{
drawList.AddRectFilled(
windowPos,
windowPos + new Vector2(accentWidth, windowSize.Y),
ImGui.ColorConvertFloat4ToU32(accentColor),
3f
);
}
}
private void DrawDurationProgressBar(LightlessNotification notification, float alpha, Vector2 windowPos, Vector2 windowSize, ImDrawListPtr drawList)
{
var progress = CalculateProgress(notification);
var progressBarColor = UIColors.Get("LightlessBlue");
var progressHeight = 2f;
var progressY = windowPos.Y + windowSize.Y - progressHeight;
var progressWidth = windowSize.X * progress;
DrawProgressBackground(drawList, windowPos, windowSize, progressY, progressHeight, progressBarColor, alpha);
if (progress > 0)
{
DrawProgressForeground(drawList, windowPos, progressY, progressHeight, progressWidth, progressBarColor, alpha);
}
}
private float CalculateProgress(LightlessNotification notification)
{
if (notification.Type == NotificationType.Download && notification.ShowProgress)
{
return Math.Clamp(notification.Progress, 0f, 1f);
}
var elapsed = DateTime.UtcNow - notification.CreatedAt;
return Math.Min(1.0f, (float)(elapsed.TotalSeconds / notification.Duration.TotalSeconds));
}
private void DrawProgressBackground(ImDrawListPtr drawList, Vector2 windowPos, Vector2 windowSize, float progressY, float progressHeight, Vector4 progressBarColor, float alpha)
{
var bgProgressColor = new Vector4(progressBarColor.X * 0.3f, progressBarColor.Y * 0.3f, progressBarColor.Z * 0.3f, 0.5f * alpha);
drawList.AddRectFilled(
new Vector2(windowPos.X, progressY),
new Vector2(windowPos.X + windowSize.X, progressY + progressHeight),
ImGui.ColorConvertFloat4ToU32(bgProgressColor),
0f
);
}
private void DrawProgressForeground(ImDrawListPtr drawList, Vector2 windowPos, float progressY, float progressHeight, float progressWidth, Vector4 progressBarColor, float alpha)
{
var progressColor = progressBarColor;
progressColor.W *= alpha;
drawList.AddRectFilled(
new Vector2(windowPos.X, progressY),
new Vector2(windowPos.X + progressWidth, progressY + progressHeight),
ImGui.ColorConvertFloat4ToU32(progressColor),
0f
);
}
private void DrawNotificationText(LightlessNotification notification, float alpha)
{
var padding = new Vector2(10f, 6f);
var contentPos = new Vector2(padding.X, padding.Y);
var windowSize = ImGui.GetWindowSize();
var contentSize = new Vector2(windowSize.X - padding.X, windowSize.Y - padding.Y * 2);
ImGui.SetCursorPos(contentPos);
var titleHeight = DrawTitle(notification, contentSize.X, alpha);
DrawMessage(notification, contentPos, contentSize.X, titleHeight, alpha);
if (notification.Actions.Count > 0)
{
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + ImGui.GetStyle().ItemSpacing.Y);
ImGui.SetCursorPosX(contentPos.X);
DrawNotificationActions(notification, contentSize.X, alpha);
}
}
private float DrawTitle(LightlessNotification notification, float contentWidth, float alpha)
{
using (ImRaii.PushColor(ImGuiCol.Text, new Vector4(1f, 1f, 1f, alpha)))
{
ImGui.PushTextWrapPos(ImGui.GetCursorPosX() + 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 void DrawMessage(LightlessNotification notification, Vector2 contentPos, float contentWidth, float titleHeight, float alpha)
{
if (string.IsNullOrEmpty(notification.Message)) return;
ImGui.SetCursorPos(contentPos + new Vector2(0f, titleHeight + 4f));
ImGui.PushTextWrapPos(ImGui.GetCursorPosX() + contentWidth);
using (ImRaii.PushColor(ImGuiCol.Text, new Vector4(0.9f, 0.9f, 0.9f, alpha)))
{
ImGui.TextWrapped(notification.Message);
}
ImGui.PopTextWrapPos();
}
private void DrawNotificationActions(LightlessNotification notification, float availableWidth, float alpha)
{
var buttonSpacing = 8f;
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}",
notification.Actions.Count, buttonWidth, availableWidth);
var startCursorPos = ImGui.GetCursorPos();
for (int i = 0; i < notification.Actions.Count; i++)
{
var action = notification.Actions[i];
if (i > 0)
{
ImGui.SameLine();
var currentX = startCursorPos.X + i * (buttonWidth + buttonSpacing);
ImGui.SetCursorPosX(currentX);
}
DrawActionButton(action, notification, alpha, 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);
var buttonColor = action.Color;
buttonColor.W *= alpha;
var hoveredColor = buttonColor * 1.1f;
hoveredColor.W = buttonColor.W;
var activeColor = buttonColor * 0.9f;
activeColor.W = buttonColor.W;
using (ImRaii.PushColor(ImGuiCol.Button, buttonColor))
using (ImRaii.PushColor(ImGuiCol.ButtonHovered, hoveredColor))
using (ImRaii.PushColor(ImGuiCol.ButtonActive, activeColor))
using (ImRaii.PushColor(ImGuiCol.Text, new Vector4(1f, 1f, 1f, alpha)))
{
var buttonPressed = false;
if (action.Icon != FontAwesomeIcon.None)
{
buttonPressed = DrawIconTextButton(action.Icon, action.Label, buttonWidth, alpha);
}
else
{
buttonPressed = ImGui.Button(action.Label, new Vector2(buttonWidth, 0));
}
_logger.LogDebug("Button {ActionId} pressed: {ButtonPressed}", action.Id, buttonPressed);
if (buttonPressed)
{
try
{
_logger.LogDebug("Executing action: {ActionId}", action.Id);
action.OnClick(notification);
_logger.LogDebug("Action executed successfully: {ActionId}", action.Id);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error executing notification action: {ActionId}", action.Id);
}
}
}
}
private bool DrawIconTextButton(FontAwesomeIcon icon, string text, float width, float alpha)
{
var drawList = ImGui.GetWindowDrawList();
var cursorPos = ImGui.GetCursorScreenPos();
var frameHeight = ImGui.GetFrameHeight();
Vector2 iconSize;
using (ImRaii.PushFont(UiBuilder.IconFont))
{
iconSize = ImGui.CalcTextSize(icon.ToIconString());
}
var textSize = ImGui.CalcTextSize(text);
var spacing = 3f * ImGuiHelpers.GlobalScale;
var totalTextWidth = iconSize.X + spacing + textSize.X;
var buttonPressed = ImGui.InvisibleButton($"btn_{icon}_{text}", new Vector2(width, frameHeight));
var buttonMin = ImGui.GetItemRectMin();
var buttonMax = ImGui.GetItemRectMax();
var buttonSize = buttonMax - buttonMin;
var buttonColor = ImGui.GetColorU32(ImGuiCol.Button);
if (ImGui.IsItemHovered())
buttonColor = ImGui.GetColorU32(ImGuiCol.ButtonHovered);
if (ImGui.IsItemActive())
buttonColor = ImGui.GetColorU32(ImGuiCol.ButtonActive);
drawList.AddRectFilled(buttonMin, buttonMax, buttonColor, 3f);
var iconPos = buttonMin + new Vector2((buttonSize.X - totalTextWidth) / 2f, (buttonSize.Y - iconSize.Y) / 2f);
var textPos = iconPos + new Vector2(iconSize.X + spacing, (iconSize.Y - textSize.Y) / 2f);
var textColor = ImGui.GetColorU32(ImGuiCol.Text);
// Draw icon
using (ImRaii.PushFont(UiBuilder.IconFont))
{
drawList.AddText(iconPos, textColor, icon.ToIconString());
}
// Draw text
drawList.AddText(textPos, textColor, text);
return buttonPressed;
}
private float CalculateNotificationHeight(LightlessNotification notification)
{
var contentWidth = _configService.Current.NotificationWidth - 35f;
var height = 12f;
height += CalculateTitleHeight(notification, contentWidth);
height += CalculateMessageHeight(notification, contentWidth);
if (notification.ShowProgress)
{
height += 12f;
}
if (notification.Actions.Count > 0)
{
height += ImGui.GetStyle().ItemSpacing.Y;
height += ImGui.GetFrameHeight();
height += 12f;
}
return Math.Clamp(height, NotificationMinHeight, NotificationMaxHeight);
}
private float CalculateTitleHeight(LightlessNotification notification, float contentWidth)
{
var titleText = _configService.Current.ShowNotificationTimestamp
? $"[{notification.CreatedAt.ToLocalTime():HH:mm:ss}] {notification.Title}"
: notification.Title;
return ImGui.CalcTextSize(titleText, true, contentWidth).Y;
}
private float CalculateMessageHeight(LightlessNotification notification, float contentWidth)
{
if (string.IsNullOrEmpty(notification.Message)) return 0f;
var messageHeight = ImGui.CalcTextSize(notification.Message, true, contentWidth).Y;
return 4f + messageHeight;
}
private Vector4 GetNotificationAccentColor(NotificationType type)
{
return type switch
{
NotificationType.Info => UIColors.Get("LightlessPurple"),
NotificationType.Warning => UIColors.Get("LightlessYellow"),
NotificationType.Error => UIColors.Get("DimRed"),
NotificationType.PairRequest => UIColors.Get("LightlessBlue"),
NotificationType.Download => UIColors.Get("LightlessGreen"),
_ => UIColors.Get("LightlessPurple")
};
}
}

View File

@@ -0,0 +1,32 @@
using Dalamud.Interface;
using LightlessSync.LightlessConfiguration.Models;
using System.Numerics;
namespace LightlessSync.UI.Models;
public class LightlessNotification
{
public string Id { get; set; } = Guid.NewGuid().ToString();
public string Title { get; set; } = string.Empty;
public string Message { get; set; } = string.Empty;
public NotificationType Type { get; set; } = NotificationType.Info;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public TimeSpan Duration { get; set; } = TimeSpan.FromSeconds(5);
public bool IsExpired => DateTime.UtcNow - CreatedAt > Duration;
public bool IsDismissed { get; set; } = false;
public List<LightlessNotificationAction> Actions { get; set; } = new();
public bool ShowProgress { get; set; } = false;
public float Progress { get; set; } = 0f;
public float AnimationProgress { get; set; } = 0f;
public bool IsAnimatingIn { get; set; } = true;
public bool IsAnimatingOut { get; set; } = false;
public uint? SoundEffectId { get; set; } = null;
}
public class LightlessNotificationAction
{
public string Id { get; set; } = Guid.NewGuid().ToString();
public string Label { get; set; } = string.Empty;
public FontAwesomeIcon Icon { get; set; } = FontAwesomeIcon.None;
public Vector4 Color { get; set; } = Vector4.One;
public Action<LightlessNotification> OnClick { get; set; } = _ => { };
public bool IsPrimary { get; set; } = false;
public bool IsDestructive { get; set; } = false;
}

View File

@@ -0,0 +1,72 @@
using LightlessSync.LightlessConfiguration.Models;
namespace LightlessSync.UI.Models;
/// <summary>
/// Common FFXIV <se.#> sound effect IDs for notifications.
/// These correspond to the same sound IDs used in macros (116).
/// </summary>
public static class NotificationSounds
{
// ─────────────────────────────────────────────
// Base <se.#> IDs (116)
// https://ffxiv.consolegameswiki.com/wiki/Macros#Sound_Effects
// ─────────────────────────────────────────────
public const uint Se1 = 1; // Soft chime
public const uint Se2 = 2; // Higher chime
public const uint Se3 = 3; // Bell tone
public const uint Se4 = 4; // Harp tone
public const uint Se5 = 5; // Mechanical click
public const uint Se6 = 6; // Drum / percussion
public const uint Se7 = 7; // Metallic chime
public const uint Se8 = 8; // Wooden tone
public const uint Se9 = 9; // Wind / flute tone
public const uint Se10 = 11; // Magical sparkle (ID 10 is skipped in game)
public const uint Se11 = 12; // Metallic ring
public const uint Se12 = 13; // Deep thud
public const uint Se13 = 14; // "Tell received" ping
public const uint Se14 = 15; // Success fanfare
public const uint Se15 = 16; // System warning
// Note: Se16 doesn't exist - Se15 is the last available sound
/// <summary>
/// General notification sound (<se.2>)
/// </summary>
public const uint Info = Se2;
/// <summary>
/// Warning/alert sound (<se.15>)
/// </summary>
public const uint Warning = Se15;
/// <summary>
/// Error sound (<se.15> - System warning, used for errors)
/// </summary>
public const uint Error = Se15;
/// <summary>
/// Success sound (<se.14>)
/// </summary>
public const uint Success = Se14;
/// <summary>
/// Pair request sound (<se.13>, same as tell notification)
/// </summary>
public const uint PairRequest = Se13;
/// <summary>
/// Download complete sound (<se.10>, a clean sparkle tone)
/// </summary>
public const uint DownloadComplete = Se10;
/// <summary>
/// Get default sound for notification type
/// </summary>
public static uint GetDefaultSound(NotificationType type) => type switch
{
NotificationType.Info => Info,
NotificationType.Warning => Warning,
NotificationType.Error => Error,
_ => Info
};
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
@@ -11,12 +11,8 @@ using LightlessSync.Services;
using LightlessSync.Services.Mediator;
using LightlessSync.Utils;
using LightlessSync.WebAPI;
using Serilog;
using System;
using System.Collections.Generic;
using System.Numerics;
using System.Reflection.Emit;
using System.Threading.Tasks;
namespace LightlessSync.UI;
@@ -33,13 +29,14 @@ public class TopTabMenu
private bool _pairRequestsExpanded; // useless for now
private int _lastRequestCount;
private readonly UiSharedService _uiSharedService;
private readonly NotificationService _lightlessNotificationService;
private string _filter = string.Empty;
private int _globalControlCountdown = 0;
private float _pairRequestsHeight = 150f;
private string _pairToAdd = string.Empty;
private SelectedTab _selectedTab = SelectedTab.None;
public TopTabMenu(LightlessMediator lightlessMediator, ApiController apiController, PairManager pairManager, UiSharedService uiSharedService, PairRequestService pairRequestService, DalamudUtilService dalamudUtilService)
public TopTabMenu(LightlessMediator lightlessMediator, ApiController apiController, PairManager pairManager, UiSharedService uiSharedService, PairRequestService pairRequestService, DalamudUtilService dalamudUtilService, NotificationService lightlessNotificationService)
{
_lightlessMediator = lightlessMediator;
_apiController = apiController;
@@ -47,6 +44,7 @@ public class TopTabMenu
_pairRequestService = pairRequestService;
_dalamudUtilService = dalamudUtilService;
_uiSharedService = uiSharedService;
_lightlessNotificationService = lightlessNotificationService;
}
private enum SelectedTab
@@ -199,16 +197,79 @@ public class TopTabMenu
if (TabSelection != SelectedTab.None) ImGuiHelpers.ScaledDummy(3f);
#if DEBUG
if (ImGui.Button("Add Test Pair Request"))
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"))
{
var fakeCid = Guid.NewGuid().ToString("N");
var display = _pairRequestService.RegisterIncomingRequest(fakeCid, "Debug pair request");
_lightlessMediator.Publish(new NotificationMessage(
"Pair request received (debug)",
display.Message,
"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);
@@ -850,4 +911,4 @@ public class TopTabMenu
ImGui.EndPopup();
}
}
}
}

View File

@@ -1,4 +1,4 @@
using LightlessSync.API.Data;
using LightlessSync.API.Data;
using LightlessSync.API.Data.Enum;
using LightlessSync.API.Dto;
using LightlessSync.API.Dto.CharaData;
@@ -6,6 +6,7 @@ using LightlessSync.API.Dto.Group;
using LightlessSync.API.Dto.User;
using LightlessSync.LightlessConfiguration.Models;
using LightlessSync.Services.Mediator;
using LightlessSync.Utils;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.Logging;
@@ -104,25 +105,27 @@ public partial class ApiController
return Task.CompletedTask;
}
public Task Client_ReceiveBroadcastPairRequest(UserPairNotificationDto dto)
{
if (dto == null)
return Task.CompletedTask;
var request = _pairRequestService.RegisterIncomingRequest(dto.myHashedCid, dto.message ?? string.Empty);
var senderName = string.IsNullOrEmpty(request.DisplayName) ? "Unknown User" : request.DisplayName;
Mediator.Publish(new NotificationMessage(
"Pair request received",
request.Message,
NotificationType.Info,
TimeSpan.FromSeconds(5)));
_lightlessNotificationService.ShowPairRequestNotification(
senderName,
request.HashedCid,
onAccept: () => _pairRequestService.AcceptPairRequest(request.HashedCid),
onDecline: () => _pairRequestService.DeclinePairRequest(request.HashedCid));
return Task.CompletedTask;
}
public Task Client_UpdateSystemInfo(SystemInfoDto systemInfo)
{
SystemInfoDto = systemInfo;
//Mediator.Publish(new UpdateSystemInfoMessage(systemInfo));
return Task.CompletedTask;
}

View File

@@ -1,4 +1,4 @@
using Dalamud.Utility;
using Dalamud.Utility;
using LightlessSync.API.Data;
using LightlessSync.API.Data.Extensions;
using LightlessSync.API.Dto;
@@ -32,6 +32,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
private readonly ServerConfigurationManager _serverManager;
private readonly TokenProvider _tokenProvider;
private readonly LightlessConfigService _lightlessConfigService;
private readonly NotificationService _lightlessNotificationService;
private CancellationTokenSource _connectionCancellationTokenSource;
private ConnectionDto? _connectionDto;
private bool _doNotNotifyOnNextInfo = false;
@@ -44,7 +45,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
public ApiController(ILogger<ApiController> logger, HubFactory hubFactory, DalamudUtilService dalamudUtil,
PairManager pairManager, PairRequestService pairRequestService, ServerConfigurationManager serverManager, LightlessMediator mediator,
TokenProvider tokenProvider, LightlessConfigService lightlessConfigService) : base(logger, mediator)
TokenProvider tokenProvider, LightlessConfigService lightlessConfigService, NotificationService lightlessNotificationService) : base(logger, mediator)
{
_hubFactory = hubFactory;
_dalamudUtil = dalamudUtil;
@@ -53,6 +54,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
_serverManager = serverManager;
_tokenProvider = tokenProvider;
_lightlessConfigService = lightlessConfigService;
_lightlessNotificationService = lightlessNotificationService;
_connectionCancellationTokenSource = new CancellationTokenSource();
Mediator.Subscribe<DalamudLoginMessage>(this, (_) => DalamudUtilOnLogIn());