notifications improvement, working pairs incoming request feature and working user logging in notif

This commit is contained in:
choco
2025-10-06 20:25:47 +02:00
parent 090b81c989
commit 83e4555e4b
7 changed files with 176 additions and 49 deletions

View File

@@ -25,6 +25,7 @@ public sealed class PairManager : DisposableMediatorSubscriberBase
private readonly LightlessConfigService _configurationService; private readonly LightlessConfigService _configurationService;
private readonly IContextMenu _dalamudContextMenu; private readonly IContextMenu _dalamudContextMenu;
private readonly PairFactory _pairFactory; private readonly PairFactory _pairFactory;
private readonly LightlessNotificationService _lightlessNotificationService;
private Lazy<List<Pair>> _directPairsInternal; private Lazy<List<Pair>> _directPairsInternal;
private Lazy<Dictionary<GroupFullInfoDto, List<Pair>>> _groupPairsInternal; private Lazy<Dictionary<GroupFullInfoDto, List<Pair>>> _groupPairsInternal;
private Lazy<Dictionary<Pair, List<GroupFullInfoDto>>> _pairsWithGroupsInternal; private Lazy<Dictionary<Pair, List<GroupFullInfoDto>>> _pairsWithGroupsInternal;
@@ -35,12 +36,14 @@ public sealed class PairManager : DisposableMediatorSubscriberBase
public PairManager(ILogger<PairManager> logger, PairFactory pairFactory, public PairManager(ILogger<PairManager> logger, PairFactory pairFactory,
LightlessConfigService configurationService, LightlessMediator mediator, LightlessConfigService configurationService, LightlessMediator mediator,
IContextMenu dalamudContextMenu, PairProcessingLimiter pairProcessingLimiter) : base(logger, mediator) IContextMenu dalamudContextMenu, PairProcessingLimiter pairProcessingLimiter,
LightlessNotificationService lightlessNotificationService) : base(logger, mediator)
{ {
_pairFactory = pairFactory; _pairFactory = pairFactory;
_configurationService = configurationService; _configurationService = configurationService;
_dalamudContextMenu = dalamudContextMenu; _dalamudContextMenu = dalamudContextMenu;
_pairProcessingLimiter = pairProcessingLimiter; _pairProcessingLimiter = pairProcessingLimiter;
_lightlessNotificationService = lightlessNotificationService;
Mediator.Subscribe<DisconnectedMessage>(this, (_) => ClearPairs()); Mediator.Subscribe<DisconnectedMessage>(this, (_) => ClearPairs());
Mediator.Subscribe<CutsceneEndMessage>(this, (_) => ReapplyPairData()); Mediator.Subscribe<CutsceneEndMessage>(this, (_) => ReapplyPairData());
_directPairsInternal = DirectPairsLazy(); _directPairsInternal = DirectPairsLazy();
@@ -168,7 +171,7 @@ public sealed class PairManager : DisposableMediatorSubscriberBase
var msg = !string.IsNullOrEmpty(note) var msg = !string.IsNullOrEmpty(note)
? $"{note} ({pair.UserData.AliasOrUID}) is now online" ? $"{note} ({pair.UserData.AliasOrUID}) is now online"
: $"{pair.UserData.AliasOrUID} is now online"; : $"{pair.UserData.AliasOrUID} is now online";
Mediator.Publish(new NotificationMessage("User online", msg, NotificationType.Info, TimeSpan.FromSeconds(5))); _lightlessNotificationService.ShowNotification("User Online", msg, NotificationType.Info, TimeSpan.FromSeconds(5));
} }
QueuePairCreation(pair, dto); QueuePairCreation(pair, dto);

View File

@@ -145,7 +145,7 @@ public sealed class Plugin : IDalamudPlugin
collection.AddSingleton((s) => new DtrEntry(s.GetRequiredService<ILogger<DtrEntry>>(), dtrBar, s.GetRequiredService<LightlessConfigService>(), 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>())); s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<PairManager>(), s.GetRequiredService<ApiController>(), s.GetRequiredService<ServerConfigurationManager>()));
collection.AddSingleton(s => new PairManager(s.GetRequiredService<ILogger<PairManager>>(), s.GetRequiredService<PairFactory>(), collection.AddSingleton(s => new PairManager(s.GetRequiredService<ILogger<PairManager>>(), s.GetRequiredService<PairFactory>(),
s.GetRequiredService<LightlessConfigService>(), s.GetRequiredService<LightlessMediator>(), contextMenu, s.GetRequiredService<PairProcessingLimiter>())); s.GetRequiredService<LightlessConfigService>(), s.GetRequiredService<LightlessMediator>(), contextMenu, s.GetRequiredService<PairProcessingLimiter>(), s.GetRequiredService<LightlessNotificationService>()));
collection.AddSingleton<RedrawManager>(); collection.AddSingleton<RedrawManager>();
collection.AddSingleton<BroadcastService>(); collection.AddSingleton<BroadcastService>();
collection.AddSingleton(addonLifecycle); collection.AddSingleton(addonLifecycle);

View File

@@ -6,7 +6,6 @@ using LightlessSync.UI;
using LightlessSync.UI.Models; using LightlessSync.UI.Models;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Numerics;
namespace LightlessSync.Services; namespace LightlessSync.Services;
public class LightlessNotificationService : DisposableMediatorSubscriberBase, IHostedService public class LightlessNotificationService : DisposableMediatorSubscriberBase, IHostedService
{ {
@@ -37,7 +36,7 @@ public class LightlessNotificationService : DisposableMediatorSubscriberBase, IH
_notificationUI = notificationUI; _notificationUI = notificationUI;
} }
public void ShowNotification(string title, string message, NotificationType type = NotificationType.Info, public void ShowNotification(string title, string message, NotificationType type = NotificationType.Info,
TimeSpan? duration = null, List<LightlessNotificationAction>? actions = null) TimeSpan? duration = null, List<LightlessNotificationAction>? actions = null, uint? soundEffectId = null)
{ {
var notification = new LightlessNotification var notification = new LightlessNotification
{ {
@@ -45,8 +44,16 @@ public class LightlessNotificationService : DisposableMediatorSubscriberBase, IH
Message = message, Message = message,
Type = type, Type = type,
Duration = duration ?? TimeSpan.FromSeconds(10), Duration = duration ?? TimeSpan.FromSeconds(10),
Actions = actions ?? new List<LightlessNotificationAction>() Actions = actions ?? new List<LightlessNotificationAction>(),
SoundEffectId = soundEffectId ?? NotificationSounds.GetDefaultSound(type)
}; };
// Play sound effect if specified
if (notification.SoundEffectId.HasValue)
{
PlayNotificationSound(notification.SoundEffectId.Value);
}
Mediator.Publish(new LightlessNotificationMessage(notification)); Mediator.Publish(new LightlessNotificationMessage(notification));
} }
public void ShowPairRequestNotification(string senderName, string senderId, Action onAccept, Action onDecline) public void ShowPairRequestNotification(string senderName, string senderId, Action onAccept, Action onDecline)
@@ -57,6 +64,7 @@ public class LightlessNotificationService : DisposableMediatorSubscriberBase, IH
Message = $"{senderName} wants to pair with you.", Message = $"{senderName} wants to pair with you.",
Type = NotificationType.Info, Type = NotificationType.Info,
Duration = TimeSpan.FromSeconds(60), Duration = TimeSpan.FromSeconds(60),
SoundEffectId = NotificationSounds.PairRequest,
Actions = new List<LightlessNotificationAction> Actions = new List<LightlessNotificationAction>
{ {
new() new()
@@ -91,6 +99,13 @@ public class LightlessNotificationService : DisposableMediatorSubscriberBase, IH
} }
} }
}; };
// Play sound effect
if (notification.SoundEffectId.HasValue)
{
PlayNotificationSound(notification.SoundEffectId.Value);
}
Mediator.Publish(new LightlessNotificationMessage(notification)); Mediator.Publish(new LightlessNotificationMessage(notification));
} }
public void ShowDownloadCompleteNotification(string fileName, int fileCount, Action? onOpenFolder = null) public void ShowDownloadCompleteNotification(string fileName, int fileCount, Action? onOpenFolder = null)
@@ -121,8 +136,16 @@ public class LightlessNotificationService : DisposableMediatorSubscriberBase, IH
$"Downloaded {fileName} successfully.", $"Downloaded {fileName} successfully.",
Type = NotificationType.Info, Type = NotificationType.Info,
Duration = TimeSpan.FromSeconds(8), Duration = TimeSpan.FromSeconds(8),
Actions = actions Actions = actions,
SoundEffectId = NotificationSounds.DownloadComplete
}; };
// Play sound effect
if (notification.SoundEffectId.HasValue)
{
PlayNotificationSound(notification.SoundEffectId.Value);
}
Mediator.Publish(new LightlessNotificationMessage(notification)); Mediator.Publish(new LightlessNotificationMessage(notification));
} }
public void ShowErrorNotification(string title, string message, Exception? exception = null, Action? onRetry = null, Action? onViewLog = null) public void ShowErrorNotification(string title, string message, Exception? exception = null, Action? onRetry = null, Action? onViewLog = null)
@@ -161,21 +184,47 @@ public class LightlessNotificationService : DisposableMediatorSubscriberBase, IH
Message = exception != null ? $"{message}\n\nError: {exception.Message}" : message, Message = exception != null ? $"{message}\n\nError: {exception.Message}" : message,
Type = NotificationType.Error, Type = NotificationType.Error,
Duration = TimeSpan.FromSeconds(15), Duration = TimeSpan.FromSeconds(15),
Actions = actions Actions = actions,
SoundEffectId = NotificationSounds.Error
}; };
// Play sound effect
if (notification.SoundEffectId.HasValue)
{
PlayNotificationSound(notification.SoundEffectId.Value);
}
Mediator.Publish(new LightlessNotificationMessage(notification)); Mediator.Publish(new LightlessNotificationMessage(notification));
} }
public void ShowPairDownloadNotification(List<(string playerName, float progress, string status)> downloadStatus) public void ShowPairDownloadNotification(List<(string playerName, float progress, string status)> downloadStatus, int queueWaiting = 0)
{ {
var totalProgress = downloadStatus.Count > 0 ? downloadStatus.Average(x => x.progress) : 0f; // Filter out queue status from user downloads
var completedCount = downloadStatus.Count(x => x.progress >= 1.0f); var userDownloads = downloadStatus.Where(x => x.playerName != "Pair Queue").ToList();
var totalCount = downloadStatus.Count;
var message = $"Progress: {completedCount}/{totalCount} completed"; var totalProgress = userDownloads.Count > 0 ? userDownloads.Average(x => x.progress) : 0f;
if (downloadStatus.Any(x => x.progress < 1.0f)) var completedCount = userDownloads.Count(x => x.progress >= 1.0f);
var totalCount = userDownloads.Count;
var message = "";
// Add queue status at the top if there are waiting items
if (queueWaiting > 0)
{ {
var activeDownloads = downloadStatus.Where(x => x.progress < 1.0f).Take(3); message = $"Queue: {queueWaiting} waiting";
message += "\n" + string.Join("\n", activeDownloads.Select(x => }
// Add download progress if there are downloads
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 var statusText = x.status switch
{ {
@@ -188,11 +237,12 @@ public class LightlessNotificationService : DisposableMediatorSubscriberBase, IH
return $"• {x.playerName}: {statusText}"; return $"• {x.playerName}: {statusText}";
})); }));
if (downloadStatus.Count(x => x.progress < 1.0f) > 3) message += string.IsNullOrEmpty(message) ? downloadLines : $"\n{downloadLines}";
{
message += $"\n• ... and {downloadStatus.Count(x => x.progress < 1.0f) - 3} more";
}
} }
// Check if all downloads are completed
var allDownloadsCompleted = userDownloads.All(x => x.progress >= 1.0f) && userDownloads.Any();
var notification = new LightlessNotification var notification = new LightlessNotification
{ {
Id = "pair_download_progress", Id = "pair_download_progress",
@@ -204,9 +254,33 @@ public class LightlessNotificationService : DisposableMediatorSubscriberBase, IH
Progress = totalProgress Progress = totalProgress
}; };
Mediator.Publish(new LightlessNotificationMessage(notification)); Mediator.Publish(new LightlessNotificationMessage(notification));
if (allDownloadsCompleted)
{
DismissPairDownloadNotification();
}
} }
public void DismissPairDownloadNotification() public void DismissPairDownloadNotification()
{ {
Mediator.Publish(new LightlessNotificationDismissMessage("pair_download_progress")); Mediator.Publish(new LightlessNotificationDismissMessage("pair_download_progress"));
} }
private void PlayNotificationSound(uint soundEffectId)
{
try
{
// TODO: Implement proper sound playback
// The ChatGui.PlaySoundEffect method doesn't exist in the current Dalamud API
// For now, just log what sound would be played
_logger.LogDebug("Would play notification sound effect {SoundId}", soundEffectId);
// Future implementation options:
// 1. Use UIModule->PlaySound() with proper unsafe interop
// 2. Use game's sound system through SigScanner
// 3. Wait for official Dalamud sound API
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to play notification sound effect {SoundId}", soundEffectId);
}
}
} }

View File

@@ -1,5 +1,4 @@
using System; using Dalamud.Bindings.ImGui;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Colors; using Dalamud.Interface.Colors;
using LightlessSync.LightlessConfiguration; using LightlessSync.LightlessConfiguration;
using LightlessSync.PlayerData.Handlers; using LightlessSync.PlayerData.Handlers;
@@ -88,19 +87,7 @@ public class DownloadUi : WindowMediatorSubscriberBase
if (_configService.Current.ShowTransferWindow) if (_configService.Current.ShowTransferWindow)
{ {
var limiterSnapshot = _pairProcessingLimiter.GetSnapshot(); 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 try
{ {
if (_fileTransferManager.IsUploading) if (_fileTransferManager.IsUploading)
@@ -138,18 +125,30 @@ public class DownloadUi : WindowMediatorSubscriberBase
// Use new notification stuff // Use new notification stuff
if (_currentDownloads.Any()) if (_currentDownloads.Any())
{ {
UpdateDownloadNotification(); UpdateDownloadNotification(limiterSnapshot);
_notificationDismissed = false; _notificationDismissed = false;
} }
else if (!_notificationDismissed) else if (!_notificationDismissed)
{ {
_notificationService.DismissPairDownloadNotification(); _notificationService.DismissPairDownloadNotification();
_notificationDismissed = true; _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 else
{ {
// text overlay UiSharedService.DrawOutlinedFont("Pair apply limiter disabled", ImGuiColors.DalamudGrey, new Vector4(0, 0, 0, 255), 1);
ImGui.NewLine();
}
foreach (var item in _currentDownloads.ToList()) foreach (var item in _currentDownloads.ToList())
{ {
var dlSlot = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.WaitingForSlot); var dlSlot = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.WaitingForSlot);
@@ -292,7 +291,7 @@ public class DownloadUi : WindowMediatorSubscriberBase
}; };
} }
private void UpdateDownloadNotification() private void UpdateDownloadNotification(PairProcessingLimiterSnapshot limiterSnapshot)
{ {
var downloadStatus = new List<(string playerName, float progress, string status)>(); var downloadStatus = new List<(string playerName, float progress, string status)>();
@@ -319,9 +318,12 @@ public class DownloadUi : WindowMediatorSubscriberBase
downloadStatus.Add((item.Key.Name, progress, status)); downloadStatus.Add((item.Key.Name, progress, status));
} }
if (downloadStatus.Any()) // 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); _notificationService.ShowPairDownloadNotification(downloadStatus, queueWaiting);
} }
} }
} }

View File

@@ -1,4 +1,4 @@
using Dalamud.Interface; using Dalamud.Interface;
using LightlessSync.LightlessConfiguration.Models; using LightlessSync.LightlessConfiguration.Models;
using System.Numerics; using System.Numerics;
namespace LightlessSync.UI.Models; namespace LightlessSync.UI.Models;
@@ -21,6 +21,9 @@ public class LightlessNotification
public float AnimationProgress { get; set; } = 0f; public float AnimationProgress { get; set; } = 0f;
public bool IsAnimatingIn { get; set; } = true; public bool IsAnimatingIn { get; set; } = true;
public bool IsAnimatingOut { get; set; } = false; public bool IsAnimatingOut { get; set; } = false;
// Sound properties
public uint? SoundEffectId { get; set; } = null;
} }
public class LightlessNotificationAction public class LightlessNotificationAction
{ {

View File

@@ -0,0 +1,50 @@
using LightlessSync.LightlessConfiguration.Models;
namespace LightlessSync.UI.Models;
/// <summary>
/// Common FFXIV sound effect IDs for notifications
/// </summary>
public static class NotificationSounds
{
/// <summary>
/// General notification sound (quest complete)
/// </summary>
public const uint Info = 37;
/// <summary>
/// Warning/alert sound (system error)
/// </summary>
public const uint Warning = 15;
/// <summary>
/// Error sound (action failed)
/// </summary>
public const uint Error = 16;
/// <summary>
/// Success sound (level up)
/// </summary>
public const uint Success = 25;
/// <summary>
/// Pair request sound (tell received)
/// </summary>
public const uint PairRequest = 13;
/// <summary>
/// Download complete sound (item obtained)
/// </summary>
public const uint DownloadComplete = 30;
/// <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
};
}

View File

@@ -11,12 +11,8 @@ using LightlessSync.Services;
using LightlessSync.Services.Mediator; using LightlessSync.Services.Mediator;
using LightlessSync.Utils; using LightlessSync.Utils;
using LightlessSync.WebAPI; using LightlessSync.WebAPI;
using Serilog;
using System;
using System.Collections.Generic;
using System.Numerics; using System.Numerics;
using System.Reflection.Emit;
using System.Threading.Tasks;
namespace LightlessSync.UI; namespace LightlessSync.UI;
@@ -269,7 +265,6 @@ public class TopTabMenu
_lightlessNotificationService.ShowPairDownloadNotification(downloadStatus); _lightlessNotificationService.ShowPairDownloadNotification(downloadStatus);
} }
ImGui.SameLine(); ImGui.SameLine();
if (ImGui.Button("Dismiss Download")) if (ImGui.Button("Dismiss Download"))
{ {