notifications refactor with duplication bugfix

This commit is contained in:
choco
2025-10-09 11:13:47 +02:00
parent 3fdc9dd958
commit 2b118df892
10 changed files with 434 additions and 585 deletions

View File

@@ -25,7 +25,7 @@ public sealed class PairManager : DisposableMediatorSubscriberBase
private readonly LightlessConfigService _configurationService;
private readonly IContextMenu _dalamudContextMenu;
private readonly PairFactory _pairFactory;
private readonly LightlessNotificationService _lightlessNotificationService;
private readonly NotificationService _lightlessNotificationService;
private Lazy<List<Pair>> _directPairsInternal;
private Lazy<Dictionary<GroupFullInfoDto, List<Pair>>> _groupPairsInternal;
private Lazy<Dictionary<Pair, List<GroupFullInfoDto>>> _pairsWithGroupsInternal;
@@ -37,7 +37,7 @@ public sealed class PairManager : DisposableMediatorSubscriberBase
public PairManager(ILogger<PairManager> logger, PairFactory pairFactory,
LightlessConfigService configurationService, LightlessMediator mediator,
IContextMenu dalamudContextMenu, PairProcessingLimiter pairProcessingLimiter,
LightlessNotificationService lightlessNotificationService) : base(logger, mediator)
NotificationService lightlessNotificationService) : base(logger, mediator)
{
_pairFactory = pairFactory;
_configurationService = configurationService;

View File

@@ -1,4 +1,4 @@
using Dalamud.Game;
using Dalamud.Game;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Interface.ImGuiFileDialog;
using Dalamud.Interface.Windowing;
@@ -145,7 +145,7 @@ public sealed class Plugin : IDalamudPlugin
collection.AddSingleton((s) => new DtrEntry(s.GetRequiredService<ILogger<DtrEntry>>(), dtrBar, s.GetRequiredService<LightlessConfigService>(),
s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<PairManager>(), s.GetRequiredService<ApiController>(), s.GetRequiredService<ServerConfigurationManager>()));
collection.AddSingleton(s => new PairManager(s.GetRequiredService<ILogger<PairManager>>(), s.GetRequiredService<PairFactory>(),
s.GetRequiredService<LightlessConfigService>(), s.GetRequiredService<LightlessMediator>(), contextMenu, s.GetRequiredService<PairProcessingLimiter>(), s.GetRequiredService<LightlessNotificationService>()));
s.GetRequiredService<LightlessConfigService>(), s.GetRequiredService<LightlessMediator>(), contextMenu, s.GetRequiredService<PairProcessingLimiter>(), s.GetRequiredService<NotificationService>()));
collection.AddSingleton<RedrawManager>();
collection.AddSingleton<BroadcastService>();
collection.AddSingleton(addonLifecycle);
@@ -172,16 +172,14 @@ 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 LightlessNotificationService(
s.GetRequiredService<ILogger<LightlessNotificationService>>(),
collection.AddSingleton((s) => new NotificationService(
s.GetRequiredService<ILogger<NotificationService>>(),
s.GetRequiredService<LightlessConfigService>(),
s.GetRequiredService<DalamudUtilService>(),
notificationManager,
chatGui,
s.GetRequiredService<LightlessMediator>()));
s.GetRequiredService<LightlessMediator>(),
s.GetServices<WindowMediatorSubscriberBase>()));
collection.AddSingleton((s) =>
{
var httpClient = new HttpClient();
@@ -257,7 +255,7 @@ public sealed class Plugin : IDalamudPlugin
s.GetRequiredService<UiFactory>(),
s.GetRequiredService<FileDialogManager>(),
s.GetRequiredService<LightlessMediator>(),
s.GetRequiredService<LightlessNotificationService>()));
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>()));
@@ -276,7 +274,6 @@ public sealed class Plugin : IDalamudPlugin
collection.AddHostedService(p => p.GetRequiredService<FileCacheManager>());
collection.AddHostedService(p => p.GetRequiredService<ConfigurationMigrator>());
collection.AddHostedService(p => p.GetRequiredService<DalamudUtilService>());
collection.AddHostedService(p => p.GetRequiredService<LightlessNotificationService>());
collection.AddHostedService(p => p.GetRequiredService<PerformanceCollectorService>());
collection.AddHostedService(p => p.GetRequiredService<DtrEntry>());
collection.AddHostedService(p => p.GetRequiredService<EventAggregator>());

View File

@@ -1,501 +0,0 @@
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 LightlessNotificationService : DisposableMediatorSubscriberBase, IHostedService
{
private readonly ILogger<LightlessNotificationService> _logger;
private readonly LightlessConfigService _configService;
private readonly DalamudUtilService _dalamudUtilService;
private readonly INotificationManager _notificationManager;
private readonly IChatGui _chatGui;
private LightlessNotificationUI? _notificationUI;
public LightlessNotificationService(
ILogger<LightlessNotificationService> logger,
LightlessConfigService configService,
DalamudUtilService dalamudUtilService,
INotificationManager notificationManager,
IChatGui chatGui,
LightlessMediator mediator) : base(logger, mediator)
{
_logger = logger;
_configService = configService;
_dalamudUtilService = dalamudUtilService;
_notificationManager = notificationManager;
_chatGui = chatGui;
}
public Task StartAsync(CancellationToken cancellationToken)
{
Mediator.Subscribe<NotificationMessage>(this, HandleNotificationMessage);
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
public void SetNotificationUI(LightlessNotificationUI notificationUI)
{
_notificationUI = notificationUI;
}
public void ShowNotification(string title, string message, NotificationType type = NotificationType.Info,
TimeSpan? duration = null, List<LightlessNotificationAction>? actions = null, uint? soundEffectId = null)
{
var notification = new LightlessNotification
{
Title = title,
Message = message,
Type = type,
Duration = duration ?? TimeSpan.FromSeconds(_configService.Current.DefaultNotificationDurationSeconds),
Actions = actions ?? new List<LightlessNotificationAction>(),
SoundEffectId = GetSoundEffectId(type, soundEffectId),
ShowProgress = _configService.Current.ShowNotificationProgress,
CreatedAt = DateTime.UtcNow
};
if (_configService.Current.AutoDismissOnAction && notification.Actions.Any())
{
foreach (var action in notification.Actions)
{
var originalOnClick = action.OnClick;
action.OnClick = (n) =>
{
originalOnClick(n);
if (_configService.Current.AutoDismissOnAction)
{
n.IsDismissed = true;
n.IsAnimatingOut = true;
}
};
}
}
if (notification.SoundEffectId.HasValue && _configService.Current.EnableNotificationSounds)
{
PlayNotificationSound(notification.SoundEffectId.Value);
}
Mediator.Publish(new LightlessNotificationMessage(notification));
}
public void ShowPairRequestNotification(string senderName, string senderId, Action onAccept, Action onDecline)
{
var notification = new LightlessNotification
{
Title = "Pair Request Received",
Message = $"{senderName} wants to pair with you.",
Type = NotificationType.Info,
Duration = TimeSpan.FromSeconds(60),
SoundEffectId = NotificationSounds.PairRequest,
Actions = 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();
n.IsDismissed = true;
n.IsAnimatingOut = true;
}
},
new()
{
Id = "decline",
Label = "Decline",
Icon = FontAwesomeIcon.Times,
Color = UIColors.Get("DimRed"),
IsDestructive = true,
OnClick = (n) =>
{
_logger.LogInformation("Pair request declined");
onDecline();
n.IsDismissed = true;
n.IsAnimatingOut = true;
}
}
}
};
if (notification.SoundEffectId.HasValue)
{
PlayNotificationSound(notification.SoundEffectId.Value);
}
Mediator.Publish(new LightlessNotificationMessage(notification));
}
public void ShowDownloadCompleteNotification(string fileName, int fileCount, Action? onOpenFolder = null)
{
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();
n.IsDismissed = true;
n.IsAnimatingOut = true;
}
});
}
var notification = new LightlessNotification
{
Title = "Download Complete",
Message = fileCount > 1 ?
$"Downloaded {fileCount} files successfully." :
$"Downloaded {fileName} successfully.",
Type = NotificationType.Info,
Duration = TimeSpan.FromSeconds(8),
Actions = actions,
SoundEffectId = NotificationSounds.DownloadComplete
};
if (notification.SoundEffectId.HasValue)
{
PlayNotificationSound(notification.SoundEffectId.Value);
}
Mediator.Publish(new LightlessNotificationMessage(notification));
}
public void ShowErrorNotification(string title, string message, Exception? exception = null, Action? onRetry = null, Action? onViewLog = null)
{
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();
n.IsDismissed = true;
n.IsAnimatingOut = true;
}
});
}
if (onViewLog != null)
{
actions.Add(new LightlessNotificationAction
{
Id = "view_log",
Label = "View Log",
Icon = FontAwesomeIcon.FileAlt,
Color = UIColors.Get("LightlessYellow"),
OnClick = (n) => onViewLog()
});
}
var notification = new LightlessNotification
{
Title = title,
Message = exception != null ? $"{message}\n\nError: {exception.Message}" : message,
Type = NotificationType.Error,
Duration = TimeSpan.FromSeconds(15),
Actions = actions,
SoundEffectId = NotificationSounds.Error
};
if (notification.SoundEffectId.HasValue)
{
PlayNotificationSound(notification.SoundEffectId.Value);
}
Mediator.Publish(new LightlessNotificationMessage(notification));
}
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 completedCount = userDownloads.Count(x => x.progress >= 1.0f);
var totalCount = userDownloads.Count;
var message = "";
if (queueWaiting > 0)
{
message = $"Queue: {queueWaiting} waiting";
}
if (totalCount > 0)
{
var progressMessage = $"Progress: {completedCount}/{totalCount} completed";
message = string.IsNullOrEmpty(message) ? progressMessage : $"{message}\n{progressMessage}";
}
if (userDownloads.Any(x => x.progress < 1.0f))
{
var maxNamesToShow = _configService.Current.MaxConcurrentPairApplications;
var activeDownloads = userDownloads.Where(x => x.progress < 1.0f).Take(maxNamesToShow);
var downloadLines = string.Join("\n", activeDownloads.Select(x =>
{
var statusText = x.status switch
{
"downloading" => $"{x.progress:P0}",
"decompressing" => "decompressing",
"queued" => "queued",
"waiting" => "waiting for slot",
_ => x.status
};
return $"• {x.playerName}: {statusText}";
}));
message += string.IsNullOrEmpty(message) ? downloadLines : $"\n{downloadLines}";
}
var allDownloadsCompleted = userDownloads.All(x => x.progress >= 1.0f) && userDownloads.Any();
var notification = new LightlessNotification
{
Id = "pair_download_progress",
Title = "Downloading Pair Data",
Message = message,
Type = NotificationType.Info,
Duration = TimeSpan.FromMinutes(5),
ShowProgress = true,
Progress = totalProgress
};
Mediator.Publish(new LightlessNotificationMessage(notification));
if (allDownloadsCompleted)
{
DismissPairDownloadNotification();
}
}
public void DismissPairDownloadNotification()
{
Mediator.Publish(new LightlessNotificationDismissMessage("pair_download_progress"));
}
private uint? GetSoundEffectId(NotificationType type, uint? overrideSoundId)
{
if (!_configService.Current.EnableNotificationSounds)
return null;
if (overrideSoundId.HasValue)
return overrideSoundId;
if (_configService.Current.UseCustomSounds)
{
return type switch
{
NotificationType.Info => _configService.Current.CustomInfoSoundId,
NotificationType.Warning => _configService.Current.CustomWarningSoundId,
NotificationType.Error => _configService.Current.CustomErrorSoundId,
_ => NotificationSounds.GetDefaultSound(type)
};
}
return NotificationSounds.GetDefaultSound(type);
}
private void PlayNotificationSound(uint soundEffectId)
{
try
{
try
{
UIGlobals.PlayChatSoundEffect(soundEffectId);
_logger.LogDebug("Played notification sound effect {SoundId} via ChatGui", soundEffectId);
}
catch (Exception chatEx)
{
_logger.LogWarning(chatEx, "Failed to play sound via ChatGui for ID {SoundId}", 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;
// Get both old and new notification locations
var oldLocation = msg.Type switch
{
NotificationType.Info => _configService.Current.InfoNotification,
NotificationType.Warning => _configService.Current.WarningNotification,
NotificationType.Error => _configService.Current.ErrorNotification,
_ => NotificationLocation.Nowhere
};
var newLocation = msg.Type switch
{
NotificationType.Info => _configService.Current.LightlessInfoNotification,
NotificationType.Warning => _configService.Current.LightlessWarningNotification,
NotificationType.Error => _configService.Current.LightlessErrorNotification,
_ => NotificationLocation.LightlessUI
};
// Show notifications based on system selection with backwards compatibility
if (!_configService.Current.UseLightlessNotifications)
{
// Only use old system when new system is disabled
ShowNotificationLocationBased(msg, oldLocation);
}
else
{
// Use new enhanced system as primary
ShowNotificationLocationBased(msg, newLocation);
// Also use old system as fallback for backwards compatibility
// Only if it's different from the new location and not "Nowhere"
if (oldLocation != NotificationLocation.Nowhere &&
oldLocation != newLocation &&
!IsLightlessLocation(oldLocation))
{
ShowNotificationLocationBased(msg, oldLocation);
}
}
}
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 ?? TimeSpan.FromSeconds(_configService.Current.DefaultNotificationDurationSeconds);
uint? soundId = null;
if (_configService.Current.EnableNotificationSounds)
{
if (_configService.Current.UseCustomSounds)
{
soundId = msg.Type switch
{
NotificationType.Info => _configService.Current.CustomInfoSoundId,
NotificationType.Warning => _configService.Current.CustomWarningSoundId,
NotificationType.Error => _configService.Current.CustomErrorSoundId,
_ => NotificationSounds.GetDefaultSound(msg.Type)
};
}
else
{
soundId = NotificationSounds.GetDefaultSound(msg.Type);
}
}
ShowNotification(msg.Title ?? "Lightless Sync", msg.Message ?? string.Empty, msg.Type, duration, null, soundId);
}
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)
});
}
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)
{
SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] Error: " + message);
_chatGui.PrintError(se.BuiltString);
}
private void PrintInfoChat(string? message)
{
SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] Info: ").AddItalics(message ?? string.Empty);
_chatGui.Print(se.BuiltString);
}
private void PrintWarnChat(string? message)
{
SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] ").AddUiForeground("Warning: " + (message ?? string.Empty), 31).AddUiForegroundOff();
_chatGui.Print(se.BuiltString);
}
private bool IsLightlessLocation(NotificationLocation location)
{
return location switch
{
NotificationLocation.LightlessUI => true,
NotificationLocation.ChatAndLightlessUI => true,
_ => false
};
}
}

View File

@@ -1,99 +1,377 @@
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;
private readonly LightlessNotificationUI? _notificationUI;
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,
IEnumerable<WindowMediatorSubscriberBase> windows) : base(logger, mediator)
{
_logger = logger;
_configService = configService;
_dalamudUtilService = dalamudUtilService;
_notificationManager = notificationManager;
_chatGui = chatGui;
_configurationService = configurationService;
_notificationUI = windows.OfType<LightlessNotificationUI>().FirstOrDefault();
}
public Task StartAsync(CancellationToken cancellationToken)
{
Mediator.Subscribe<NotificationMessage>(this, ShowNotification);
Mediator.Subscribe<NotificationMessage>(this, HandleNotificationMessage);
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
private void PrintErrorChat(string? message)
public void ShowNotification(string title, string message, NotificationType type = NotificationType.Info,
TimeSpan? duration = null, List<LightlessNotificationAction>? actions = null, uint? soundEffectId = null)
{
SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] Error: " + message);
_chatGui.PrintError(se.BuiltString);
}
private void PrintInfoChat(string? message)
{
SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] Info: ").AddItalics(message ?? string.Empty);
_chatGui.Print(se.BuiltString);
}
private void PrintWarnChat(string? message)
{
SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] ").AddUiForeground("Warning: " + (message ?? string.Empty), 31).AddUiForegroundOff();
_chatGui.Print(se.BuiltString);
}
private void ShowChat(NotificationMessage msg)
{
switch (msg.Type)
var notification = new LightlessNotification
{
case NotificationType.Info:
PrintInfoChat(msg.Message);
break;
Title = title,
Message = message,
Type = type,
Duration = duration ?? TimeSpan.FromSeconds(_configService.Current.DefaultNotificationDurationSeconds),
Actions = actions ?? new List<LightlessNotificationAction>(),
SoundEffectId = GetSoundEffectId(type, soundEffectId),
ShowProgress = _configService.Current.ShowNotificationProgress,
CreatedAt = DateTime.UtcNow
};
case NotificationType.Warning:
PrintWarnChat(msg.Message);
break;
if (_configService.Current.AutoDismissOnAction && notification.Actions.Any())
{
foreach (var action in notification.Actions)
{
var originalOnClick = action.OnClick;
action.OnClick = (n) =>
{
originalOnClick(n);
if (_configService.Current.AutoDismissOnAction)
{
n.IsDismissed = true;
n.IsAnimatingOut = true;
}
};
}
}
case NotificationType.Error:
PrintErrorChat(msg.Message);
break;
if (notification.SoundEffectId.HasValue && _configService.Current.EnableNotificationSounds)
{
PlayNotificationSound(notification.SoundEffectId.Value);
}
Mediator.Publish(new LightlessNotificationMessage(notification));
}
public void ShowPairRequestNotification(string senderName, string senderId, Action onAccept, Action onDecline)
{
var notification = new LightlessNotification
{
Title = "Pair Request Received",
Message = $"{senderName} wants to pair with you.",
Type = NotificationType.Info,
Duration = TimeSpan.FromSeconds(60),
SoundEffectId = NotificationSounds.PairRequest,
Actions = 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();
n.IsDismissed = true;
n.IsAnimatingOut = true;
}
},
new()
{
Id = "decline",
Label = "Decline",
Icon = FontAwesomeIcon.Times,
Color = UIColors.Get("DimRed"),
IsDestructive = true,
OnClick = (n) =>
{
_logger.LogInformation("Pair request declined");
onDecline();
n.IsDismissed = true;
n.IsAnimatingOut = true;
}
}
}
};
if (notification.SoundEffectId.HasValue)
{
PlayNotificationSound(notification.SoundEffectId.Value);
}
Mediator.Publish(new LightlessNotificationMessage(notification));
}
public void ShowDownloadCompleteNotification(string fileName, int fileCount, Action? onOpenFolder = null)
{
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();
n.IsDismissed = true;
n.IsAnimatingOut = true;
}
});
}
var notification = new LightlessNotification
{
Title = "Download Complete",
Message = fileCount > 1 ?
$"Downloaded {fileCount} files successfully." :
$"Downloaded {fileName} successfully.",
Type = NotificationType.Info,
Duration = TimeSpan.FromSeconds(8),
Actions = actions,
SoundEffectId = NotificationSounds.DownloadComplete
};
if (notification.SoundEffectId.HasValue)
{
PlayNotificationSound(notification.SoundEffectId.Value);
}
Mediator.Publish(new LightlessNotificationMessage(notification));
}
public void ShowErrorNotification(string title, string message, Exception? exception = null, Action? onRetry = null, Action? onViewLog = null)
{
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();
n.IsDismissed = true;
n.IsAnimatingOut = true;
}
});
}
if (onViewLog != null)
{
actions.Add(new LightlessNotificationAction
{
Id = "view_log",
Label = "View Log",
Icon = FontAwesomeIcon.FileAlt,
Color = UIColors.Get("LightlessYellow"),
OnClick = (n) => onViewLog()
});
}
var notification = new LightlessNotification
{
Title = title,
Message = exception != null ? $"{message}\n\nError: {exception.Message}" : message,
Type = NotificationType.Error,
Duration = TimeSpan.FromSeconds(15),
Actions = actions,
SoundEffectId = NotificationSounds.Error
};
if (notification.SoundEffectId.HasValue)
{
PlayNotificationSound(notification.SoundEffectId.Value);
}
Mediator.Publish(new LightlessNotificationMessage(notification));
}
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 completedCount = userDownloads.Count(x => x.progress >= 1.0f);
var totalCount = userDownloads.Count;
var message = "";
if (queueWaiting > 0)
{
message = $"Queue: {queueWaiting} waiting";
}
if (totalCount > 0)
{
var progressMessage = $"Progress: {completedCount}/{totalCount} completed";
message = string.IsNullOrEmpty(message) ? progressMessage : $"{message}\n{progressMessage}";
}
if (userDownloads.Any(x => x.progress < 1.0f))
{
var maxNamesToShow = _configService.Current.MaxConcurrentPairApplications;
var activeDownloads = userDownloads.Where(x => x.progress < 1.0f).Take(maxNamesToShow);
var downloadLines = string.Join("\n", activeDownloads.Select(x =>
{
var statusText = x.status switch
{
"downloading" => $"{x.progress:P0}",
"decompressing" => "decompressing",
"queued" => "queued",
"waiting" => "waiting for slot",
_ => x.status
};
return $"• {x.playerName}: {statusText}";
}));
message += string.IsNullOrEmpty(message) ? downloadLines : $"\n{downloadLines}";
}
var allDownloadsCompleted = userDownloads.All(x => x.progress >= 1.0f) && userDownloads.Any();
var notification = new LightlessNotification
{
Id = "pair_download_progress",
Title = "Downloading Pair Data",
Message = message,
Type = NotificationType.Info,
Duration = TimeSpan.FromMinutes(5),
ShowProgress = true,
Progress = totalProgress
};
Mediator.Publish(new LightlessNotificationMessage(notification));
if (allDownloadsCompleted)
{
DismissPairDownloadNotification();
}
}
private void ShowNotification(NotificationMessage msg)
public void DismissPairDownloadNotification()
{
Logger.LogInformation("{msg}", msg.ToString());
Mediator.Publish(new LightlessNotificationDismissMessage("pair_download_progress"));
}
private uint? GetSoundEffectId(NotificationType type, uint? overrideSoundId)
{
if (!_configService.Current.EnableNotificationSounds)
return null;
if (overrideSoundId.HasValue)
return overrideSoundId;
if (_configService.Current.UseCustomSounds)
{
return type switch
{
NotificationType.Info => _configService.Current.CustomInfoSoundId,
NotificationType.Warning => _configService.Current.CustomWarningSoundId,
NotificationType.Error => _configService.Current.CustomErrorSoundId,
_ => NotificationSounds.GetDefaultSound(type)
};
}
return NotificationSounds.GetDefaultSound(type);
}
private void PlayNotificationSound(uint soundEffectId)
{
try
{
try
{
UIGlobals.PlayChatSoundEffect(soundEffectId);
_logger.LogDebug("Played notification sound effect {SoundId} via ChatGui", soundEffectId);
}
catch (Exception chatEx)
{
_logger.LogWarning(chatEx, "Failed to play sound via ChatGui for ID {SoundId}", 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;
switch (msg.Type)
// Get both old and new notification locations
var oldLocation = msg.Type switch
{
case NotificationType.Info:
ShowNotificationLocationBased(msg, _configurationService.Current.InfoNotification);
break;
NotificationType.Info => _configService.Current.InfoNotification,
NotificationType.Warning => _configService.Current.WarningNotification,
NotificationType.Error => _configService.Current.ErrorNotification,
_ => NotificationLocation.Nowhere
};
case NotificationType.Warning:
ShowNotificationLocationBased(msg, _configurationService.Current.WarningNotification);
break;
var newLocation = msg.Type switch
{
NotificationType.Info => _configService.Current.LightlessInfoNotification,
NotificationType.Warning => _configService.Current.LightlessWarningNotification,
NotificationType.Error => _configService.Current.LightlessErrorNotification,
_ => NotificationLocation.LightlessUI
};
case NotificationType.Error:
ShowNotificationLocationBased(msg, _configurationService.Current.ErrorNotification);
break;
// Show notifications based on system selection with backwards compatibility
if (!_configService.Current.UseLightlessNotifications)
{
// Only use old system when new system is disabled
ShowNotificationLocationBased(msg, oldLocation);
}
else
{
// Use new system as primary
ShowNotificationLocationBased(msg, newLocation);
// Also use old system as fallback for backwards compatibility
// Only if it's different from the new location and not "Nowhere"
if (oldLocation != NotificationLocation.Nowhere &&
oldLocation != newLocation &&
!IsLightlessLocation(oldLocation))
{
ShowNotificationLocationBased(msg, oldLocation);
}
}
}
@@ -114,11 +392,46 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
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 ?? TimeSpan.FromSeconds(_configService.Current.DefaultNotificationDurationSeconds);
uint? soundId = null;
if (_configService.Current.EnableNotificationSounds)
{
if (_configService.Current.UseCustomSounds)
{
soundId = msg.Type switch
{
NotificationType.Info => _configService.Current.CustomInfoSoundId,
NotificationType.Warning => _configService.Current.CustomWarningSoundId,
NotificationType.Error => _configService.Current.CustomErrorSoundId,
_ => NotificationSounds.GetDefaultSound(msg.Type)
};
}
else
{
soundId = NotificationSounds.GetDefaultSound(msg.Type);
}
}
ShowNotification(msg.Title ?? "Lightless Sync", msg.Message ?? string.Empty, msg.Type, duration, null, soundId);
}
private void ShowToast(NotificationMessage msg)
{
Dalamud.Interface.ImGuiNotification.NotificationType dalamudType = msg.Type switch
@@ -138,4 +451,50 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
InitialDuration = msg.TimeShownOnScreen ?? TimeSpan.FromSeconds(3)
});
}
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)
{
SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] Error: " + message);
_chatGui.PrintError(se.BuiltString);
}
private void PrintInfoChat(string? message)
{
SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] Info: ").AddItalics(message ?? string.Empty);
_chatGui.Print(se.BuiltString);
}
private void PrintWarnChat(string? message)
{
SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] ").AddUiForeground("Warning: " + (message ?? string.Empty), 31).AddUiForegroundOff();
_chatGui.Print(se.BuiltString);
}
private bool IsLightlessLocation(NotificationLocation location)
{
return location switch
{
NotificationLocation.LightlessUI => true,
NotificationLocation.ChatAndLightlessUI => true,
_ => false
};
}
}

View File

@@ -1,4 +1,4 @@
using Dalamud.Interface;
using Dalamud.Interface;
using Dalamud.Interface.ImGuiFileDialog;
using Dalamud.Interface.Windowing;
using LightlessSync.LightlessConfiguration;
@@ -23,7 +23,7 @@ public sealed class UiService : DisposableMediatorSubscriberBase
IEnumerable<WindowMediatorSubscriberBase> windows,
UiFactory uiFactory, FileDialogManager fileDialogManager,
LightlessMediator lightlessMediator,
LightlessNotificationService lightlessNotificationService) : base(logger, lightlessMediator)
NotificationService notificationService) : base(logger, lightlessMediator)
{
_logger = logger;
_logger.LogTrace("Creating {type}", GetType().Name);
@@ -41,12 +41,6 @@ public sealed class UiService : DisposableMediatorSubscriberBase
foreach (var window in windows)
{
_windowSystem.AddWindow(window);
// Connect the notification service to the notification UI
if (window is LightlessNotificationUI notificationUI)
{
lightlessNotificationService.SetNotificationUI(notificationUI);
}
}
Mediator.Subscribe<ProfileOpenStandaloneMessage>(this, (msg) =>

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, LightlessNotificationService lightlessNotificationService) : base(logger, mediator, "###LightlessSyncMainUI", performanceCollectorService)
PlayerPerformanceConfigService playerPerformanceConfig, PairRequestService pairRequestService, DalamudUtilService dalamudUtilService, NotificationService lightlessNotificationService) : base(logger, mediator, "###LightlessSyncMainUI", performanceCollectorService)
{
_uiSharedService = uiShared;
_configService = configService;

View File

@@ -1,4 +1,4 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Colors;
using LightlessSync.LightlessConfiguration;
using LightlessSync.PlayerData.Handlers;
@@ -21,12 +21,12 @@ public class DownloadUi : WindowMediatorSubscriberBase
private readonly UiSharedService _uiShared;
private readonly PairProcessingLimiter _pairProcessingLimiter;
private readonly ConcurrentDictionary<GameObjectHandler, bool> _uploadingPlayers = new();
private readonly LightlessNotificationService _notificationService;
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, LightlessNotificationService notificationService)
PerformanceCollectorService performanceCollectorService, NotificationService notificationService)
: base(logger, mediator, "Lightless Sync Downloads", performanceCollectorService)
{
_dalamudUtilService = dalamudUtilService;

View File

@@ -61,7 +61,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
private readonly IProgress<(int, int, FileCacheEntity)> _validationProgress;
private readonly NameplateService _nameplateService;
private readonly NameplateHandler _nameplateHandler;
private readonly LightlessNotificationService _lightlessNotificationService;
private readonly NotificationService _lightlessNotificationService;
private (int, int, FileCacheEntity) _currentProgress;
private bool _deleteAccountPopupModalShown = false;
private bool _deleteFilesPopupModalShown = false;
@@ -107,7 +107,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
DalamudUtilService dalamudUtilService, HttpClient httpClient,
NameplateService nameplateService,
NameplateHandler nameplateHandler,
LightlessNotificationService lightlessNotificationService) : base(logger, mediator, "Lightless Sync Settings", performanceCollector)
NotificationService lightlessNotificationService) : base(logger, mediator, "Lightless Sync Settings", performanceCollector)
{
_configService = configService;
_pairManager = pairManager;

View File

@@ -29,14 +29,14 @@ public class TopTabMenu
private bool _pairRequestsExpanded; // useless for now
private int _lastRequestCount;
private readonly UiSharedService _uiSharedService;
private readonly LightlessNotificationService _lightlessNotificationService;
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, LightlessNotificationService lightlessNotificationService)
public TopTabMenu(LightlessMediator lightlessMediator, ApiController apiController, PairManager pairManager, UiSharedService uiSharedService, PairRequestService pairRequestService, DalamudUtilService dalamudUtilService, NotificationService lightlessNotificationService)
{
_lightlessMediator = lightlessMediator;
_apiController = apiController;

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,7 +32,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
private readonly ServerConfigurationManager _serverManager;
private readonly TokenProvider _tokenProvider;
private readonly LightlessConfigService _lightlessConfigService;
private readonly LightlessNotificationService _lightlessNotificationService;
private readonly NotificationService _lightlessNotificationService;
private CancellationTokenSource _connectionCancellationTokenSource;
private ConnectionDto? _connectionDto;
private bool _doNotNotifyOnNextInfo = false;
@@ -45,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, LightlessNotificationService lightlessNotificationService) : base(logger, mediator)
TokenProvider tokenProvider, LightlessConfigService lightlessConfigService, NotificationService lightlessNotificationService) : base(logger, mediator)
{
_hubFactory = hubFactory;
_dalamudUtil = dalamudUtil;