All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m27s
2.0.0 Changes: - Reworked shell finder UI with compact or list view with profile tags showing with the listing, allowing moderators to broadcast the syncshell as well to have it be used more. - Reworked user list in syncshell admin screen to have filter visible and moved away from table to its own thing, allowing to copy uid/note/alias when clicking on the name. - Reworked download bars and download box to make it look more modern, removed the jitter around, so it shouldn't vibrate around much. - Chat has been added to the top menu, working in Zone or in Syncshells to be used there. - Paired system has been revamped to make pausing and unpausing faster, and loading people should be faster as well. - Moved to the internal object table to have faster load times for users; people should load in faster - Compactor is running on a multi-threaded level instead of single-threaded; this should increase the speed of compacting files - Nameplate Service has been reworked so it wouldn't use the nameplate handler anymore. - Files can be resized when downloading to reduce load on users if they aren't compressed. (can be toggled to resize all). - Penumbra Collections are now only made when people are visible, reducing the load on boot-up when having many syncshells in your list. - Lightfinder plates have been moved away from using Nameplates, but will use an overlay. - Main UI has been changed a bit with a gradient, and on hover will glow up now. - Reworked Profile UI for Syncshell and Users to be more user-facing with more customizable items. - Reworked Settings UI to look more modern. - Performance should be better due to new systems that would dispose of the collections and better caching of items. Co-authored-by: defnotken <itsdefnotken@gmail.com> Co-authored-by: azyges <aaaaaa@aaa.aaa> Co-authored-by: choco <choco@patat.nl> Co-authored-by: cake <admin@cakeandbanana.nl> Co-authored-by: Minmoose <KennethBohr@outlook.com> Reviewed-on: #92
783 lines
30 KiB
C#
783 lines
30 KiB
C#
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;
|
|
using LightlessSync.PlayerData.Factories;
|
|
using LightlessSync.PlayerData.Pairs;
|
|
using LightlessSync.PlayerData.Pairs;
|
|
using LightlessSync.Services.Mediator;
|
|
using LightlessSync.UI;
|
|
using LightlessSync.UI.Models;
|
|
using LightlessSync.UI.Services;
|
|
using Microsoft.Extensions.Hosting;
|
|
using Microsoft.Extensions.Logging;
|
|
using FFXIVClientStructs.FFXIV.Client.UI;
|
|
using LightlessSync.API.Data;
|
|
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 PairRequestService _pairRequestService;
|
|
private readonly HashSet<string> _shownPairRequestNotifications = [];
|
|
private readonly PairUiService _pairUiService;
|
|
private readonly PairFactory _pairFactory;
|
|
|
|
public NotificationService(
|
|
ILogger<NotificationService> logger,
|
|
LightlessConfigService configService,
|
|
DalamudUtilService dalamudUtilService,
|
|
INotificationManager notificationManager,
|
|
IChatGui chatGui,
|
|
LightlessMediator mediator,
|
|
PairRequestService pairRequestService,
|
|
PairUiService pairUiService,
|
|
PairFactory pairFactory) : base(logger, mediator)
|
|
{
|
|
_logger = logger;
|
|
_configService = configService;
|
|
_dalamudUtilService = dalamudUtilService;
|
|
_notificationManager = notificationManager;
|
|
_chatGui = chatGui;
|
|
_pairRequestService = pairRequestService;
|
|
_pairUiService = pairUiService;
|
|
_pairFactory = pairFactory;
|
|
}
|
|
|
|
public Task StartAsync(CancellationToken cancellationToken)
|
|
{
|
|
Mediator.Subscribe<NotificationMessage>(this, HandleNotificationMessage);
|
|
Mediator.Subscribe<PairRequestReceivedMessage>(this, HandlePairRequestReceived);
|
|
Mediator.Subscribe<PairRequestsUpdatedMessage>(this, HandlePairRequestsUpdated);
|
|
Mediator.Subscribe<PairDownloadStatusMessage>(this, HandlePairDownloadStatus);
|
|
Mediator.Subscribe<PerformanceNotificationMessage>(this, HandlePerformanceNotification);
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
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)
|
|
{
|
|
var notification = CreateNotification(title, message, type, duration, actions, soundEffectId);
|
|
|
|
if (_configService.Current.AutoDismissOnAction && notification.Actions.Count != 0)
|
|
{
|
|
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 static void DismissNotification(LightlessNotification notification)
|
|
{
|
|
notification.IsDismissed = true;
|
|
notification.IsAnimatingOut = true;
|
|
}
|
|
|
|
public void ShowPairRequestNotification(string senderName, string senderId, Action onAccept, Action onDecline)
|
|
{
|
|
var location = GetNotificationLocation(NotificationType.PairRequest);
|
|
|
|
// Show in chat if configured
|
|
if (location == NotificationLocation.Chat || location == NotificationLocation.ChatAndLightlessUi)
|
|
{
|
|
ShowChat(new NotificationMessage("Pair Request Received", $"{senderName} wants to directly pair with you.", NotificationType.PairRequest));
|
|
}
|
|
|
|
// Show Lightless notification if configured and action buttons are enabled
|
|
if ((location == NotificationLocation.LightlessUi || location == NotificationLocation.ChatAndLightlessUi)
|
|
&& _configService.Current.UseLightlessNotifications
|
|
&& _configService.Current.ShowPairRequestNotificationActions)
|
|
{
|
|
var notification = new LightlessNotification
|
|
{
|
|
Id = $"pair_request_{senderId}",
|
|
Title = "Pair Request Received",
|
|
Message = $"{senderName} wants to directly pair with you.",
|
|
Type = NotificationType.PairRequest,
|
|
Duration = TimeSpan.FromSeconds(_configService.Current.PairRequestDurationSeconds),
|
|
SoundEffectId = GetPairRequestSoundId(),
|
|
Actions = CreatePairRequestActions(onAccept, onDecline)
|
|
};
|
|
|
|
if (notification.SoundEffectId.HasValue)
|
|
{
|
|
PlayNotificationSound(notification.SoundEffectId.Value);
|
|
}
|
|
|
|
Mediator.Publish(new LightlessNotificationMessage(notification));
|
|
}
|
|
else if (location != NotificationLocation.Nowhere && location != NotificationLocation.Chat)
|
|
{
|
|
// Fall back to regular notification without action buttons
|
|
HandleNotificationMessage(new NotificationMessage("Pair Request Received", $"{senderName} wants to directly pair with you.", NotificationType.PairRequest));
|
|
}
|
|
}
|
|
|
|
private uint? GetPairRequestSoundId() =>
|
|
!_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 static string FormatDownloadCompleteMessage(string fileName, int fileCount)
|
|
{
|
|
return 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 static string FormatErrorMessage(string message, Exception? exception)
|
|
{
|
|
return 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;
|
|
}
|
|
|
|
|
|
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 static string FormatDownloadStatus((string PlayerName, float Progress, string Status) download)
|
|
{
|
|
return download.Status switch
|
|
{
|
|
"downloading" => $"{download.Progress:P0}",
|
|
"decompressing" => "decompressing",
|
|
"queued" => "queued",
|
|
"waiting" => "waiting for slot",
|
|
_ => download.Status
|
|
};
|
|
}
|
|
|
|
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),
|
|
NotificationType.Performance => TimeSpan.FromSeconds(_configService.Current.PerformanceNotificationDurationSeconds),
|
|
_ => 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.Performance => _configService.Current.DisablePerformanceSound,
|
|
NotificationType.Download => true, // Download sounds always disabled
|
|
_ => false
|
|
};
|
|
|
|
private uint GetConfiguredSoundForType(NotificationType type) => type switch
|
|
{
|
|
NotificationType.Info => _configService.Current.CustomInfoSoundId,
|
|
NotificationType.Warning => _configService.Current.CustomWarningSoundId,
|
|
NotificationType.Error => _configService.Current.CustomErrorSoundId,
|
|
NotificationType.Performance => _configService.Current.PerformanceSoundId,
|
|
_ => 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 Pair? ResolvePair(UserData userData)
|
|
{
|
|
var snapshot = _pairUiService.GetSnapshot();
|
|
if (snapshot.PairsByUid.TryGetValue(userData.UID, out var pair))
|
|
{
|
|
return pair;
|
|
}
|
|
|
|
var ident = new PairUniqueIdentifier(userData.UID);
|
|
return _pairFactory.Create(ident);
|
|
}
|
|
|
|
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,
|
|
NotificationType.Performance => _configService.Current.LightlessPerformanceNotification,
|
|
_ => 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 static Dalamud.Interface.ImGuiNotification.NotificationType
|
|
ConvertToDalamudNotificationType(NotificationType type)
|
|
{
|
|
return 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;
|
|
|
|
case NotificationType.PairRequest:
|
|
PrintPairRequestChat(msg.Title, msg.Message);
|
|
break;
|
|
|
|
case NotificationType.Performance:
|
|
PrintPerformanceChat(msg.Title, msg.Message);
|
|
break;
|
|
|
|
// Download notifications don't support chat output, will be a giga spam otherwise
|
|
case NotificationType.Download:
|
|
break;
|
|
}
|
|
}
|
|
|
|
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 void PrintPairRequestChat(string? title, string? message)
|
|
{
|
|
SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] ")
|
|
.AddUiForeground("Pair Request: ", 541).AddUiForegroundOff()
|
|
.AddText(title ?? message ?? string.Empty);
|
|
_chatGui.Print(se.BuiltString);
|
|
}
|
|
|
|
private void PrintPerformanceChat(string? title, string? message)
|
|
{
|
|
SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] ")
|
|
.AddUiForeground("Performance: ", 508).AddUiForegroundOff()
|
|
.AddText(title ?? message ?? string.Empty);
|
|
_chatGui.Print(se.BuiltString);
|
|
}
|
|
|
|
private void HandlePairRequestReceived(PairRequestReceivedMessage msg)
|
|
{
|
|
var request = _pairRequestService.RegisterIncomingRequest(msg.HashedCid, msg.Message);
|
|
var senderName = string.IsNullOrEmpty(request.DisplayName) ? "Unknown User" : request.DisplayName;
|
|
|
|
_shownPairRequestNotifications.Add(request.HashedCid);
|
|
ShowPairRequestNotification(
|
|
senderName,
|
|
request.HashedCid,
|
|
onAccept: () => _pairRequestService.AcceptPairRequest(request.HashedCid, senderName),
|
|
onDecline: () => _pairRequestService.DeclinePairRequest(request.HashedCid, senderName));
|
|
}
|
|
|
|
private void HandlePairRequestsUpdated(PairRequestsUpdatedMessage _)
|
|
{
|
|
var activeRequests = _pairRequestService.GetActiveRequests();
|
|
var activeRequestIds = activeRequests.Select(r => r.HashedCid).ToHashSet(StringComparer.Ordinal);
|
|
|
|
// Dismiss notifications for requests that are no longer active (expired)
|
|
var notificationsToRemove = _shownPairRequestNotifications
|
|
.Where(hashedCid => !activeRequestIds.Contains(hashedCid))
|
|
.ToList();
|
|
|
|
foreach (var hashedCid in notificationsToRemove)
|
|
{
|
|
var notificationId = $"pair_request_{hashedCid}";
|
|
Mediator.Publish(new LightlessNotificationDismissMessage(notificationId));
|
|
_shownPairRequestNotifications.Remove(hashedCid);
|
|
}
|
|
}
|
|
|
|
private void HandlePairDownloadStatus(PairDownloadStatusMessage msg)
|
|
{
|
|
var userDownloads = msg.DownloadStatus.Where(x => !string.Equals(x.PlayerName, "Pair Queue", StringComparison.Ordinal)).ToList();
|
|
var totalProgress = userDownloads.Count > 0 ? userDownloads.Average(x => x.Progress) : 0f;
|
|
var message = BuildPairDownloadMessage(userDownloads, msg.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));
|
|
}
|
|
|
|
private void HandlePerformanceNotification(PerformanceNotificationMessage msg)
|
|
{
|
|
var location = GetNotificationLocation(NotificationType.Performance);
|
|
|
|
// Show in chat if configured
|
|
if (location == NotificationLocation.Chat || location == NotificationLocation.ChatAndLightlessUi)
|
|
{
|
|
ShowChat(new NotificationMessage(msg.Title, msg.Message, NotificationType.Performance));
|
|
}
|
|
|
|
// Show Lightless notification if configured and action buttons are enabled
|
|
if ((location == NotificationLocation.LightlessUi || location == NotificationLocation.ChatAndLightlessUi)
|
|
&& _configService.Current.UseLightlessNotifications
|
|
&& _configService.Current.ShowPerformanceNotificationActions)
|
|
{
|
|
var actions = CreatePerformanceActions(msg.UserData, msg.IsPaused, msg.PlayerName);
|
|
var notification = new LightlessNotification
|
|
{
|
|
Title = msg.Title,
|
|
Message = msg.Message,
|
|
Type = NotificationType.Performance,
|
|
Duration = TimeSpan.FromSeconds(_configService.Current.PerformanceNotificationDurationSeconds),
|
|
Actions = actions,
|
|
SoundEffectId = GetSoundEffectId(NotificationType.Performance, null)
|
|
};
|
|
|
|
if (notification.SoundEffectId.HasValue)
|
|
{
|
|
PlayNotificationSound(notification.SoundEffectId.Value);
|
|
}
|
|
|
|
Mediator.Publish(new LightlessNotificationMessage(notification));
|
|
}
|
|
else if (location != NotificationLocation.Nowhere && location != NotificationLocation.Chat)
|
|
{
|
|
// Fall back to regular notification without action buttons
|
|
HandleNotificationMessage(new NotificationMessage(msg.Title, msg.Message, NotificationType.Performance));
|
|
}
|
|
}
|
|
|
|
private List<LightlessNotificationAction> CreatePerformanceActions(UserData userData, bool isPaused, string playerName)
|
|
{
|
|
var actions = new List<LightlessNotificationAction>();
|
|
|
|
if (isPaused)
|
|
{
|
|
actions.Add(new LightlessNotificationAction
|
|
{
|
|
Label = "Unpause",
|
|
Icon = FontAwesomeIcon.Play,
|
|
Color = UIColors.Get("LightlessGreen"),
|
|
IsPrimary = true,
|
|
OnClick = (notification) =>
|
|
{
|
|
try
|
|
{
|
|
var pair = ResolvePair(userData);
|
|
if (pair == null)
|
|
{
|
|
_logger.LogWarning("Cannot cycle pause {uid} because pair is missing", userData.UID);
|
|
throw new InvalidOperationException("Pair not available");
|
|
}
|
|
|
|
Mediator.Publish(new CyclePauseMessage(pair));
|
|
DismissNotification(notification);
|
|
|
|
var displayName = GetUserDisplayName(userData, playerName);
|
|
ShowNotification(
|
|
"Player Unpaused",
|
|
$"Successfully unpaused {displayName}",
|
|
NotificationType.Info,
|
|
TimeSpan.FromSeconds(3));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to unpause player {uid}", userData.UID);
|
|
var displayName = GetUserDisplayName(userData, playerName);
|
|
ShowNotification(
|
|
"Unpause Failed",
|
|
$"Failed to unpause {displayName}",
|
|
NotificationType.Error,
|
|
TimeSpan.FromSeconds(5));
|
|
}
|
|
}
|
|
});
|
|
}
|
|
else
|
|
{
|
|
actions.Add(new LightlessNotificationAction
|
|
{
|
|
Label = "Pause",
|
|
Icon = FontAwesomeIcon.Pause,
|
|
Color = UIColors.Get("LightlessOrange"),
|
|
IsPrimary = true,
|
|
OnClick = (notification) =>
|
|
{
|
|
try
|
|
{
|
|
Mediator.Publish(new PauseMessage(userData));
|
|
DismissNotification(notification);
|
|
|
|
var displayName = GetUserDisplayName(userData, playerName);
|
|
ShowNotification(
|
|
"Player Paused",
|
|
$"Successfully paused {displayName}",
|
|
NotificationType.Info,
|
|
TimeSpan.FromSeconds(3));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to pause player {uid}", userData.UID);
|
|
var displayName = GetUserDisplayName(userData, playerName);
|
|
ShowNotification(
|
|
"Pause Failed",
|
|
$"Failed to pause {displayName}",
|
|
NotificationType.Error,
|
|
TimeSpan.FromSeconds(5));
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Add dismiss button
|
|
actions.Add(new LightlessNotificationAction
|
|
{
|
|
Label = "Dismiss",
|
|
Icon = FontAwesomeIcon.Times,
|
|
Color = UIColors.Get("DimRed"),
|
|
IsPrimary = false,
|
|
OnClick = (notification) =>
|
|
{
|
|
DismissNotification(notification);
|
|
}
|
|
});
|
|
|
|
return actions;
|
|
}
|
|
|
|
private static string GetUserDisplayName(UserData userData, string playerName)
|
|
{
|
|
if (!string.IsNullOrEmpty(userData.Alias) && !string.Equals(userData.Alias, userData.UID, StringComparison.Ordinal))
|
|
{
|
|
return $"{playerName} ({userData.Alias})";
|
|
}
|
|
return $"{playerName} ({userData.UID})";
|
|
}
|
|
} |