Merge pull request 'Merge master to profiles' (#61) from master into syncshell-profiles
Reviewed-on: #61
This commit was merged in pull request #61.
This commit is contained in:
@@ -211,6 +211,16 @@ public class BroadcastScannerService : DisposableMediatorSubscriberBase, IDispos
|
||||
UpdateSyncshellBroadcasts();
|
||||
}
|
||||
|
||||
public int CountActiveBroadcasts(string? excludeHashedCid = null)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var comparer = StringComparer.Ordinal;
|
||||
return _broadcastCache.Count(entry =>
|
||||
entry.Value.IsBroadcasting &&
|
||||
entry.Value.ExpiryTime > now &&
|
||||
(excludeHashedCid is null || !comparer.Equals(entry.Key, excludeHashedCid)));
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
@@ -7,6 +7,7 @@ using LightlessSync.WebAPI;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Threading;
|
||||
|
||||
namespace LightlessSync.Services;
|
||||
public class BroadcastService : IHostedService, IMediatorSubscriber
|
||||
@@ -16,9 +17,11 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
|
||||
private readonly LightlessMediator _mediator;
|
||||
private readonly LightlessConfigService _config;
|
||||
private readonly DalamudUtilService _dalamudUtil;
|
||||
private CancellationTokenSource? _lightfinderCancelTokens;
|
||||
private Action? _connectedHandler;
|
||||
public LightlessMediator Mediator => _mediator;
|
||||
|
||||
public bool IsLightFinderAvailable { get; private set; } = true;
|
||||
public bool IsLightFinderAvailable { get; private set; } = false;
|
||||
|
||||
public bool IsBroadcasting => _config.Current.BroadcastEnabled;
|
||||
private bool _syncedOnStartup = false;
|
||||
@@ -57,24 +60,125 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
|
||||
await action().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
private async Task<string?> GetLocalHashedCidAsync(string context)
|
||||
{
|
||||
try
|
||||
{
|
||||
var cid = await _dalamudUtil.GetCIDAsync().ConfigureAwait(false);
|
||||
return cid.ToString().GetHash256();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to resolve CID for {Context}", context);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyBroadcastDisabled(bool forcePublish = false)
|
||||
{
|
||||
bool wasEnabled = _config.Current.BroadcastEnabled;
|
||||
bool hadExpiry = _config.Current.BroadcastTtl != DateTime.MinValue;
|
||||
bool hadRemaining = _remainingTtl.HasValue;
|
||||
|
||||
_config.Current.BroadcastEnabled = false;
|
||||
_config.Current.BroadcastTtl = DateTime.MinValue;
|
||||
|
||||
if (wasEnabled || hadExpiry)
|
||||
_config.Save();
|
||||
|
||||
_remainingTtl = null;
|
||||
_waitingForTtlFetch = false;
|
||||
_syncedOnStartup = false;
|
||||
|
||||
if (forcePublish || wasEnabled || hadRemaining)
|
||||
_mediator.Publish(new BroadcastStatusChangedMessage(false, null));
|
||||
}
|
||||
|
||||
private bool TryApplyBroadcastEnabled(TimeSpan? ttl, string context)
|
||||
{
|
||||
if (ttl is not { } validTtl || validTtl <= TimeSpan.Zero)
|
||||
{
|
||||
_logger.LogWarning("Lightfinder enable skipped ({Context}): invalid TTL ({TTL})", context, ttl);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool wasEnabled = _config.Current.BroadcastEnabled;
|
||||
TimeSpan? previousRemaining = _remainingTtl;
|
||||
DateTime previousExpiry = _config.Current.BroadcastTtl;
|
||||
|
||||
var newExpiry = DateTime.UtcNow + validTtl;
|
||||
|
||||
_config.Current.BroadcastEnabled = true;
|
||||
_config.Current.BroadcastTtl = newExpiry;
|
||||
|
||||
if (!wasEnabled || previousExpiry != newExpiry)
|
||||
_config.Save();
|
||||
|
||||
_remainingTtl = validTtl;
|
||||
_waitingForTtlFetch = false;
|
||||
|
||||
if (!wasEnabled || previousRemaining != validTtl)
|
||||
_mediator.Publish(new BroadcastStatusChangedMessage(true, validTtl));
|
||||
|
||||
_logger.LogInformation("Lightfinder broadcast enabled ({Context}), TTL: {TTL}", context, validTtl);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void HandleLightfinderUnavailable(string message, Exception? ex = null)
|
||||
{
|
||||
if (ex != null)
|
||||
_logger.LogWarning(ex, message);
|
||||
else
|
||||
_logger.LogWarning(message);
|
||||
|
||||
IsLightFinderAvailable = false;
|
||||
ApplyBroadcastDisabled(forcePublish: true);
|
||||
}
|
||||
|
||||
private void OnDisconnected()
|
||||
{
|
||||
IsLightFinderAvailable = false;
|
||||
ApplyBroadcastDisabled(forcePublish: true);
|
||||
_logger.LogDebug("Cleared Lightfinder state due to disconnect.");
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_mediator.Subscribe<EnableBroadcastMessage>(this, OnEnableBroadcast);
|
||||
_mediator.Subscribe<BroadcastStatusChangedMessage>(this, OnBroadcastStatusChanged);
|
||||
_mediator.Subscribe<PriorityFrameworkUpdateMessage>(this, OnTick);
|
||||
_mediator.Subscribe<DisconnectedMessage>(this, _ => OnDisconnected());
|
||||
|
||||
_apiController.OnConnected += () => _ = CheckLightfinderSupportAsync(cancellationToken);
|
||||
//_ = CheckLightfinderSupportAsync(cancellationToken);
|
||||
IsLightFinderAvailable = false;
|
||||
|
||||
_lightfinderCancelTokens?.Cancel();
|
||||
_lightfinderCancelTokens?.Dispose();
|
||||
_lightfinderCancelTokens = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
_connectedHandler = () => _ = CheckLightfinderSupportAsync(_lightfinderCancelTokens.Token);
|
||||
_apiController.OnConnected += _connectedHandler;
|
||||
|
||||
if (_apiController.IsConnected)
|
||||
_ = CheckLightfinderSupportAsync(_lightfinderCancelTokens.Token);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_lightfinderCancelTokens?.Cancel();
|
||||
_lightfinderCancelTokens?.Dispose();
|
||||
_lightfinderCancelTokens = null;
|
||||
|
||||
if (_connectedHandler is not null)
|
||||
{
|
||||
_apiController.OnConnected -= _connectedHandler;
|
||||
_connectedHandler = null;
|
||||
}
|
||||
|
||||
_mediator.UnsubscribeAll(this);
|
||||
_apiController.OnConnected -= () => _ = CheckLightfinderSupportAsync(cancellationToken);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// need to rework this, this is cooked
|
||||
private async Task CheckLightfinderSupportAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
@@ -85,25 +189,54 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
var dummy = "0".PadLeft(64, '0');
|
||||
var hashedCid = await GetLocalHashedCidAsync("Lightfinder state check").ConfigureAwait(false);
|
||||
if (string.IsNullOrEmpty(hashedCid))
|
||||
return;
|
||||
|
||||
await _apiController.IsUserBroadcasting(dummy).ConfigureAwait(false);
|
||||
await _apiController.SetBroadcastStatus(dummy, true, null).ConfigureAwait(false);
|
||||
await _apiController.GetBroadcastTtl(dummy).ConfigureAwait(false);
|
||||
await _apiController.AreUsersBroadcasting([dummy]).ConfigureAwait(false);
|
||||
BroadcastStatusInfoDto? status = null;
|
||||
try
|
||||
{
|
||||
status = await _apiController.IsUserBroadcasting(hashedCid).ConfigureAwait(false);
|
||||
}
|
||||
catch (HubException ex) when (ex.Message.Contains("Method does not exist", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
HandleLightfinderUnavailable("Lightfinder unavailable on server (required method missing).", ex);
|
||||
}
|
||||
|
||||
if (!IsLightFinderAvailable)
|
||||
_logger.LogInformation("Lightfinder is available.");
|
||||
|
||||
IsLightFinderAvailable = true;
|
||||
_logger.LogInformation("Lightfinder is available.");
|
||||
}
|
||||
catch (HubException ex) when (ex.Message.Contains("Method does not exist"))
|
||||
{
|
||||
_logger.LogWarning("Lightfinder unavailable: required method missing.");
|
||||
IsLightFinderAvailable = false;
|
||||
|
||||
_config.Current.BroadcastEnabled = false;
|
||||
_config.Current.BroadcastTtl = DateTime.MinValue;
|
||||
_config.Save();
|
||||
_mediator.Publish(new BroadcastStatusChangedMessage(false, null));
|
||||
bool isBroadcasting = status?.IsBroadcasting == true;
|
||||
TimeSpan? ttl = status?.TTL;
|
||||
|
||||
if (isBroadcasting)
|
||||
{
|
||||
if (ttl is not { } remaining || remaining <= TimeSpan.Zero)
|
||||
ttl = await GetBroadcastTtlAsync(hashedCid).ConfigureAwait(false);
|
||||
|
||||
if (TryApplyBroadcastEnabled(ttl, "server handshake"))
|
||||
{
|
||||
_syncedOnStartup = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
isBroadcasting = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isBroadcasting)
|
||||
{
|
||||
ApplyBroadcastDisabled(forcePublish: true);
|
||||
_logger.LogInformation("Lightfinder is available but no active broadcast was found.");
|
||||
}
|
||||
|
||||
if (_config.Current.LightfinderAutoEnableOnConnect && !isBroadcasting)
|
||||
{
|
||||
_logger.LogInformation("Auto-enabling Lightfinder broadcast after reconnect.");
|
||||
_mediator.Publish(new EnableBroadcastMessage(hashedCid, true));
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
@@ -111,14 +244,7 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Lightfinder check failed.");
|
||||
IsLightFinderAvailable = false;
|
||||
|
||||
_config.Current.BroadcastEnabled = false;
|
||||
_config.Current.BroadcastTtl = DateTime.MinValue;
|
||||
_config.Save();
|
||||
|
||||
_mediator.Publish(new BroadcastStatusChangedMessage(false, null));
|
||||
HandleLightfinderUnavailable("Lightfinder check failed.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,46 +265,38 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
|
||||
};
|
||||
}
|
||||
|
||||
await _apiController.SetBroadcastStatus(msg.HashedCid, msg.Enabled, groupDto).ConfigureAwait(false);
|
||||
await _apiController.SetBroadcastStatus(msg.Enabled, groupDto).ConfigureAwait(false);
|
||||
|
||||
_logger.LogDebug("Broadcast {Status} for {Cid}", msg.Enabled ? "enabled" : "disabled", msg.HashedCid);
|
||||
|
||||
if (!msg.Enabled)
|
||||
{
|
||||
_config.Current.BroadcastEnabled = false;
|
||||
_config.Current.BroadcastTtl = DateTime.MinValue;
|
||||
_config.Save();
|
||||
|
||||
_mediator.Publish(new BroadcastStatusChangedMessage(false, null));
|
||||
ApplyBroadcastDisabled(forcePublish: true);
|
||||
Mediator.Publish(new EventMessage(new Services.Events.Event(nameof(BroadcastService), Services.Events.EventSeverity.Informational, $"Disabled Lightfinder for Player: {msg.HashedCid}")));
|
||||
return;
|
||||
}
|
||||
|
||||
_waitingForTtlFetch = true;
|
||||
|
||||
TimeSpan? ttl = await GetBroadcastTtlAsync(msg.HashedCid).ConfigureAwait(false);
|
||||
|
||||
if (ttl is { } remaining && remaining > TimeSpan.Zero)
|
||||
try
|
||||
{
|
||||
_config.Current.BroadcastTtl = DateTime.UtcNow + remaining;
|
||||
_config.Current.BroadcastEnabled = true;
|
||||
_config.Save();
|
||||
TimeSpan? ttl = await GetBroadcastTtlAsync(msg.HashedCid).ConfigureAwait(false);
|
||||
|
||||
_logger.LogDebug("Fetched TTL from server: {TTL}", remaining);
|
||||
_mediator.Publish(new BroadcastStatusChangedMessage(true, remaining));
|
||||
Mediator.Publish(new EventMessage(new Services.Events.Event(nameof(BroadcastService), Services.Events.EventSeverity.Informational, $"Enabled Lightfinder for Player: {msg.HashedCid}")));
|
||||
if (TryApplyBroadcastEnabled(ttl, "client request"))
|
||||
{
|
||||
_logger.LogDebug("Fetched TTL from server: {TTL}", ttl);
|
||||
Mediator.Publish(new EventMessage(new Services.Events.Event(nameof(BroadcastService), Services.Events.EventSeverity.Informational, $"Enabled Lightfinder for Player: {msg.HashedCid}")));
|
||||
}
|
||||
else
|
||||
{
|
||||
ApplyBroadcastDisabled(forcePublish: true);
|
||||
_logger.LogWarning("No valid TTL returned after enabling broadcast. Disabling.");
|
||||
}
|
||||
}
|
||||
else
|
||||
finally
|
||||
{
|
||||
_logger.LogWarning("No valid TTL returned after enabling broadcast. Disabling.");
|
||||
_config.Current.BroadcastEnabled = false;
|
||||
_config.Current.BroadcastTtl = DateTime.MinValue;
|
||||
_config.Save();
|
||||
|
||||
_mediator.Publish(new BroadcastStatusChangedMessage(false, null));
|
||||
_waitingForTtlFetch = false;
|
||||
}
|
||||
|
||||
_waitingForTtlFetch = false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -219,17 +337,24 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<TimeSpan?> GetBroadcastTtlAsync(string cid)
|
||||
public async Task<TimeSpan?> GetBroadcastTtlAsync(string? cidForLog = null)
|
||||
{
|
||||
TimeSpan? ttl = null;
|
||||
await RequireConnectionAsync(nameof(GetBroadcastTtlAsync), async () => {
|
||||
try
|
||||
{
|
||||
ttl = await _apiController.GetBroadcastTtl(cid).ConfigureAwait(false);
|
||||
ttl = await _apiController.GetBroadcastTtl().ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to fetch broadcast TTL for {cid}", cid);
|
||||
if (cidForLog is { Length: > 0 })
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to fetch broadcast TTL for {Cid}", cidForLog);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to fetch broadcast TTL");
|
||||
}
|
||||
}
|
||||
}).ConfigureAwait(false);
|
||||
return ttl;
|
||||
@@ -281,7 +406,12 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
|
||||
return;
|
||||
}
|
||||
|
||||
var hashedCid = (await _dalamudUtil.GetCIDAsync().ConfigureAwait(false)).ToString().GetHash256();
|
||||
var hashedCid = await GetLocalHashedCidAsync(nameof(ToggleBroadcast)).ConfigureAwait(false);
|
||||
if (string.IsNullOrEmpty(hashedCid))
|
||||
{
|
||||
_logger.LogWarning("ToggleBroadcast - unable to resolve CID.");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
@@ -321,31 +451,31 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
|
||||
await RequireConnectionAsync(nameof(OnTick), async () => {
|
||||
if (!_syncedOnStartup && _config.Current.BroadcastEnabled)
|
||||
{
|
||||
_syncedOnStartup = true;
|
||||
try
|
||||
{
|
||||
string hashedCid = (await _dalamudUtil.GetCIDAsync().ConfigureAwait(false)).ToString().GetHash256();
|
||||
TimeSpan? ttl = await GetBroadcastTtlAsync(hashedCid).ConfigureAwait(false);
|
||||
if (ttl is { }
|
||||
remaining && remaining > TimeSpan.Zero)
|
||||
var hashedCid = await GetLocalHashedCidAsync("startup TTL refresh").ConfigureAwait(false);
|
||||
if (string.IsNullOrEmpty(hashedCid))
|
||||
{
|
||||
_config.Current.BroadcastTtl = DateTime.UtcNow + remaining;
|
||||
_config.Current.BroadcastEnabled = true;
|
||||
_config.Save();
|
||||
_logger.LogDebug("Refreshed broadcast TTL from server on first OnTick: {TTL}", remaining);
|
||||
_logger.LogDebug("Skipping TTL refresh; hashed CID unavailable.");
|
||||
return;
|
||||
}
|
||||
|
||||
TimeSpan? ttl = await GetBroadcastTtlAsync(hashedCid).ConfigureAwait(false);
|
||||
if (TryApplyBroadcastEnabled(ttl, "startup TTL refresh"))
|
||||
{
|
||||
_syncedOnStartup = true;
|
||||
_logger.LogDebug("Refreshed broadcast TTL from server on first OnTick: {TTL}", ttl);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("No valid TTL found on OnTick. Disabling broadcast state.");
|
||||
_config.Current.BroadcastEnabled = false;
|
||||
_config.Current.BroadcastTtl = DateTime.MinValue;
|
||||
_config.Save();
|
||||
_mediator.Publish(new BroadcastStatusChangedMessage(false, null));
|
||||
ApplyBroadcastDisabled(forcePublish: true);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to refresh TTL in OnTick");
|
||||
_syncedOnStartup = false;
|
||||
}
|
||||
}
|
||||
if (_config.Current.BroadcastEnabled)
|
||||
@@ -362,10 +492,7 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
|
||||
if (_remainingTtl == null)
|
||||
{
|
||||
_logger.LogDebug("Broadcast TTL expired. Disabling broadcast locally.");
|
||||
_config.Current.BroadcastEnabled = false;
|
||||
_config.Current.BroadcastTtl = DateTime.MinValue;
|
||||
_config.Save();
|
||||
_mediator.Publish(new BroadcastStatusChangedMessage(false, null));
|
||||
ApplyBroadcastDisabled(forcePublish: true);
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
@@ -43,7 +43,7 @@ public sealed class CommandManagerService : IDisposable
|
||||
"\t /light gpose - Opens the Lightless Character Data Hub window" + Environment.NewLine +
|
||||
"\t /light analyze - Opens the Lightless Character Data Analysis window" + Environment.NewLine +
|
||||
"\t /light settings - Opens the Lightless Settings window" + Environment.NewLine +
|
||||
"\t /light lightfinder - Opens the Lightfinder window"
|
||||
"\t /light finder - Opens the Lightfinder window"
|
||||
});
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ public sealed class CommandManagerService : IDisposable
|
||||
{
|
||||
_mediator.Publish(new UiToggleMessage(typeof(SettingsUi)));
|
||||
}
|
||||
else if (string.Equals(splitArgs[0], "lightfinder", StringComparison.OrdinalIgnoreCase))
|
||||
else if (string.Equals(splitArgs[0], "finder", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_mediator.Publish(new UiToggleMessage(typeof(BroadcastUI)));
|
||||
}
|
||||
|
||||
@@ -147,7 +147,7 @@ internal class ContextMenuService : IHostedService
|
||||
var receiverCid = DalamudUtilService.GetHashedCIDFromPlayerPointer(targetData.Address);
|
||||
|
||||
_logger.LogInformation("Sending pair request: sender {SenderCid}, receiver {ReceiverCid}", senderCid, receiverCid);
|
||||
await _apiController.TryPairWithContentId(receiverCid, senderCid).ConfigureAwait(false);
|
||||
await _apiController.TryPairWithContentId(receiverCid).ConfigureAwait(false);
|
||||
if (!string.IsNullOrWhiteSpace(receiverCid))
|
||||
{
|
||||
_pairRequestService.RemoveRequest(receiverCid);
|
||||
|
||||
@@ -17,6 +17,7 @@ namespace LightlessSync.Services.Mediator;
|
||||
public record SwitchToIntroUiMessage : MessageBase;
|
||||
public record SwitchToMainUiMessage : MessageBase;
|
||||
public record OpenSettingsUiMessage : MessageBase;
|
||||
public record OpenLightfinderSettingsMessage : MessageBase;
|
||||
public record DalamudLoginMessage : MessageBase;
|
||||
public record DalamudLogoutMessage : MessageBase;
|
||||
public record PriorityFrameworkUpdateMessage : SameThreadMessage;
|
||||
@@ -53,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;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Dalamud.Interface.Windowing;
|
||||
using LightlessSync.UI.Style;
|
||||
using Dalamud.Interface.Windowing;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LightlessSync.Services.Mediator;
|
||||
@@ -34,18 +33,6 @@ public abstract class WindowMediatorSubscriberBase : Window, IMediatorSubscriber
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
public override void PreDraw()
|
||||
{
|
||||
base.PreDraw();
|
||||
MainStyle.PushStyle(); // internally checks ShouldUseTheme
|
||||
}
|
||||
|
||||
public override void PostDraw()
|
||||
{
|
||||
MainStyle.PopStyle(); // always attempts to pop if pushed
|
||||
base.PostDraw();
|
||||
}
|
||||
|
||||
public override void Draw()
|
||||
{
|
||||
_performanceCollectorService.LogPerformance(this, $"Draw", DrawInternal);
|
||||
|
||||
@@ -221,6 +221,9 @@ public unsafe class NameplateHandler : IMediatorSubscriber
|
||||
if (pNode == null)
|
||||
continue;
|
||||
|
||||
if (mpNameplateAddon == null)
|
||||
continue;
|
||||
|
||||
var cid = DalamudUtilService.GetHashedCIDFromPlayerPointer((nint)objectInfo->GameObject);
|
||||
|
||||
if (cid == null || !_activeBroadcastingCids.Contains(cid))
|
||||
@@ -247,9 +250,17 @@ public unsafe class NameplateHandler : IMediatorSubscriber
|
||||
var pNameplateIconNode = nameplateObject.MarkerIcon;
|
||||
var pNameplateResNode = nameplateObject.NameContainer;
|
||||
var pNameplateTextNode = nameplateObject.NameText;
|
||||
bool IsVisible = pNameplateIconNode->AtkResNode.IsVisible() || (pNameplateResNode->IsVisible() && pNameplateTextNode->AtkResNode.IsVisible());
|
||||
bool IsVisible = pNameplateIconNode->AtkResNode.IsVisible() || (pNameplateResNode->IsVisible() && pNameplateTextNode->AtkResNode.IsVisible()) || _configService.Current.LightfinderLabelShowHidden;
|
||||
pNode->AtkResNode.ToggleVisibility(IsVisible);
|
||||
|
||||
if (nameplateObject.RootComponentNode == null ||
|
||||
nameplateObject.NameContainer == null ||
|
||||
nameplateObject.NameText == null)
|
||||
{
|
||||
pNode->AtkResNode.ToggleVisibility(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
var nameContainer = nameplateObject.NameContainer;
|
||||
var nameText = nameplateObject.NameText;
|
||||
|
||||
@@ -259,8 +270,8 @@ public unsafe class NameplateHandler : IMediatorSubscriber
|
||||
continue;
|
||||
}
|
||||
|
||||
var labelColor = UIColors.Get("LightlessPurple");
|
||||
var edgeColor = UIColors.Get("FullBlack");
|
||||
var labelColor = UIColors.Get("Lightfinder");
|
||||
var edgeColor = UIColors.Get("LightfinderEdge");
|
||||
var config = _configService.Current;
|
||||
|
||||
var scaleMultiplier = System.Math.Clamp(config.LightfinderLabelScale, 0.5f, 2.0f);
|
||||
@@ -360,33 +371,35 @@ public unsafe class NameplateHandler : IMediatorSubscriber
|
||||
}
|
||||
int positionX;
|
||||
|
||||
|
||||
if (!config.LightfinderLabelUseIcon && (string.IsNullOrWhiteSpace(labelContent) || string.Equals(labelContent, "-", StringComparison.Ordinal)))
|
||||
labelContent = DefaultLabelText;
|
||||
|
||||
pNode->FontType = config.LightfinderLabelUseIcon ? FontType.Axis : FontType.MiedingerMed;
|
||||
|
||||
pNode->SetText(labelContent);
|
||||
|
||||
if (!config.LightfinderLabelUseIcon)
|
||||
{
|
||||
pNode->TextFlags &= ~TextFlags.AutoAdjustNodeSize;
|
||||
pNode->AtkResNode.Width = 0;
|
||||
nodeWidth = (int)pNode->AtkResNode.GetWidth();
|
||||
if (nodeWidth <= 0)
|
||||
nodeWidth = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeWidth * effectiveScale);
|
||||
pNode->AtkResNode.Width = (ushort)nodeWidth;
|
||||
}
|
||||
else
|
||||
{
|
||||
pNode->TextFlags |= TextFlags.AutoAdjustNodeSize;
|
||||
pNode->AtkResNode.Width = 0;
|
||||
nodeWidth = pNode->AtkResNode.GetWidth();
|
||||
}
|
||||
|
||||
|
||||
if (config.LightfinderAutoAlign && nameContainer != null && hasValidOffset)
|
||||
{
|
||||
var nameplateWidth = (int)nameContainer->Width;
|
||||
|
||||
if (!config.LightfinderLabelUseIcon)
|
||||
{
|
||||
pNode->TextFlags &= ~TextFlags.AutoAdjustNodeSize;
|
||||
pNode->AtkResNode.Width = 0;
|
||||
pNode->SetText(labelContent);
|
||||
|
||||
nodeWidth = (int)pNode->AtkResNode.GetWidth();
|
||||
if (nodeWidth <= 0)
|
||||
nodeWidth = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeWidth * effectiveScale);
|
||||
|
||||
if (nodeWidth > nameplateWidth)
|
||||
nodeWidth = nameplateWidth;
|
||||
|
||||
pNode->AtkResNode.Width = (ushort)nodeWidth;
|
||||
}
|
||||
else
|
||||
{
|
||||
pNode->TextFlags |= TextFlags.AutoAdjustNodeSize;
|
||||
pNode->AtkResNode.Width = 0;
|
||||
pNode->SetText(labelContent);
|
||||
nodeWidth = (int)pNode->AtkResNode.GetWidth();
|
||||
}
|
||||
|
||||
int leftPos = nameplateWidth / 8;
|
||||
int rightPos = nameplateWidth - nodeWidth - (nameplateWidth / 8);
|
||||
int centrePos = (nameplateWidth - nodeWidth) / 2;
|
||||
@@ -414,7 +427,7 @@ public unsafe class NameplateHandler : IMediatorSubscriber
|
||||
positionX = 58 + config.LightfinderLabelOffsetX;
|
||||
alignment = AlignmentType.Bottom;
|
||||
}
|
||||
|
||||
|
||||
positionY += config.LightfinderLabelOffsetY;
|
||||
|
||||
alignment = (AlignmentType)System.Math.Clamp((int)alignment, 0, 8);
|
||||
|
||||
@@ -1,62 +1,495 @@
|
||||
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 PairRequestService _pairRequestService;
|
||||
private readonly HashSet<string> _shownPairRequestNotifications = new();
|
||||
|
||||
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,
|
||||
PairRequestService pairRequestService) : base(logger, mediator)
|
||||
{
|
||||
_logger = logger;
|
||||
_configService = configService;
|
||||
_dalamudUtilService = dalamudUtilService;
|
||||
_notificationManager = notificationManager;
|
||||
_chatGui = chatGui;
|
||||
_configurationService = configurationService;
|
||||
_pairRequestService = pairRequestService;
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
Mediator.Subscribe<NotificationMessage>(this, ShowNotification);
|
||||
Mediator.Subscribe<NotificationMessage>(this, HandleNotificationMessage);
|
||||
Mediator.Subscribe<PairRequestsUpdatedMessage>(this, HandlePairRequestsUpdated);
|
||||
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 void PrintErrorChat(string? message)
|
||||
private LightlessNotification CreateNotification(string title, string message, NotificationType type,
|
||||
TimeSpan? duration, List<LightlessNotificationAction>? actions, uint? soundEffectId)
|
||||
{
|
||||
SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] Error: " + message);
|
||||
_chatGui.PrintError(se.BuiltString);
|
||||
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 PrintInfoChat(string? message)
|
||||
private void WrapActionsWithAutoDismiss(LightlessNotification notification)
|
||||
{
|
||||
SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] Info: ").AddItalics(message ?? string.Empty);
|
||||
_chatGui.Print(se.BuiltString);
|
||||
foreach (var action in notification.Actions)
|
||||
{
|
||||
var originalOnClick = action.OnClick;
|
||||
action.OnClick = (n) =>
|
||||
{
|
||||
originalOnClick(n);
|
||||
if (_configService.Current.AutoDismissOnAction)
|
||||
{
|
||||
DismissNotification(n);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private void PrintWarnChat(string? message)
|
||||
private void DismissNotification(LightlessNotification notification)
|
||||
{
|
||||
SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] ").AddUiForeground("Warning: " + (message ?? string.Empty), 31).AddUiForegroundOff();
|
||||
_chatGui.Print(se.BuiltString);
|
||||
notification.IsDismissed = true;
|
||||
notification.IsAnimatingOut = true;
|
||||
}
|
||||
|
||||
public void ShowPairRequestNotification(string senderName, string senderId, Action onAccept, Action onDecline)
|
||||
{
|
||||
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));
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -75,67 +508,54 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowNotification(NotificationMessage msg)
|
||||
private void PrintErrorChat(string? message)
|
||||
{
|
||||
Logger.LogInformation("{msg}", msg.ToString());
|
||||
SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] Error: " + message);
|
||||
_chatGui.PrintError(se.BuiltString);
|
||||
}
|
||||
|
||||
if (!_dalamudUtilService.IsLoggedIn) return;
|
||||
private void PrintInfoChat(string? message)
|
||||
{
|
||||
SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] Info: ")
|
||||
.AddItalics(message ?? string.Empty);
|
||||
_chatGui.Print(se.BuiltString);
|
||||
}
|
||||
|
||||
switch (msg.Type)
|
||||
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 HandlePairRequestsUpdated(PairRequestsUpdatedMessage _)
|
||||
{
|
||||
var activeRequests = _pairRequestService.GetActiveRequests();
|
||||
var activeRequestIds = activeRequests.Select(r => r.HashedCid).ToHashSet();
|
||||
|
||||
// Dismiss notifications for requests that are no longer active
|
||||
var notificationsToRemove = _shownPairRequestNotifications
|
||||
.Where(hashedCid => !activeRequestIds.Contains(hashedCid))
|
||||
.ToList();
|
||||
|
||||
foreach (var hashedCid in notificationsToRemove)
|
||||
{
|
||||
case NotificationType.Info:
|
||||
ShowNotificationLocationBased(msg, _configurationService.Current.InfoNotification);
|
||||
break;
|
||||
var notificationId = $"pair_request_{hashedCid}";
|
||||
Mediator.Publish(new LightlessNotificationDismissMessage(notificationId));
|
||||
_shownPairRequestNotifications.Remove(hashedCid);
|
||||
}
|
||||
|
||||
case NotificationType.Warning:
|
||||
ShowNotificationLocationBased(msg, _configurationService.Current.WarningNotification);
|
||||
break;
|
||||
|
||||
case NotificationType.Error:
|
||||
ShowNotificationLocationBased(msg, _configurationService.Current.ErrorNotification);
|
||||
break;
|
||||
// Show/update notifications for all active requests
|
||||
foreach (var request in activeRequests)
|
||||
{
|
||||
_shownPairRequestNotifications.Add(request.HashedCid);
|
||||
ShowPairRequestNotification(
|
||||
request.DisplayName,
|
||||
request.HashedCid,
|
||||
() => _pairRequestService.AcceptPairRequest(request.HashedCid, request.DisplayName),
|
||||
() => _pairRequestService.DeclinePairRequest(request.HashedCid)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,40 @@ public sealed class PairRequestService : DisposableMediatorSubscriberBase
|
||||
return _requests.RemoveAll(r => now - r.ReceivedAt > Expiration) > 0;
|
||||
}
|
||||
|
||||
public void AcceptPairRequest(string hashedCid, string displayName)
|
||||
{
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await _apiController.Value.TryPairWithContentId(hashedCid).ConfigureAwait(false);
|
||||
RemoveRequest(hashedCid);
|
||||
|
||||
var displayText = string.IsNullOrEmpty(displayName) ? hashedCid : displayName;
|
||||
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);
|
||||
|
||||
@@ -607,8 +607,9 @@ public class ServerConfigurationManager
|
||||
{
|
||||
var baseUri = serverUri.Replace("wss://", "https://").Replace("ws://", "http://");
|
||||
var oauthCheckUri = LightlessAuth.GetUIDsFullPath(new Uri(baseUri));
|
||||
_httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
|
||||
var response = await _httpClient.GetAsync(oauthCheckUri).ConfigureAwait(false);
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, oauthCheckUri);
|
||||
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
|
||||
using var response = await _httpClient.SendAsync(request).ConfigureAwait(false);
|
||||
var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
|
||||
return await JsonSerializer.DeserializeAsync<Dictionary<string, string>>(responseStream).ConfigureAwait(false) ?? [];
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.ImGuiFileDialog;
|
||||
using Dalamud.Interface.Windowing;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.UI;
|
||||
using LightlessSync.UI.Style;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LightlessSync.Services;
|
||||
@@ -22,7 +23,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);
|
||||
@@ -119,7 +121,15 @@ public sealed class UiService : DisposableMediatorSubscriberBase
|
||||
|
||||
private void Draw()
|
||||
{
|
||||
_windowSystem.Draw();
|
||||
_fileDialogManager.Draw();
|
||||
MainStyle.PushStyle();
|
||||
try
|
||||
{
|
||||
_windowSystem.Draw();
|
||||
_fileDialogManager.Draw();
|
||||
}
|
||||
finally
|
||||
{
|
||||
MainStyle.PopStyle();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user