Compare commits
4 Commits
620ebf9195
...
2.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
d777dc599f
|
|||
|
e3d9300ca3
|
|||
|
699678f641
|
|||
|
7a391e6253
|
3
.gitignore
vendored
3
.gitignore
vendored
@@ -3,6 +3,7 @@
|
||||
##
|
||||
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
|
||||
.idea
|
||||
qodana.yaml
|
||||
# User-specific files
|
||||
*.rsuser
|
||||
*.suo
|
||||
@@ -13,6 +14,8 @@
|
||||
MareSynchronos/.DS_Store
|
||||
*.zip
|
||||
UmbraServer_extracted/
|
||||
NuGet.config
|
||||
Directory.Build.props
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
2
MareAPI
2
MareAPI
Submodule MareAPI updated: deb911cb0a...d105d20507
@@ -87,6 +87,8 @@ public class MareConfig : IMareConfiguration
|
||||
public bool ExtraChatTags { get; set; } = false;
|
||||
public bool TypingIndicatorShowOnNameplates { get; set; } = true;
|
||||
public bool TypingIndicatorShowOnPartyList { get; set; } = true;
|
||||
public bool TypingIndicatorEnabled { get; set; } = true;
|
||||
public bool TypingIndicatorShowSelf { get; set; } = true;
|
||||
public TypingIndicatorBubbleSize TypingIndicatorBubbleSize { get; set; } = TypingIndicatorBubbleSize.Large;
|
||||
|
||||
public bool MareAPI { get; set; } = true;
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using MareSynchronos.MareConfiguration.Models;
|
||||
|
||||
namespace MareSynchronos.MareConfiguration.Configurations;
|
||||
|
||||
[Serializable]
|
||||
public class NotificationsConfig : IMareConfiguration
|
||||
{
|
||||
public List<StoredNotification> Notifications { get; set; } = new();
|
||||
public int Version { get; set; } = 1;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace MareSynchronos.MareConfiguration.Models;
|
||||
|
||||
[Serializable]
|
||||
public class StoredNotification
|
||||
{
|
||||
public string Category { get; set; } = string.Empty; // name of enum NotificationCategory
|
||||
public string Id { get; set; } = string.Empty;
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string? Description { get; set; }
|
||||
public DateTime CreatedAtUtc { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using MareSynchronos.MareConfiguration.Configurations;
|
||||
|
||||
namespace MareSynchronos.MareConfiguration;
|
||||
|
||||
public class NotificationsConfigService : ConfigurationServiceBase<NotificationsConfig>
|
||||
{
|
||||
public const string ConfigName = "notifications.json";
|
||||
|
||||
public NotificationsConfigService(string configDir) : base(configDir)
|
||||
{
|
||||
}
|
||||
|
||||
public override string ConfigurationName => ConfigName;
|
||||
}
|
||||
@@ -50,6 +50,8 @@
|
||||
<PropertyGroup>
|
||||
<SourceRevisionId>build$([System.DateTime]::UtcNow.ToString("yyyy-MM-ddTHH:mm:ss:fffZ"))</SourceRevisionId>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<DisablePackageVulnerabilityAnalysis>true</DisablePackageVulnerabilityAnalysis>
|
||||
<NoWarn>$(NoWarn);NU1900</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -167,6 +167,7 @@ public sealed class Plugin : IDalamudPlugin
|
||||
collection.AddSingleton((s) => new ServerBlockConfigService(pluginInterface.ConfigDirectory.FullName));
|
||||
collection.AddSingleton((s) => new CharaDataConfigService(pluginInterface.ConfigDirectory.FullName));
|
||||
collection.AddSingleton((s) => new RemoteConfigCacheService(pluginInterface.ConfigDirectory.FullName));
|
||||
collection.AddSingleton((s) => new NotificationsConfigService(pluginInterface.ConfigDirectory.FullName));
|
||||
collection.AddSingleton<IConfigService<IMareConfiguration>>(s => s.GetRequiredService<MareConfigService>());
|
||||
collection.AddSingleton<IConfigService<IMareConfiguration>>(s => s.GetRequiredService<ServerConfigService>());
|
||||
collection.AddSingleton<IConfigService<IMareConfiguration>>(s => s.GetRequiredService<NotesConfigService>());
|
||||
@@ -178,6 +179,7 @@ public sealed class Plugin : IDalamudPlugin
|
||||
collection.AddSingleton<IConfigService<IMareConfiguration>>(s => s.GetRequiredService<ServerBlockConfigService>());
|
||||
collection.AddSingleton<IConfigService<IMareConfiguration>>(s => s.GetRequiredService<CharaDataConfigService>());
|
||||
collection.AddSingleton<IConfigService<IMareConfiguration>>(s => s.GetRequiredService<RemoteConfigCacheService>());
|
||||
collection.AddSingleton<IConfigService<IMareConfiguration>>(s => s.GetRequiredService<NotificationsConfigService>());
|
||||
collection.AddSingleton<ConfigurationMigrator>();
|
||||
collection.AddSingleton<ConfigurationSaveService>();
|
||||
|
||||
|
||||
@@ -73,6 +73,13 @@ public sealed class SyncshellDiscoveryService : IHostedService, IMediatorSubscri
|
||||
}
|
||||
|
||||
public async Task<bool> SetVisibilityAsync(string gid, bool visible, CancellationToken ct)
|
||||
{
|
||||
return await SetVisibilityAsync(gid, visible, null, null, null, null, null, ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<bool> SetVisibilityAsync(string gid, bool visible, int? displayDurationHours,
|
||||
int[]? activeWeekdays, TimeSpan? timeStartLocal, TimeSpan? timeEndLocal, string? timeZone,
|
||||
CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -80,6 +87,11 @@ public sealed class SyncshellDiscoveryService : IHostedService, IMediatorSubscri
|
||||
{
|
||||
GID = gid,
|
||||
AutoDetectVisible = visible,
|
||||
DisplayDurationHours = displayDurationHours,
|
||||
ActiveWeekdays = activeWeekdays,
|
||||
TimeStartLocal = timeStartLocal.HasValue ? new DateTime(timeStartLocal.Value.Ticks).ToString("HH:mm") : null,
|
||||
TimeEndLocal = timeEndLocal.HasValue ? new DateTime(timeEndLocal.Value.Ticks).ToString("HH:mm") : null,
|
||||
TimeZone = timeZone,
|
||||
};
|
||||
var success = await _apiController.SyncshellDiscoverySetVisibility(request).ConfigureAwait(false);
|
||||
if (!success) return false;
|
||||
|
||||
@@ -8,6 +8,7 @@ using Dalamud.Plugin.Services;
|
||||
using MareSynchronos.API.Data;
|
||||
using MareSynchronos.Interop;
|
||||
using MareSynchronos.MareConfiguration;
|
||||
using MareSynchronos.API.Data.Enum;
|
||||
using MareSynchronos.MareConfiguration.Models;
|
||||
using MareSynchronos.PlayerData.Pairs;
|
||||
using MareSynchronos.Services.Mediator;
|
||||
@@ -38,6 +39,7 @@ public class ChatService : DisposableMediatorSubscriberBase
|
||||
private CancellationTokenSource? _typingCts;
|
||||
private bool _isTypingAnnounced;
|
||||
private DateTime _lastTypingSent = DateTime.MinValue;
|
||||
private TypingScope _lastScope = TypingScope.Unknown;
|
||||
private static readonly TimeSpan TypingIdle = TimeSpan.FromSeconds(2);
|
||||
private static readonly TimeSpan TypingResendInterval = TimeSpan.FromMilliseconds(750);
|
||||
|
||||
@@ -79,7 +81,7 @@ public class ChatService : DisposableMediatorSubscriberBase
|
||||
if (_gameChatHooks.IsValueCreated)
|
||||
_gameChatHooks.Value!.Dispose();
|
||||
}
|
||||
public void NotifyTypingKeystroke()
|
||||
public void NotifyTypingKeystroke(TypingScope scope)
|
||||
{
|
||||
lock (_typingLock)
|
||||
{
|
||||
@@ -88,11 +90,12 @@ public class ChatService : DisposableMediatorSubscriberBase
|
||||
{
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try { await _apiController.UserSetTypingState(true).ConfigureAwait(false); }
|
||||
try { await _apiController.UserSetTypingState(true, scope).ConfigureAwait(false); }
|
||||
catch (Exception ex) { _logger.LogDebug(ex, "NotifyTypingKeystroke: failed to send typing=true"); }
|
||||
});
|
||||
_isTypingAnnounced = true;
|
||||
_lastTypingSent = now;
|
||||
_lastScope = scope;
|
||||
}
|
||||
|
||||
_typingCts?.Cancel();
|
||||
@@ -105,7 +108,7 @@ public class ChatService : DisposableMediatorSubscriberBase
|
||||
try
|
||||
{
|
||||
await Task.Delay(TypingIdle, token).ConfigureAwait(false);
|
||||
await _apiController.UserSetTypingState(false).ConfigureAwait(false);
|
||||
await _apiController.UserSetTypingState(false, _lastScope).ConfigureAwait(false);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
@@ -140,7 +143,7 @@ public class ChatService : DisposableMediatorSubscriberBase
|
||||
{
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try { await _apiController.UserSetTypingState(false).ConfigureAwait(false); }
|
||||
try { await _apiController.UserSetTypingState(false, _lastScope).ConfigureAwait(false); }
|
||||
catch (Exception ex) { _logger.LogDebug(ex, "ClearTypingState: failed to send typing=false"); }
|
||||
});
|
||||
_isTypingAnnounced = false;
|
||||
|
||||
@@ -10,6 +10,8 @@ using FFXIVClientStructs.FFXIV.Client.UI.Shell;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MareSynchronos.PlayerData.Pairs;
|
||||
using MareSynchronos.WebAPI;
|
||||
using MareSynchronos.MareConfiguration;
|
||||
using MareSynchronos.API.Data.Enum;
|
||||
|
||||
namespace MareSynchronos.Services;
|
||||
|
||||
@@ -24,16 +26,18 @@ public sealed class ChatTypingDetectionService : IDisposable
|
||||
private readonly ApiController _apiController;
|
||||
private readonly PairManager _pairManager;
|
||||
private readonly IPartyList _partyList;
|
||||
private readonly MareConfigService _configService;
|
||||
|
||||
private string _lastChatText = string.Empty;
|
||||
private bool _isTyping;
|
||||
private bool _notifyingRemote;
|
||||
private bool _serverSupportWarnLogged;
|
||||
private bool _remoteNotificationsEnabled;
|
||||
private bool _subscribed;
|
||||
|
||||
public ChatTypingDetectionService(ILogger<ChatTypingDetectionService> logger, IFramework framework,
|
||||
IClientState clientState, IGameGui gameGui, ChatService chatService, PairManager pairManager, IPartyList partyList,
|
||||
TypingIndicatorStateService typingStateService, ApiController apiController)
|
||||
TypingIndicatorStateService typingStateService, ApiController apiController, MareConfigService configService)
|
||||
{
|
||||
_logger = logger;
|
||||
_framework = framework;
|
||||
@@ -44,17 +48,50 @@ public sealed class ChatTypingDetectionService : IDisposable
|
||||
_partyList = partyList;
|
||||
_typingStateService = typingStateService;
|
||||
_apiController = apiController;
|
||||
_configService = configService;
|
||||
|
||||
_framework.Update += OnFrameworkUpdate;
|
||||
Subscribe();
|
||||
_logger.LogInformation("ChatTypingDetectionService initialized");
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_framework.Update -= OnFrameworkUpdate;
|
||||
Unsubscribe();
|
||||
ResetTypingState();
|
||||
}
|
||||
|
||||
public void SoftRestart()
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("TypingDetection: soft restarting");
|
||||
Unsubscribe();
|
||||
ResetTypingState();
|
||||
_chatService.ClearTypingState();
|
||||
_typingStateService.ClearAll();
|
||||
Subscribe();
|
||||
_logger.LogInformation("TypingDetection: soft restart completed");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "TypingDetection: soft restart failed");
|
||||
}
|
||||
}
|
||||
|
||||
private void Subscribe()
|
||||
{
|
||||
if (_subscribed) return;
|
||||
_framework.Update += OnFrameworkUpdate;
|
||||
_subscribed = true;
|
||||
}
|
||||
|
||||
private void Unsubscribe()
|
||||
{
|
||||
if (!_subscribed) return;
|
||||
_framework.Update -= OnFrameworkUpdate;
|
||||
_subscribed = false;
|
||||
}
|
||||
|
||||
private void OnFrameworkUpdate(IFramework framework)
|
||||
{
|
||||
try
|
||||
@@ -65,6 +102,13 @@ public sealed class ChatTypingDetectionService : IDisposable
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_configService.Current.TypingIndicatorEnabled)
|
||||
{
|
||||
ResetTypingState();
|
||||
_chatService.ClearTypingState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!TryGetChatInput(out var chatText) || string.IsNullOrEmpty(chatText))
|
||||
{
|
||||
ResetTypingState();
|
||||
@@ -89,7 +133,8 @@ public sealed class ChatTypingDetectionService : IDisposable
|
||||
{
|
||||
if (notifyRemote)
|
||||
{
|
||||
_chatService.NotifyTypingKeystroke();
|
||||
var scope = GetCurrentTypingScope();
|
||||
_chatService.NotifyTypingKeystroke(scope);
|
||||
_notifyingRemote = true;
|
||||
}
|
||||
|
||||
@@ -120,6 +165,35 @@ public sealed class ChatTypingDetectionService : IDisposable
|
||||
_typingStateService.SetSelfTypingLocal(false);
|
||||
}
|
||||
|
||||
private unsafe TypingScope GetCurrentTypingScope()
|
||||
{
|
||||
try
|
||||
{
|
||||
var shellModule = RaptureShellModule.Instance();
|
||||
if (shellModule == null)
|
||||
return TypingScope.Unknown;
|
||||
|
||||
var chatType = (XivChatType)shellModule->ChatType;
|
||||
switch (chatType)
|
||||
{
|
||||
case XivChatType.Say:
|
||||
case XivChatType.Shout:
|
||||
case XivChatType.Yell:
|
||||
return TypingScope.Proximity;
|
||||
case XivChatType.Party:
|
||||
return TypingScope.Party;
|
||||
case XivChatType.CrossParty:
|
||||
return TypingScope.CrossParty;
|
||||
default:
|
||||
return TypingScope.Unknown;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return TypingScope.Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsIgnoredCommand(string chatText)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(chatText))
|
||||
@@ -146,6 +220,11 @@ public sealed class ChatTypingDetectionService : IDisposable
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!_configService.Current.TypingIndicatorEnabled)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var supportsTypingState = _apiController.SystemInfoDto.SupportsTypingState;
|
||||
var connected = _apiController.IsConnected;
|
||||
if (!connected || !supportsTypingState)
|
||||
|
||||
@@ -2,29 +2,45 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using MareSynchronos.Services.Mediator;
|
||||
using MareSynchronos.MareConfiguration;
|
||||
using MareSynchronos.MareConfiguration.Configurations;
|
||||
using MareSynchronos.MareConfiguration.Models;
|
||||
|
||||
namespace MareSynchronos.Services.Notifications;
|
||||
|
||||
public enum NotificationCategory
|
||||
{
|
||||
AutoDetect,
|
||||
Syncshell,
|
||||
}
|
||||
|
||||
public sealed record NotificationEntry(NotificationCategory Category, string Id, string Title, string? Description, DateTime CreatedAt)
|
||||
{
|
||||
public static NotificationEntry AutoDetect(string uid, string displayName)
|
||||
=> new(NotificationCategory.AutoDetect, uid, displayName, "Nouvelle demande d'appairage via AutoDetect.", DateTime.UtcNow);
|
||||
|
||||
public static NotificationEntry SyncshellPublic(string gid, string aliasOrGid)
|
||||
=> new(NotificationCategory.Syncshell, gid, $"Syncshell publique: {aliasOrGid}", "La Syncshell est désormais visible via AutoDetect.", DateTime.UtcNow);
|
||||
|
||||
public static NotificationEntry SyncshellNotPublic(string gid, string aliasOrGid)
|
||||
=> new(NotificationCategory.Syncshell, gid, $"Syncshell non publique: {aliasOrGid}", "La Syncshell n'est plus visible via AutoDetect.", DateTime.UtcNow);
|
||||
}
|
||||
|
||||
public sealed class NotificationTracker
|
||||
{
|
||||
private const int MaxStored = 100;
|
||||
|
||||
private readonly MareMediator _mediator;
|
||||
private readonly NotificationsConfigService _configService;
|
||||
private readonly Dictionary<(NotificationCategory Category, string Id), NotificationEntry> _entries = new();
|
||||
private readonly object _lock = new();
|
||||
|
||||
public NotificationTracker(MareMediator mediator)
|
||||
public NotificationTracker(MareMediator mediator, NotificationsConfigService configService)
|
||||
{
|
||||
_mediator = mediator;
|
||||
_configService = configService;
|
||||
LoadPersisted();
|
||||
PublishState();
|
||||
}
|
||||
|
||||
public void Upsert(NotificationEntry entry)
|
||||
@@ -32,6 +48,8 @@ public sealed class NotificationTracker
|
||||
lock (_lock)
|
||||
{
|
||||
_entries[(entry.Category, entry.Id)] = entry;
|
||||
TrimIfNecessary_NoLock();
|
||||
Persist_NoLock();
|
||||
}
|
||||
PublishState();
|
||||
}
|
||||
@@ -41,6 +59,7 @@ public sealed class NotificationTracker
|
||||
lock (_lock)
|
||||
{
|
||||
_entries.Remove((category, id));
|
||||
Persist_NoLock();
|
||||
}
|
||||
PublishState();
|
||||
}
|
||||
@@ -70,4 +89,56 @@ public sealed class NotificationTracker
|
||||
{
|
||||
_mediator.Publish(new NotificationStateChanged(Count));
|
||||
}
|
||||
|
||||
private void LoadPersisted()
|
||||
{
|
||||
try
|
||||
{
|
||||
var list = _configService.Current.Notifications ?? new List<StoredNotification>();
|
||||
foreach (var s in list)
|
||||
{
|
||||
if (!Enum.TryParse<NotificationCategory>(s.Category, out var cat)) continue;
|
||||
var entry = new NotificationEntry(cat, s.Id, s.Title, s.Description, s.CreatedAtUtc);
|
||||
_entries[(entry.Category, entry.Id)] = entry;
|
||||
}
|
||||
TrimIfNecessary_NoLock();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore load errors, start empty
|
||||
}
|
||||
}
|
||||
|
||||
private void Persist_NoLock()
|
||||
{
|
||||
try
|
||||
{
|
||||
var stored = _entries.Values
|
||||
.OrderBy(e => e.CreatedAt)
|
||||
.Select(e => new StoredNotification
|
||||
{
|
||||
Category = e.Category.ToString(),
|
||||
Id = e.Id,
|
||||
Title = e.Title,
|
||||
Description = e.Description,
|
||||
CreatedAtUtc = e.CreatedAt
|
||||
})
|
||||
.ToList();
|
||||
_configService.Current.Notifications = stored;
|
||||
_configService.Save();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore persistence errors
|
||||
}
|
||||
}
|
||||
|
||||
private void TrimIfNecessary_NoLock()
|
||||
{
|
||||
if (_entries.Count <= MaxStored) return;
|
||||
foreach (var kv in _entries.Values.OrderByDescending(v => v.CreatedAt).Skip(MaxStored).ToList())
|
||||
{
|
||||
_entries.Remove((kv.Category, kv.Id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Interface.ImGuiNotification;
|
||||
using Dalamud.Plugin.Services;
|
||||
@@ -17,22 +18,29 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
|
||||
private readonly INotificationManager _notificationManager;
|
||||
private readonly IChatGui _chatGui;
|
||||
private readonly MareConfigService _configurationService;
|
||||
private readonly Services.Notifications.NotificationTracker _notificationTracker;
|
||||
private readonly PlayerData.Pairs.PairManager _pairManager;
|
||||
|
||||
public NotificationService(ILogger<NotificationService> logger, MareMediator mediator,
|
||||
DalamudUtilService dalamudUtilService,
|
||||
INotificationManager notificationManager,
|
||||
IChatGui chatGui, MareConfigService configurationService) : base(logger, mediator)
|
||||
IChatGui chatGui, MareConfigService configurationService,
|
||||
Services.Notifications.NotificationTracker notificationTracker,
|
||||
PlayerData.Pairs.PairManager pairManager) : base(logger, mediator)
|
||||
{
|
||||
_dalamudUtilService = dalamudUtilService;
|
||||
_notificationManager = notificationManager;
|
||||
_chatGui = chatGui;
|
||||
_configurationService = configurationService;
|
||||
_notificationTracker = notificationTracker;
|
||||
_pairManager = pairManager;
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
Mediator.Subscribe<NotificationMessage>(this, ShowNotification);
|
||||
Mediator.Subscribe<DualNotificationMessage>(this, ShowDualNotification);
|
||||
Mediator.Subscribe<Services.Mediator.SyncshellAutoDetectStateChanged>(this, OnSyncshellAutoDetectStateChanged);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
@@ -113,6 +121,31 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
|
||||
ShowChat(baseMsg);
|
||||
}
|
||||
|
||||
private void OnSyncshellAutoDetectStateChanged(SyncshellAutoDetectStateChanged msg)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (msg.Visible) return; // only handle transition to not visible
|
||||
|
||||
var gid = msg.Gid;
|
||||
// Try to resolve alias from PairManager snapshot; fallback to gid
|
||||
var alias = _pairManager.Groups.Values.FirstOrDefault(g => string.Equals(g.GID, gid, StringComparison.OrdinalIgnoreCase))?.GroupAliasOrGID ?? gid;
|
||||
|
||||
var title = $"Syncshell non publique: {alias}";
|
||||
var message = "La Syncshell n'est plus visible via AutoDetect.";
|
||||
|
||||
// Show toast + chat
|
||||
ShowDualNotification(new DualNotificationMessage(title, message, NotificationType.Info, TimeSpan.FromSeconds(4)));
|
||||
|
||||
// Persist into notification center
|
||||
_notificationTracker.Upsert(Services.Notifications.NotificationEntry.SyncshellNotPublic(gid, alias));
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore failures
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ShouldForceChat(NotificationMessage msg, out bool appendInstruction)
|
||||
{
|
||||
appendInstruction = false;
|
||||
|
||||
@@ -41,8 +41,8 @@ public class PartyListTypingService : DisposableMediatorSubscriberBase
|
||||
|
||||
public void Draw()
|
||||
{
|
||||
if (!_configService.Current.TypingIndicatorEnabled) return;
|
||||
if (!_configService.Current.TypingIndicatorShowOnPartyList) return;
|
||||
// Build map of visible users by AliasOrUID -> UID (case-insensitive)
|
||||
var visibleByAlias = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
try
|
||||
{
|
||||
|
||||
@@ -5,24 +5,27 @@ using MareSynchronos.Services.Mediator;
|
||||
using MareSynchronos.WebAPI;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MareSynchronos.API.Data;
|
||||
using MareSynchronos.MareConfiguration;
|
||||
|
||||
namespace MareSynchronos.Services;
|
||||
|
||||
public sealed class TypingIndicatorStateService : IMediatorSubscriber, IDisposable
|
||||
{
|
||||
private sealed record TypingEntry(UserData User, DateTime FirstSeen, DateTime LastUpdate);
|
||||
private sealed record TypingEntry(UserData User, DateTime FirstSeen, DateTime LastUpdate, MareSynchronos.API.Data.Enum.TypingScope Scope);
|
||||
|
||||
private readonly ConcurrentDictionary<string, TypingEntry> _typingUsers = new(StringComparer.Ordinal);
|
||||
private readonly ApiController _apiController;
|
||||
private readonly ILogger<TypingIndicatorStateService> _logger;
|
||||
private readonly MareConfigService _configService;
|
||||
private DateTime _selfTypingLast = DateTime.MinValue;
|
||||
private DateTime _selfTypingStart = DateTime.MinValue;
|
||||
private bool _selfTypingActive;
|
||||
|
||||
public TypingIndicatorStateService(ILogger<TypingIndicatorStateService> logger, MareMediator mediator, ApiController apiController)
|
||||
public TypingIndicatorStateService(ILogger<TypingIndicatorStateService> logger, MareMediator mediator, ApiController apiController, MareConfigService configService)
|
||||
{
|
||||
_logger = logger;
|
||||
_apiController = apiController;
|
||||
_configService = configService;
|
||||
Mediator = mediator;
|
||||
|
||||
mediator.Subscribe<UserTypingStateMessage>(this, OnTypingState);
|
||||
@@ -51,8 +54,19 @@ public sealed class TypingIndicatorStateService : IMediatorSubscriber, IDisposab
|
||||
_selfTypingActive = isTyping;
|
||||
}
|
||||
|
||||
public void ClearAll()
|
||||
{
|
||||
_typingUsers.Clear();
|
||||
_selfTypingActive = false;
|
||||
_selfTypingStart = DateTime.MinValue;
|
||||
_selfTypingLast = DateTime.MinValue;
|
||||
_logger.LogDebug("TypingIndicatorStateService: cleared all typing state");
|
||||
}
|
||||
|
||||
private void OnTypingState(UserTypingStateMessage msg)
|
||||
{
|
||||
if (!_configService.Current.TypingIndicatorEnabled)
|
||||
return;
|
||||
var uid = msg.Typing.User.UID;
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
@@ -74,8 +88,8 @@ public sealed class TypingIndicatorStateService : IMediatorSubscriber, IDisposab
|
||||
else if (msg.Typing.IsTyping)
|
||||
{
|
||||
_typingUsers.AddOrUpdate(uid,
|
||||
_ => new TypingEntry(msg.Typing.User, now, now),
|
||||
(_, existing) => new TypingEntry(msg.Typing.User, existing.FirstSeen, now));
|
||||
_ => new TypingEntry(msg.Typing.User, now, now, msg.Typing.Scope),
|
||||
(_, existing) => new TypingEntry(msg.Typing.User, existing.FirstSeen, now, msg.Typing.Scope));
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -101,7 +115,7 @@ public sealed class TypingIndicatorStateService : IMediatorSubscriber, IDisposab
|
||||
return true;
|
||||
}
|
||||
|
||||
public IReadOnlyDictionary<string, (UserData User, DateTime FirstSeen, DateTime LastUpdate)> GetActiveTypers(TimeSpan maxAge)
|
||||
public IReadOnlyDictionary<string, (UserData User, DateTime FirstSeen, DateTime LastUpdate, MareSynchronos.API.Data.Enum.TypingScope Scope)> GetActiveTypers(TimeSpan maxAge)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
foreach (var kvp in _typingUsers.ToArray())
|
||||
@@ -112,6 +126,6 @@ public sealed class TypingIndicatorStateService : IMediatorSubscriber, IDisposab
|
||||
}
|
||||
}
|
||||
|
||||
return _typingUsers.ToDictionary(k => k.Key, v => (v.Value.User, v.Value.FirstSeen, v.Value.LastUpdate), StringComparer.Ordinal);
|
||||
return _typingUsers.ToDictionary(k => k.Key, v => (v.Value.User, v.Value.FirstSeen, v.Value.LastUpdate, v.Value.Scope), StringComparer.Ordinal);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using MareSynchronos.PlayerData.Pairs;
|
||||
using MareSynchronos.Services.AutoDetect;
|
||||
using MareSynchronos.Services.Mediator;
|
||||
using MareSynchronos.Services.ServerConfiguration;
|
||||
using MareSynchronos.Services.Notifications;
|
||||
using MareSynchronos.UI;
|
||||
using MareSynchronos.UI.Components.Popup;
|
||||
using MareSynchronos.WebAPI;
|
||||
@@ -21,10 +22,11 @@ public class UiFactory
|
||||
private readonly MareProfileManager _mareProfileManager;
|
||||
private readonly PerformanceCollectorService _performanceCollectorService;
|
||||
private readonly SyncshellDiscoveryService _syncshellDiscoveryService;
|
||||
private readonly NotificationTracker _notificationTracker;
|
||||
|
||||
public UiFactory(ILoggerFactory loggerFactory, MareMediator mareMediator, ApiController apiController,
|
||||
UiSharedService uiSharedService, PairManager pairManager, SyncshellDiscoveryService syncshellDiscoveryService, ServerConfigurationManager serverConfigManager,
|
||||
MareProfileManager mareProfileManager, PerformanceCollectorService performanceCollectorService)
|
||||
MareProfileManager mareProfileManager, PerformanceCollectorService performanceCollectorService, NotificationTracker notificationTracker)
|
||||
{
|
||||
_loggerFactory = loggerFactory;
|
||||
_mareMediator = mareMediator;
|
||||
@@ -35,12 +37,13 @@ public class UiFactory
|
||||
_serverConfigManager = serverConfigManager;
|
||||
_mareProfileManager = mareProfileManager;
|
||||
_performanceCollectorService = performanceCollectorService;
|
||||
_notificationTracker = notificationTracker;
|
||||
}
|
||||
|
||||
public SyncshellAdminUI CreateSyncshellAdminUi(GroupFullInfoDto dto)
|
||||
{
|
||||
return new SyncshellAdminUI(_loggerFactory.CreateLogger<SyncshellAdminUI>(), _mareMediator,
|
||||
_apiController, _uiSharedService, _pairManager, _syncshellDiscoveryService, dto, _performanceCollectorService);
|
||||
_apiController, _uiSharedService, _pairManager, _syncshellDiscoveryService, dto, _performanceCollectorService, _notificationTracker);
|
||||
}
|
||||
|
||||
public StandaloneProfileUi CreateStandaloneProfileUi(Pair pair)
|
||||
|
||||
@@ -1114,7 +1114,10 @@ public sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
|
||||
ImGui.TableSetupColumn("Expire");
|
||||
ImGui.TableSetupColumn("Téléchargements");
|
||||
ImGui.TableSetupColumn("Accès");
|
||||
ImGui.TableSetupColumn("Actions", ImGuiTableColumnFlags.WidthFixed, 220f);
|
||||
var style = ImGui.GetStyle();
|
||||
float BtnWidth(string label) => ImGui.CalcTextSize(label).X + style.FramePadding.X * 2f;
|
||||
float ownActionsWidth = BtnWidth("Appliquer en GPose") + style.ItemSpacing.X + BtnWidth("Enregistrer") + style.ItemSpacing.X + BtnWidth("Supprimer") + 2f; // small margin
|
||||
ImGui.TableSetupColumn("Actions", ImGuiTableColumnFlags.WidthFixed, ownActionsWidth);
|
||||
ImGui.TableHeadersRow();
|
||||
|
||||
foreach (var entry in _mcdfShareManager.OwnShares)
|
||||
@@ -1134,6 +1137,32 @@ public sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted($"UID : {entry.AllowedIndividuals.Count}, Syncshells : {entry.AllowedSyncshells.Count}");
|
||||
if (ImGui.IsItemHovered())
|
||||
{
|
||||
ImGui.BeginTooltip();
|
||||
if (entry.AllowedIndividuals.Count > 0)
|
||||
{
|
||||
ImGui.TextUnformatted("UID autorisés:");
|
||||
foreach (var uid in entry.AllowedIndividuals)
|
||||
ImGui.BulletText(FormatUidWithName(uid));
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui.TextDisabled("Aucun UID autorisé");
|
||||
}
|
||||
ImGui.Separator();
|
||||
if (entry.AllowedSyncshells.Count > 0)
|
||||
{
|
||||
ImGui.TextUnformatted("Syncshells autorisées:");
|
||||
foreach (var gid in entry.AllowedSyncshells)
|
||||
ImGui.BulletText(FormatSyncshellLabel(gid));
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui.TextDisabled("Aucune syncshell autorisée");
|
||||
}
|
||||
ImGui.EndTooltip();
|
||||
}
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
using (ImRaii.PushId("ownShare" + entry.Id))
|
||||
@@ -1177,7 +1206,10 @@ public sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
|
||||
ImGui.TableSetupColumn("Propriétaire");
|
||||
ImGui.TableSetupColumn("Expire");
|
||||
ImGui.TableSetupColumn("Téléchargements");
|
||||
ImGui.TableSetupColumn("Actions", ImGuiTableColumnFlags.WidthFixed, 180f);
|
||||
var style2 = ImGui.GetStyle();
|
||||
float BtnWidth2(string label) => ImGui.CalcTextSize(label).X + style2.FramePadding.X * 2f;
|
||||
float sharedActionsWidth = BtnWidth2("Appliquer") + style2.ItemSpacing.X + BtnWidth2("Enregistrer") + 2f;
|
||||
ImGui.TableSetupColumn("Actions", ImGuiTableColumnFlags.WidthFixed, sharedActionsWidth);
|
||||
ImGui.TableHeadersRow();
|
||||
|
||||
foreach (var entry in _mcdfShareManager.SharedShares)
|
||||
@@ -1188,6 +1220,17 @@ public sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted(string.IsNullOrEmpty(entry.OwnerAlias) ? entry.OwnerUid : entry.OwnerAlias);
|
||||
if (ImGui.IsItemHovered())
|
||||
{
|
||||
ImGui.BeginTooltip();
|
||||
ImGui.TextUnformatted($"UID propriétaire: {entry.OwnerUid}");
|
||||
if (!string.IsNullOrEmpty(entry.OwnerAlias))
|
||||
{
|
||||
ImGui.Separator();
|
||||
ImGui.TextUnformatted($"Alias: {entry.OwnerAlias}");
|
||||
}
|
||||
ImGui.EndTooltip();
|
||||
}
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted(entry.ExpiresAtUtc.HasValue ? entry.ExpiresAtUtc.Value.ToLocalTime().ToString("g") : "Jamais");
|
||||
@@ -1334,6 +1377,14 @@ public sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
|
||||
return trimmed.ToUpperInvariant();
|
||||
}
|
||||
|
||||
private string FormatUidWithName(string uid)
|
||||
{
|
||||
if (string.IsNullOrEmpty(uid)) return string.Empty;
|
||||
var note = _serverConfigurationManager.GetNoteForUid(uid);
|
||||
if (!string.IsNullOrEmpty(note)) return $"{uid} ({note})";
|
||||
return uid;
|
||||
}
|
||||
|
||||
private string FormatPairLabel(string candidate)
|
||||
{
|
||||
if (string.IsNullOrEmpty(candidate))
|
||||
|
||||
@@ -860,28 +860,34 @@ if (showNearby && pendingInvites > 0)
|
||||
ImGuiHelpers.ScaledDummy(4f);
|
||||
var indent = 18f * ImGuiHelpers.GlobalScale;
|
||||
ImGui.Indent(indent);
|
||||
foreach (var e in nearbyEntries)
|
||||
|
||||
// Use a table to guarantee right-aligned action within the card content area
|
||||
var actionButtonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.UserPlus);
|
||||
if (ImGui.BeginTable("nearby-table", 2, ImGuiTableFlags.SizingStretchProp | ImGuiTableFlags.RowBg | ImGuiTableFlags.PadOuterX | ImGuiTableFlags.BordersInnerV))
|
||||
{
|
||||
if (!e.AcceptPairRequests || string.IsNullOrEmpty(e.Token))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.WidthStretch, 1f);
|
||||
ImGui.TableSetupColumn("Action", ImGuiTableColumnFlags.WidthFixed, actionButtonSize.X);
|
||||
|
||||
var name = e.DisplayName ?? e.Name;
|
||||
ImGui.AlignTextToFramePadding();
|
||||
ImGui.TextUnformatted(name);
|
||||
var right = ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth();
|
||||
ImGui.SameLine();
|
||||
foreach (var e in nearbyEntries)
|
||||
{
|
||||
if (!e.AcceptPairRequests || string.IsNullOrEmpty(e.Token))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ImGui.TableNextRow();
|
||||
|
||||
ImGui.TableSetColumnIndex(0);
|
||||
var name = e.DisplayName ?? e.Name;
|
||||
ImGui.AlignTextToFramePadding();
|
||||
ImGui.TextUnformatted(name);
|
||||
|
||||
// Right column: action button, aligned to the right within the column
|
||||
ImGui.TableSetColumnIndex(1);
|
||||
var curX = ImGui.GetCursorPosX();
|
||||
var availX = ImGui.GetContentRegionAvail().X; // width of the action column
|
||||
ImGui.SetCursorPosX(curX + MathF.Max(0, availX - actionButtonSize.X));
|
||||
|
||||
var statusButtonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.UserPlus);
|
||||
ImGui.SetCursorPosX(right - statusButtonSize.X);
|
||||
if (!e.AcceptPairRequests)
|
||||
{
|
||||
_uiSharedService.IconText(FontAwesomeIcon.Ban, ImGuiColors.DalamudGrey3);
|
||||
UiSharedService.AttachToolTip("Les demandes sont désactivées pour ce joueur");
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(e.Token))
|
||||
{
|
||||
using (ImRaii.PushId(e.Token ?? e.Uid ?? e.Name ?? string.Empty))
|
||||
{
|
||||
if (_uiSharedService.IconButton(FontAwesomeIcon.UserPlus))
|
||||
@@ -891,12 +897,9 @@ if (showNearby && pendingInvites > 0)
|
||||
}
|
||||
UiSharedService.AttachToolTip("Envoyer une invitation d'apparaige");
|
||||
}
|
||||
else
|
||||
{
|
||||
_uiSharedService.IconText(FontAwesomeIcon.QuestionCircle, ImGuiColors.DalamudGrey3);
|
||||
UiSharedService.AttachToolTip("Impossible d'inviter ce joueur");
|
||||
}
|
||||
ImGui.EndTable();
|
||||
}
|
||||
|
||||
ImGui.Unindent(indent);
|
||||
}, stretchWidth: true);
|
||||
}
|
||||
@@ -1173,6 +1176,9 @@ if (showNearby && pendingInvites > 0)
|
||||
case NotificationCategory.AutoDetect:
|
||||
DrawAutoDetectNotification(notification);
|
||||
break;
|
||||
case NotificationCategory.Syncshell:
|
||||
DrawSyncshellNotification(notification);
|
||||
break;
|
||||
default:
|
||||
UiSharedService.DrawCard($"notification-{notification.Category}-{notification.Id}", () =>
|
||||
{
|
||||
@@ -1237,6 +1243,30 @@ if (showNearby && pendingInvites > 0)
|
||||
}, stretchWidth: true);
|
||||
}
|
||||
|
||||
private void DrawSyncshellNotification(NotificationEntry notification)
|
||||
{
|
||||
UiSharedService.DrawCard($"notification-syncshell-{notification.Id}", () =>
|
||||
{
|
||||
ImGui.TextUnformatted(notification.Title);
|
||||
if (!string.IsNullOrEmpty(notification.Description))
|
||||
{
|
||||
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey3);
|
||||
ImGui.TextWrapped(notification.Description);
|
||||
ImGui.PopStyleColor();
|
||||
}
|
||||
|
||||
ImGuiHelpers.ScaledDummy(3f);
|
||||
|
||||
using (ImRaii.PushId($"syncshell-{notification.Id}"))
|
||||
{
|
||||
if (ImGui.Button("Effacer"))
|
||||
{
|
||||
_notificationTracker.Remove(NotificationCategory.Syncshell, notification.Id);
|
||||
}
|
||||
}
|
||||
}, stretchWidth: true);
|
||||
}
|
||||
|
||||
private void TriggerAcceptAutoDetectNotification(string uid)
|
||||
{
|
||||
_ = Task.Run(async () =>
|
||||
|
||||
@@ -79,6 +79,8 @@ public class DrawGroupPair : DrawPairBase
|
||||
width += _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Plus).X + spacing;
|
||||
}
|
||||
|
||||
width += spacing * 1.2f;
|
||||
|
||||
return width;
|
||||
}
|
||||
|
||||
@@ -215,6 +217,7 @@ public class DrawGroupPair : DrawPairBase
|
||||
var pauseIcon = _fullInfoDto.GroupUserPermissions.IsPaused() ? FontAwesomeIcon.Play : FontAwesomeIcon.Pause;
|
||||
var pauseButtonWidth = _uiSharedService.GetIconButtonSize(pauseIcon).X;
|
||||
var barButtonWidth = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Bars).X;
|
||||
var rightEdgeGap = spacing * 1.2f;
|
||||
|
||||
float totalWidth = 0f;
|
||||
void Accumulate(bool condition, float width)
|
||||
@@ -242,7 +245,7 @@ public class DrawGroupPair : DrawPairBase
|
||||
float cardPaddingX = UiSharedService.GetCardContentPaddingX();
|
||||
float rightMargin = cardPaddingX + 6f * ImGuiHelpers.GlobalScale;
|
||||
float baseX = MathF.Max(ImGui.GetCursorPosX(),
|
||||
ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth() - rightMargin - totalWidth);
|
||||
ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth() - rightMargin - rightEdgeGap - totalWidth);
|
||||
float currentX = baseX;
|
||||
|
||||
ImGui.SameLine();
|
||||
@@ -266,6 +269,16 @@ public class DrawGroupPair : DrawPairBase
|
||||
|
||||
if (showInfo && infoIconWidth > 0f)
|
||||
{
|
||||
bool centerWarning = permIcon == FontAwesomeIcon.ExclamationTriangle && showPause && showBars && !showShared && !showPlus;
|
||||
if (centerWarning)
|
||||
{
|
||||
float barsClusterWidth = showBars ? (barButtonWidth + spacing * 0.5f) : 0f;
|
||||
float leftAreaWidth = MathF.Max(totalWidth - pauseButtonWidth - barsClusterWidth, 0f);
|
||||
float warningX = baseX + MathF.Max((leftAreaWidth - infoIconWidth) / 2f, 0f);
|
||||
currentX = warningX;
|
||||
ImGui.SetCursorPosX(currentX);
|
||||
}
|
||||
|
||||
ImGui.SetCursorPosY(textPosY);
|
||||
if (individualAnimDisabled || individualSoundsDisabled || individualVFXDisabled)
|
||||
{
|
||||
@@ -359,7 +372,7 @@ public class DrawGroupPair : DrawPairBase
|
||||
{
|
||||
ImGui.SetCursorPosY(originalY);
|
||||
|
||||
if (_uiSharedService.IconButton(FontAwesomeIcon.Plus))
|
||||
if (_uiSharedService.IconPlusButtonCentered())
|
||||
{
|
||||
var targetUid = _pair.UserData.UID;
|
||||
if (!string.IsNullOrEmpty(targetUid))
|
||||
@@ -376,7 +389,7 @@ public class DrawGroupPair : DrawPairBase
|
||||
{
|
||||
float gapToBars = showBars ? spacing * 0.5f : spacing;
|
||||
ImGui.SetCursorPosY(originalY);
|
||||
if (_uiSharedService.IconButton(pauseIcon))
|
||||
if (pauseIcon == FontAwesomeIcon.Pause ? _uiSharedService.IconPauseButtonCentered() : _uiSharedService.IconButtonCentered(pauseIcon))
|
||||
{
|
||||
var newPermissions = _fullInfoDto.GroupUserPermissions ^ GroupUserPermissions.Paused;
|
||||
_fullInfoDto.GroupUserPermissions = newPermissions;
|
||||
@@ -391,7 +404,7 @@ public class DrawGroupPair : DrawPairBase
|
||||
if (showBars)
|
||||
{
|
||||
ImGui.SetCursorPosY(originalY);
|
||||
if (_uiSharedService.IconButton(FontAwesomeIcon.Bars))
|
||||
if (_uiSharedService.IconButtonCentered(FontAwesomeIcon.Bars))
|
||||
{
|
||||
ImGui.OpenPopup("Syncshell Flyout Menu");
|
||||
}
|
||||
|
||||
@@ -42,8 +42,8 @@ public abstract class DrawPairBase
|
||||
var menuButtonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Bars);
|
||||
|
||||
float pauseClusterWidth = Math.Max(pauseButtonSize.X, playButtonSize.X);
|
||||
float pauseClusterHeight = Math.Max(pauseButtonSize.Y, playButtonSize.Y);
|
||||
float reservedSpacing = style.ItemSpacing.X * 2.4f;
|
||||
float pauseClusterHeight = Math.Max(Math.Max(pauseButtonSize.Y, playButtonSize.Y), ImGui.GetFrameHeight());
|
||||
float reservedSpacing = style.ItemSpacing.X * 1.6f;
|
||||
float rightButtonWidth =
|
||||
menuButtonSize.X +
|
||||
pauseClusterWidth +
|
||||
@@ -84,11 +84,15 @@ public abstract class DrawPairBase
|
||||
|
||||
ImGui.SetCursorPos(new Vector2(rowStartCursor.X + padding.X, iconTop));
|
||||
DrawLeftSide(iconTop, iconTop);
|
||||
ImGui.SameLine();
|
||||
ImGui.SetCursorPosY(textTop);
|
||||
var posX = ImGui.GetCursorPosX();
|
||||
|
||||
float leftReserved = GetLeftSideReservedWidth();
|
||||
float nameStartX = rowStartCursor.X + padding.X + leftReserved;
|
||||
|
||||
var rightSide = DrawRightSide(buttonTop, buttonTop);
|
||||
DrawName(textTop + padding.Y * 0.15f, posX, rightSide);
|
||||
|
||||
ImGui.SameLine(nameStartX);
|
||||
ImGui.SetCursorPosY(textTop);
|
||||
DrawName(textTop + padding.Y * 0.15f, nameStartX, rightSide);
|
||||
|
||||
ImGui.SetCursorPos(new Vector2(rowStartCursor.X, rowStartCursor.Y + totalHeight));
|
||||
ImGui.SetCursorPosX(rowStartCursor.X);
|
||||
@@ -100,6 +104,8 @@ public abstract class DrawPairBase
|
||||
|
||||
protected virtual float GetRightSideExtraWidth() => 0f;
|
||||
|
||||
protected virtual float GetLeftSideReservedWidth() => UiSharedService.GetIconSize(FontAwesomeIcon.Moon).X * 2f + ImGui.GetStyle().ItemSpacing.X * 1.5f;
|
||||
|
||||
private void DrawName(float originalY, float leftSide, float rightSide)
|
||||
{
|
||||
_displayHandler.DrawPairText(_id, _pair, leftSide, originalY, () => rightSide - leftSide);
|
||||
|
||||
@@ -60,8 +60,28 @@ public class DrawUserPair : DrawPairBase
|
||||
width += _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Running).X + spacingX * 0.5f;
|
||||
}
|
||||
|
||||
width += spacingX * 1.2f;
|
||||
return width;
|
||||
}
|
||||
|
||||
protected override float GetLeftSideReservedWidth()
|
||||
{
|
||||
var style = ImGui.GetStyle();
|
||||
float spacing = style.ItemSpacing.X;
|
||||
float iconW = UiSharedService.GetIconSize(FontAwesomeIcon.Moon).X;
|
||||
|
||||
int icons = 1;
|
||||
if (!(_pair.UserPair!.OwnPermissions.IsPaired() && _pair.UserPair!.OtherPermissions.IsPaired()))
|
||||
icons++;
|
||||
else if (_pair.UserPair!.OwnPermissions.IsPaused() || _pair.UserPair!.OtherPermissions.IsPaused())
|
||||
icons++;
|
||||
if (_pair.IsOnline && _pair.IsVisible)
|
||||
icons++;
|
||||
|
||||
float iconsTotal = icons * iconW + Math.Max(0, icons - 1) * spacing;
|
||||
float cushion = spacing * 0.6f;
|
||||
return iconsTotal + cushion;
|
||||
}
|
||||
|
||||
protected override void DrawLeftSide(float textPosY, float originalY)
|
||||
{
|
||||
@@ -133,7 +153,8 @@ public class DrawUserPair : DrawPairBase
|
||||
var entryUID = _pair.UserData.AliasOrUID;
|
||||
var spacingX = ImGui.GetStyle().ItemSpacing.X;
|
||||
var edgePadding = UiSharedService.GetCardContentPaddingX() + 6f * ImGuiHelpers.GlobalScale;
|
||||
var windowEndX = ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth() - edgePadding;
|
||||
var rightEdgeGap = spacingX * 1.2f;
|
||||
var windowEndX = ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth() - edgePadding - rightEdgeGap;
|
||||
var rightSidePos = windowEndX - barButtonSize.X;
|
||||
|
||||
// Flyout Menu
|
||||
@@ -150,13 +171,12 @@ public class DrawUserPair : DrawPairBase
|
||||
ImGui.EndPopup();
|
||||
}
|
||||
|
||||
// Pause (mutual pairs only)
|
||||
if (_pair.UserPair!.OwnPermissions.IsPaired() && _pair.UserPair!.OtherPermissions.IsPaired())
|
||||
{
|
||||
rightSidePos -= pauseIconSize.X + spacingX;
|
||||
ImGui.SameLine(rightSidePos);
|
||||
ImGui.SetCursorPosY(originalY);
|
||||
if (_uiSharedService.IconButton(pauseIcon))
|
||||
if (pauseIcon == FontAwesomeIcon.Pause ? _uiSharedService.IconPauseButtonCentered() : _uiSharedService.IconButtonCentered(pauseIcon))
|
||||
{
|
||||
var perm = _pair.UserPair!.OwnPermissions;
|
||||
perm.SetPaused(!perm.IsPaused());
|
||||
|
||||
@@ -52,6 +52,8 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
private readonly AccountRegistrationService _registerService;
|
||||
private readonly ServerConfigurationManager _serverConfigurationManager;
|
||||
private readonly UiSharedService _uiShared;
|
||||
private readonly TypingIndicatorStateService _typingStateService;
|
||||
private readonly ChatTypingDetectionService _chatTypingDetectionService;
|
||||
private static readonly string DtrDefaultPreviewText = DtrEntry.DefaultGlyph + " 123";
|
||||
private bool _deleteAccountPopupModalShown = false;
|
||||
private string _lastTab = string.Empty;
|
||||
@@ -80,7 +82,9 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
FileCompactor fileCompactor, ApiController apiController,
|
||||
IpcManager ipcManager, IpcProvider ipcProvider, CacheMonitor cacheMonitor,
|
||||
DalamudUtilService dalamudUtilService, AccountRegistrationService registerService,
|
||||
AutoDetectSuppressionService autoDetectSuppressionService) : base(logger, mediator, "Umbra Settings", performanceCollector)
|
||||
AutoDetectSuppressionService autoDetectSuppressionService,
|
||||
TypingIndicatorStateService typingIndicatorStateService,
|
||||
ChatTypingDetectionService chatTypingDetectionService) : base(logger, mediator, "Umbra Settings", performanceCollector)
|
||||
{
|
||||
_configService = configService;
|
||||
_pairManager = pairManager;
|
||||
@@ -102,6 +106,8 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
_autoDetectSuppressionService = autoDetectSuppressionService;
|
||||
_fileCompactor = fileCompactor;
|
||||
_uiShared = uiShared;
|
||||
_typingStateService = typingIndicatorStateService;
|
||||
_chatTypingDetectionService = chatTypingDetectionService;
|
||||
AllowClickthrough = false;
|
||||
AllowPinning = false;
|
||||
_validationProgress = new Progress<(int, int, FileCacheEntity)>(v => _currentProgress = v);
|
||||
@@ -1123,8 +1129,10 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
var useNameColors = _configService.Current.UseNameColors;
|
||||
var nameColors = _configService.Current.NameColors;
|
||||
var autoPausedNameColors = _configService.Current.BlockedNameColors;
|
||||
var typingEnabled = _configService.Current.TypingIndicatorEnabled;
|
||||
var typingIndicatorNameplates = _configService.Current.TypingIndicatorShowOnNameplates;
|
||||
var typingIndicatorPartyList = _configService.Current.TypingIndicatorShowOnPartyList;
|
||||
var typingShowSelf = _configService.Current.TypingIndicatorShowSelf;
|
||||
if (ImGui.Checkbox("Coloriser les plaques de nom des paires", ref useNameColors))
|
||||
{
|
||||
_configService.Current.UseNameColors = useNameColors;
|
||||
@@ -1152,42 +1160,60 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
}
|
||||
}
|
||||
|
||||
if (ImGui.Checkbox("Afficher la bulle de frappe sur les plaques", ref typingIndicatorNameplates))
|
||||
if (ImGui.Checkbox("Activer le système d'indicateur de frappe", ref typingEnabled))
|
||||
{
|
||||
_configService.Current.TypingIndicatorShowOnNameplates = typingIndicatorNameplates;
|
||||
_configService.Current.TypingIndicatorEnabled = typingEnabled;
|
||||
_configService.Save();
|
||||
_chatTypingDetectionService.SoftRestart();
|
||||
}
|
||||
_uiShared.DrawHelpText("Ajoute une bulle '...' sur la plaque des paires en train d'écrire.");
|
||||
_uiShared.DrawHelpText("Active ou désactive complètement l'envoi/la réception et l'affichage des bulles de frappe.");
|
||||
|
||||
using (ImRaii.Disabled(!typingIndicatorNameplates))
|
||||
if (typingEnabled)
|
||||
{
|
||||
using var indentTyping = ImRaii.PushIndent();
|
||||
var bubbleSize = _configService.Current.TypingIndicatorBubbleSize;
|
||||
TypingIndicatorBubbleSize? selectedBubbleSize = _uiShared.DrawCombo("Taille de la bulle de frappe##typingBubbleSize",
|
||||
Enum.GetValues<TypingIndicatorBubbleSize>(),
|
||||
size => size switch
|
||||
{
|
||||
TypingIndicatorBubbleSize.Small => "Petite",
|
||||
TypingIndicatorBubbleSize.Medium => "Moyenne",
|
||||
TypingIndicatorBubbleSize.Large => "Grande",
|
||||
_ => size.ToString()
|
||||
},
|
||||
null,
|
||||
bubbleSize);
|
||||
|
||||
if (selectedBubbleSize.HasValue && selectedBubbleSize.Value != bubbleSize)
|
||||
if (ImGui.Checkbox("Afficher la bulle de frappe sur les plaques", ref typingIndicatorNameplates))
|
||||
{
|
||||
_configService.Current.TypingIndicatorBubbleSize = selectedBubbleSize.Value;
|
||||
_configService.Current.TypingIndicatorShowOnNameplates = typingIndicatorNameplates;
|
||||
_configService.Save();
|
||||
}
|
||||
}
|
||||
_uiShared.DrawHelpText("Ajoute une bulle '...' sur la plaque des paires en train d'écrire.");
|
||||
|
||||
if (ImGui.Checkbox("Tracer la frappe dans la liste de groupe", ref typingIndicatorPartyList))
|
||||
{
|
||||
_configService.Current.TypingIndicatorShowOnPartyList = typingIndicatorPartyList;
|
||||
_configService.Save();
|
||||
if (typingIndicatorNameplates)
|
||||
{
|
||||
using var indentTyping = ImRaii.PushIndent();
|
||||
var bubbleSize = _configService.Current.TypingIndicatorBubbleSize;
|
||||
TypingIndicatorBubbleSize? selectedBubbleSize = _uiShared.DrawCombo("Taille de la bulle de frappe##typingBubbleSize",
|
||||
Enum.GetValues<TypingIndicatorBubbleSize>(),
|
||||
size => size switch
|
||||
{
|
||||
TypingIndicatorBubbleSize.Small => "Petite",
|
||||
TypingIndicatorBubbleSize.Medium => "Moyenne",
|
||||
TypingIndicatorBubbleSize.Large => "Grande",
|
||||
_ => size.ToString()
|
||||
},
|
||||
null,
|
||||
bubbleSize);
|
||||
|
||||
if (selectedBubbleSize.HasValue && selectedBubbleSize.Value != bubbleSize)
|
||||
{
|
||||
_configService.Current.TypingIndicatorBubbleSize = selectedBubbleSize.Value;
|
||||
_configService.Save();
|
||||
}
|
||||
|
||||
if (ImGui.Checkbox("Tracer la frappe dans la liste de groupe", ref typingIndicatorPartyList))
|
||||
{
|
||||
_configService.Current.TypingIndicatorShowOnPartyList = typingIndicatorPartyList;
|
||||
_configService.Save();
|
||||
}
|
||||
_uiShared.DrawHelpText("Consigne dans les journaux quand une paire du groupe est en train d'écrire (bulle visuelle ultérieure).");
|
||||
|
||||
if (ImGui.Checkbox("Afficher ma propre bulle", ref typingShowSelf))
|
||||
{
|
||||
_configService.Current.TypingIndicatorShowSelf = typingShowSelf;
|
||||
_configService.Save();
|
||||
}
|
||||
_uiShared.DrawHelpText("Affiche votre propre bulle lorsque vous tapez (utile pour test/retour visuel).");
|
||||
}
|
||||
}
|
||||
_uiShared.DrawHelpText("Consigne dans les journaux quand une paire du groupe est en train d'écrire (bulle visuelle ultérieure).");
|
||||
|
||||
if (ImGui.Checkbox("Show separate Visible group", ref showVisibleSeparate))
|
||||
{
|
||||
|
||||
@@ -11,11 +11,13 @@ using MareSynchronos.PlayerData.Pairs;
|
||||
using MareSynchronos.Services;
|
||||
using MareSynchronos.Services.AutoDetect;
|
||||
using MareSynchronos.Services.Mediator;
|
||||
using MareSynchronos.Services.Notifications;
|
||||
using MareSynchronos.WebAPI;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Globalization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MareSynchronos.MareConfiguration.Models;
|
||||
|
||||
namespace MareSynchronos.UI.Components.Popup;
|
||||
|
||||
@@ -28,6 +30,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
||||
private readonly PairManager _pairManager;
|
||||
private readonly UiSharedService _uiSharedService;
|
||||
private readonly SyncshellDiscoveryService _syncshellDiscoveryService;
|
||||
private readonly NotificationTracker _notificationTracker;
|
||||
private List<BannedGroupUserDto> _bannedUsers = [];
|
||||
private int _multiInvites;
|
||||
private string _newPassword;
|
||||
@@ -41,10 +44,20 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
||||
private bool _autoDetectVisible;
|
||||
private bool _autoDetectPasswordDisabled;
|
||||
private string? _autoDetectMessage;
|
||||
|
||||
private bool _autoDetectDesiredVisibility;
|
||||
private int _adDurationHours = 2;
|
||||
private bool _adRecurring = false;
|
||||
private readonly bool[] _adWeekdays = new bool[7];
|
||||
private int _adStartHour = 21;
|
||||
private int _adStartMinute = 0;
|
||||
private int _adEndHour = 23;
|
||||
private int _adEndMinute = 0;
|
||||
private const string AutoDetectTimeZone = "Europe/Paris";
|
||||
|
||||
public SyncshellAdminUI(ILogger<SyncshellAdminUI> logger, MareMediator mediator, ApiController apiController,
|
||||
UiSharedService uiSharedService, PairManager pairManager, SyncshellDiscoveryService syncshellDiscoveryService,
|
||||
GroupFullInfoDto groupFullInfo, PerformanceCollectorService performanceCollectorService)
|
||||
GroupFullInfoDto groupFullInfo, PerformanceCollectorService performanceCollectorService, NotificationTracker notificationTracker)
|
||||
: base(logger, mediator, "Syncshell Admin Panel (" + groupFullInfo.GroupAliasOrGID + ")", performanceCollectorService)
|
||||
{
|
||||
GroupFullInfo = groupFullInfo;
|
||||
@@ -52,12 +65,14 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
||||
_uiSharedService = uiSharedService;
|
||||
_pairManager = pairManager;
|
||||
_syncshellDiscoveryService = syncshellDiscoveryService;
|
||||
_notificationTracker = notificationTracker;
|
||||
_isOwner = string.Equals(GroupFullInfo.OwnerUID, _apiController.UID, StringComparison.Ordinal);
|
||||
_isModerator = GroupFullInfo.GroupUserInfo.IsModerator();
|
||||
_newPassword = string.Empty;
|
||||
_multiInvites = 30;
|
||||
_pwChangeSuccess = true;
|
||||
_autoDetectVisible = groupFullInfo.AutoDetectVisible;
|
||||
_autoDetectDesiredVisibility = _autoDetectVisible;
|
||||
_autoDetectPasswordDisabled = groupFullInfo.PasswordTemporarilyDisabled;
|
||||
Mediator.Subscribe<SyncshellAutoDetectStateChanged>(this, OnSyncshellAutoDetectStateChanged);
|
||||
IsOpen = true;
|
||||
@@ -89,6 +104,9 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
||||
ImGui.Separator();
|
||||
var perm = GroupFullInfo.GroupPermissions;
|
||||
|
||||
using var tabColor = ImRaii.PushColor(ImGuiCol.Tab, UiSharedService.AccentColor);
|
||||
using var tabHoverColor = ImRaii.PushColor(ImGuiCol.TabHovered, UiSharedService.AccentHoverColor);
|
||||
using var tabActiveColor = ImRaii.PushColor(ImGuiCol.TabActive, UiSharedService.AccentActiveColor);
|
||||
using var tabbar = ImRaii.TabBar("syncshell_tab_" + GroupFullInfo.GID);
|
||||
|
||||
if (tabbar)
|
||||
@@ -498,26 +516,93 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
||||
UiSharedService.ColorTextWrapped(_autoDetectMessage!, ImGuiColors.DalamudYellow);
|
||||
}
|
||||
|
||||
bool desiredVisibility = _autoDetectVisible;
|
||||
using (ImRaii.Disabled(_autoDetectToggleInFlight || _autoDetectStateLoading))
|
||||
{
|
||||
if (ImGui.Checkbox("Afficher cette Syncshell dans l'AutoDetect", ref desiredVisibility))
|
||||
if (ImGui.Checkbox("Afficher cette Syncshell dans l'AutoDetect", ref _autoDetectDesiredVisibility))
|
||||
{
|
||||
_ = ToggleAutoDetectAsync(desiredVisibility);
|
||||
// Only change local desired state; sending is done via the validate button
|
||||
}
|
||||
}
|
||||
_uiSharedService.DrawHelpText("Quand cette option est activée, le mot de passe devient inactif tant que la visibilité est maintenue.");
|
||||
|
||||
if (_autoDetectDesiredVisibility)
|
||||
{
|
||||
ImGuiHelpers.ScaledDummy(4);
|
||||
ImGui.TextUnformatted("Options d'affichage AutoDetect");
|
||||
ImGui.Separator();
|
||||
|
||||
// Recurring toggle first
|
||||
ImGui.Checkbox("Affichage récurrent", ref _adRecurring);
|
||||
_uiSharedService.DrawHelpText("Si activé, vous pouvez choisir les jours et une plage horaire récurrents. Si désactivé, seule la durée sera prise en compte.");
|
||||
|
||||
// Duration in hours (only when NOT recurring)
|
||||
if (!_adRecurring)
|
||||
{
|
||||
ImGuiHelpers.ScaledDummy(4);
|
||||
int duration = _adDurationHours;
|
||||
ImGui.PushItemWidth(120 * ImGuiHelpers.GlobalScale);
|
||||
if (ImGui.InputInt("Durée (heures)", ref duration))
|
||||
{
|
||||
_adDurationHours = Math.Clamp(duration, 1, 240);
|
||||
}
|
||||
ImGui.PopItemWidth();
|
||||
_uiSharedService.DrawHelpText("Combien de temps la Syncshell doit rester visible, en heures.");
|
||||
}
|
||||
|
||||
ImGuiHelpers.ScaledDummy(4);
|
||||
if (_adRecurring)
|
||||
{
|
||||
ImGuiHelpers.ScaledDummy(4);
|
||||
ImGui.TextUnformatted("Jours de la semaine actifs :");
|
||||
string[] daysFr = new[] { "Lun", "Mar", "Mer", "Jeu", "Ven", "Sam", "Dim" };
|
||||
for (int i = 0; i < 7; i++)
|
||||
{
|
||||
ImGui.SameLine(i == 0 ? 0 : 0);
|
||||
bool v = _adWeekdays[i];
|
||||
if (ImGui.Checkbox($"##adwd{i}", ref v)) _adWeekdays[i] = v;
|
||||
ImGui.SameLine();
|
||||
ImGui.TextUnformatted(daysFr[i]);
|
||||
if (i < 6) ImGui.SameLine();
|
||||
}
|
||||
ImGui.NewLine();
|
||||
_uiSharedService.DrawHelpText("Sélectionnez les jours où l'affichage est autorisé (ex: jeudi et dimanche).");
|
||||
|
||||
ImGuiHelpers.ScaledDummy(4);
|
||||
ImGui.TextUnformatted("Plage horaire (heure locale Europe/Paris) :");
|
||||
ImGui.PushItemWidth(60 * ImGuiHelpers.GlobalScale);
|
||||
ImGui.InputInt("Début heure", ref _adStartHour); ImGui.SameLine();
|
||||
ImGui.InputInt("min", ref _adStartMinute);
|
||||
_adStartHour = Math.Clamp(_adStartHour, 0, 23);
|
||||
_adStartMinute = Math.Clamp(_adStartMinute, 0, 59);
|
||||
ImGui.SameLine();
|
||||
ImGui.TextUnformatted("→"); ImGui.SameLine();
|
||||
ImGui.InputInt("Fin heure", ref _adEndHour); ImGui.SameLine();
|
||||
ImGui.InputInt("min ", ref _adEndMinute);
|
||||
_adEndHour = Math.Clamp(_adEndHour, 0, 23);
|
||||
_adEndMinute = Math.Clamp(_adEndMinute, 0, 59);
|
||||
ImGui.PopItemWidth();
|
||||
_uiSharedService.DrawHelpText("Exemple : de 21h00 à 23h00. Le fuseau utilisé est Europe/Paris (avec changements été/hiver).");
|
||||
}
|
||||
}
|
||||
|
||||
if (_autoDetectPasswordDisabled && _autoDetectVisible)
|
||||
{
|
||||
UiSharedService.ColorTextWrapped("Le mot de passe est actuellement désactivé pendant la visibilité AutoDetect.", ImGuiColors.DalamudYellow);
|
||||
}
|
||||
|
||||
ImGuiHelpers.ScaledDummy(6);
|
||||
if (ImGui.Button("Recharger l'état"))
|
||||
using (ImRaii.Disabled(_autoDetectToggleInFlight || _autoDetectStateLoading))
|
||||
{
|
||||
_autoDetectStateLoading = true;
|
||||
_ = EnsureAutoDetectStateAsync(true);
|
||||
if (ImGui.Button("Valider et envoyer"))
|
||||
{
|
||||
_ = SubmitAutoDetectAsync();
|
||||
}
|
||||
ImGui.SameLine();
|
||||
if (ImGui.Button("Recharger l'état"))
|
||||
{
|
||||
_autoDetectStateLoading = true;
|
||||
_ = EnsureAutoDetectStateAsync(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -569,6 +654,11 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
||||
_autoDetectMessage = desiredVisibility
|
||||
? "La Syncshell est désormais visible dans AutoDetect."
|
||||
: "La Syncshell n'est plus visible dans AutoDetect.";
|
||||
|
||||
if (desiredVisibility)
|
||||
{
|
||||
PublishSyncshellPublicNotification();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -580,6 +670,72 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SubmitAutoDetectAsync()
|
||||
{
|
||||
if (_autoDetectToggleInFlight)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_autoDetectToggleInFlight = true;
|
||||
_autoDetectMessage = null;
|
||||
|
||||
try
|
||||
{
|
||||
// Duration always used when visible
|
||||
int? duration = _autoDetectDesiredVisibility ? _adDurationHours : null;
|
||||
|
||||
// Scheduling fields only if recurring is enabled
|
||||
int[]? weekdaysArr = null;
|
||||
TimeSpan? start = null;
|
||||
TimeSpan? end = null;
|
||||
string? tz = null;
|
||||
if (_autoDetectDesiredVisibility && _adRecurring)
|
||||
{
|
||||
List<int> weekdays = new();
|
||||
for (int i = 0; i < 7; i++) if (_adWeekdays[i]) weekdays.Add(i);
|
||||
weekdaysArr = weekdays.Count > 0 ? weekdays.ToArray() : Array.Empty<int>();
|
||||
start = new TimeSpan(_adStartHour, _adStartMinute, 0);
|
||||
end = new TimeSpan(_adEndHour, _adEndMinute, 0);
|
||||
tz = AutoDetectTimeZone;
|
||||
}
|
||||
|
||||
var ok = await _syncshellDiscoveryService.SetVisibilityAsync(
|
||||
GroupFullInfo.GID,
|
||||
_autoDetectDesiredVisibility,
|
||||
duration,
|
||||
weekdaysArr,
|
||||
start,
|
||||
end,
|
||||
tz,
|
||||
CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
if (!ok)
|
||||
{
|
||||
_autoDetectMessage = "Impossible d'envoyer les paramètres AutoDetect.";
|
||||
return;
|
||||
}
|
||||
|
||||
await EnsureAutoDetectStateAsync(true).ConfigureAwait(false);
|
||||
_autoDetectMessage = _autoDetectDesiredVisibility
|
||||
? "Paramètres AutoDetect envoyés. La Syncshell sera visible selon le planning défini."
|
||||
: "La Syncshell n'est plus visible dans AutoDetect.";
|
||||
|
||||
if (_autoDetectDesiredVisibility)
|
||||
{
|
||||
PublishSyncshellPublicNotification();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_autoDetectMessage = $"Erreur lors de l'envoi des paramètres AutoDetect : {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
_autoDetectToggleInFlight = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyAutoDetectState(bool visible, bool passwordDisabled, bool fromServer)
|
||||
{
|
||||
_autoDetectVisible = visible;
|
||||
@@ -602,4 +758,19 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
||||
{
|
||||
Mediator.Publish(new RemoveWindowMessage(this));
|
||||
}
|
||||
|
||||
private void PublishSyncshellPublicNotification()
|
||||
{
|
||||
try
|
||||
{
|
||||
var title = $"Syncshell publique: {GroupFullInfo.GroupAliasOrGID}";
|
||||
var message = "La Syncshell est désormais visible via AutoDetect.";
|
||||
Mediator.Publish(new DualNotificationMessage(title, message, NotificationType.Info, TimeSpan.FromSeconds(4)));
|
||||
_notificationTracker.Upsert(NotificationEntry.SyncshellPublic(GroupFullInfo.GID, GroupFullInfo.GroupAliasOrGID));
|
||||
}
|
||||
catch
|
||||
{
|
||||
// swallow any notification errors to not break UI flow
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ using Dalamud.Interface.Utility;
|
||||
using Dalamud.Plugin.Services;
|
||||
using MareSynchronos.API.Data;
|
||||
using MareSynchronos.API.Data.Extensions;
|
||||
using MareSynchronos.API.Data.Enum;
|
||||
using MareSynchronos.MareConfiguration;
|
||||
using MareSynchronos.MareConfiguration.Models;
|
||||
using MareSynchronos.PlayerData.Pairs;
|
||||
@@ -75,6 +76,10 @@ public sealed class TypingIndicatorOverlay : WindowMediatorSubscriberBase
|
||||
if (!_clientState.IsLoggedIn)
|
||||
return;
|
||||
|
||||
var typingEnabled = _configService.Current.TypingIndicatorEnabled;
|
||||
if (!typingEnabled)
|
||||
return;
|
||||
|
||||
var showParty = _configService.Current.TypingIndicatorShowOnPartyList;
|
||||
var showNameplates = _configService.Current.TypingIndicatorShowOnNameplates;
|
||||
|
||||
@@ -97,14 +102,16 @@ public sealed class TypingIndicatorOverlay : WindowMediatorSubscriberBase
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe void DrawPartyIndicators(ImDrawListPtr drawList, IReadOnlyDictionary<string, (UserData User, DateTime FirstSeen, DateTime LastUpdate)> activeTypers,
|
||||
private unsafe void DrawPartyIndicators(ImDrawListPtr drawList, IReadOnlyDictionary<string, (UserData User, DateTime FirstSeen, DateTime LastUpdate, MareSynchronos.API.Data.Enum.TypingScope Scope)> activeTypers,
|
||||
bool selfActive, DateTime now, DateTime selfStart, DateTime selfLast)
|
||||
{
|
||||
var partyAddon = (AtkUnitBase*)_gameGui.GetAddonByName("_PartyList", 1).Address;
|
||||
if (partyAddon == null || !partyAddon->IsVisible)
|
||||
return;
|
||||
|
||||
var showSelf = _configService.Current.TypingIndicatorShowSelf;
|
||||
if (selfActive
|
||||
&& showSelf
|
||||
&& (now - selfStart) >= TypingDisplayDelay
|
||||
&& (now - selfLast) <= TypingDisplayFade)
|
||||
{
|
||||
@@ -180,14 +187,16 @@ public sealed class TypingIndicatorOverlay : WindowMediatorSubscriberBase
|
||||
ImGui.ColorConvertFloat4ToU32(new Vector4(1f, 1f, 1f, 0.9f)));
|
||||
}
|
||||
|
||||
private unsafe void DrawNameplateIndicators(ImDrawListPtr drawList, IReadOnlyDictionary<string, (UserData User, DateTime FirstSeen, DateTime LastUpdate)> activeTypers,
|
||||
private unsafe void DrawNameplateIndicators(ImDrawListPtr drawList, IReadOnlyDictionary<string, (UserData User, DateTime FirstSeen, DateTime LastUpdate, MareSynchronos.API.Data.Enum.TypingScope Scope)> activeTypers,
|
||||
bool selfActive, DateTime now, DateTime selfStart, DateTime selfLast)
|
||||
{
|
||||
var iconWrap = _textureProvider.GetFromGameIcon(NameplateIconId).GetWrapOrEmpty();
|
||||
if (iconWrap == null || iconWrap.Handle == IntPtr.Zero)
|
||||
return;
|
||||
|
||||
var showSelf = _configService.Current.TypingIndicatorShowSelf;
|
||||
if (selfActive
|
||||
&& showSelf
|
||||
&& _clientState.LocalPlayer != null
|
||||
&& (now - selfStart) >= TypingDisplayDelay
|
||||
&& (now - selfLast) <= TypingDisplayFade)
|
||||
@@ -212,11 +221,22 @@ public sealed class TypingIndicatorOverlay : WindowMediatorSubscriberBase
|
||||
var pairName = pair?.PlayerName ?? entry.User.AliasOrUID ?? string.Empty;
|
||||
var pairIdent = pair?.Ident ?? string.Empty;
|
||||
var isPartyMember = IsPartyMember(objectId, pairName);
|
||||
|
||||
// Enforce party-only visibility when the scope is Party/CrossParty
|
||||
if (entry.Scope is TypingScope.Party or TypingScope.CrossParty)
|
||||
{
|
||||
if (!isPartyMember)
|
||||
{
|
||||
_typedLogger.LogTrace("TypingIndicator: suppressed non-party bubble for {uid} due to scope={scope}", uid, entry.Scope);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
var isRelevantMember = IsPlayerRelevant(pair, isPartyMember);
|
||||
|
||||
if (objectId != uint.MaxValue && objectId != 0 && TryDrawNameplateBubble(drawList, iconWrap, objectId))
|
||||
{
|
||||
_typedLogger.LogTrace("TypingIndicator: drew nameplate bubble for {uid} (objectId={objectId})", uid, objectId);
|
||||
_typedLogger.LogTrace("TypingIndicator: drew nameplate bubble for {uid} (objectId={objectId}, scope={scope})", uid, objectId, entry.Scope);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -226,13 +246,20 @@ public sealed class TypingIndicatorOverlay : WindowMediatorSubscriberBase
|
||||
if (!isRelevantMember && !isNearby)
|
||||
continue;
|
||||
|
||||
// For Party/CrossParty scope, do not draw fallback world icon for non-party even if nearby
|
||||
if (entry.Scope is TypingScope.Party or TypingScope.CrossParty)
|
||||
{
|
||||
if (!isPartyMember)
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pair == null)
|
||||
{
|
||||
_typedLogger.LogTrace("TypingIndicator: no pair found for {uid}, attempting fallback", uid);
|
||||
}
|
||||
|
||||
_typedLogger.LogTrace("TypingIndicator: fallback draw for {uid} (objectId={objectId}, name={name}, ident={ident})",
|
||||
uid, objectId, pairName, pairIdent);
|
||||
_typedLogger.LogTrace("TypingIndicator: fallback draw for {uid} (objectId={objectId}, name={name}, ident={ident}, scope={scope})",
|
||||
uid, objectId, pairName, pairIdent, entry.Scope);
|
||||
|
||||
if (hasWorldPosition)
|
||||
{
|
||||
@@ -318,26 +345,6 @@ public sealed class TypingIndicatorOverlay : WindowMediatorSubscriberBase
|
||||
|
||||
if (!iconVisible)
|
||||
{
|
||||
sizeScaleFactor = 2.5f;
|
||||
var anchor = rootPosition + iconLocalPosition + new Vector2(iconDimensions.X * 0.5f, 0f);
|
||||
|
||||
var distanceOffset = new Vector2(0f, -16f + distance) * scaleVector;
|
||||
if (iconNode->Height == 24)
|
||||
{
|
||||
distanceOffset.Y += 16f * scaleY;
|
||||
}
|
||||
distanceOffset.Y += 64f * scaleY;
|
||||
|
||||
var referenceSize = GetConfiguredBubbleSize(scaleX * sizeScaleFactor, scaleY * sizeScaleFactor, false, TypingIndicatorBubbleSize.Small);
|
||||
var manualOffset = new Vector2(referenceSize.X * 2.00f, referenceSize.Y * 2.00f);
|
||||
|
||||
var iconSizeHidden = GetConfiguredBubbleSize(scaleX * sizeScaleFactor, scaleY * sizeScaleFactor, false);
|
||||
var center = anchor + distanceOffset + manualOffset;
|
||||
var topLeft = center - (iconSizeHidden / 2f);
|
||||
|
||||
drawList.AddImage(textureWrap.Handle, topLeft, topLeft + iconSizeHidden, Vector2.Zero, Vector2.One,
|
||||
ImGui.ColorConvertFloat4ToU32(new Vector4(1f, 1f, 1f, 0.95f)));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -541,6 +541,100 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public bool IconButtonCentered(FontAwesomeIcon icon, float? height = null, float xOffset = 0f, float yOffset = 0f, bool square = false)
|
||||
{
|
||||
string text = icon.ToIconString();
|
||||
|
||||
ImGui.PushID($"centered-{text}");
|
||||
Vector2 glyphSize;
|
||||
using (IconFont.Push())
|
||||
glyphSize = ImGui.CalcTextSize(text);
|
||||
ImDrawListPtr drawList = ImGui.GetWindowDrawList();
|
||||
Vector2 cursorScreenPos = ImGui.GetCursorScreenPos();
|
||||
float frameHeight = height ?? ImGui.GetFrameHeight();
|
||||
float buttonWidth = square ? frameHeight : glyphSize.X + ImGui.GetStyle().FramePadding.X * 2f;
|
||||
using var hoverColor = ImRaii.PushColor(ImGuiCol.ButtonHovered, AccentHoverColor);
|
||||
using var activeColor = ImRaii.PushColor(ImGuiCol.ButtonActive, AccentActiveColor);
|
||||
bool clicked = ImGui.Button(string.Empty, new Vector2(buttonWidth, frameHeight));
|
||||
Vector2 pos = new Vector2(
|
||||
cursorScreenPos.X + (buttonWidth - glyphSize.X) / 2f + xOffset,
|
||||
cursorScreenPos.Y + frameHeight / 2f - glyphSize.Y / 2f + yOffset);
|
||||
using (IconFont.Push())
|
||||
drawList.AddText(pos, ImGui.GetColorU32(ImGuiCol.Text), text);
|
||||
ImGui.PopID();
|
||||
|
||||
return clicked;
|
||||
}
|
||||
public bool IconPauseButtonCentered(float? height = null)
|
||||
{
|
||||
ImGui.PushID("centered-pause-custom");
|
||||
Vector2 glyphSize;
|
||||
using (IconFont.Push())
|
||||
glyphSize = ImGui.CalcTextSize(FontAwesomeIcon.Pause.ToIconString());
|
||||
float frameHeight = height ?? ImGui.GetFrameHeight();
|
||||
float buttonWidth = glyphSize.X + ImGui.GetStyle().FramePadding.X * 2f;
|
||||
|
||||
using var hoverColor = ImRaii.PushColor(ImGuiCol.ButtonHovered, AccentHoverColor);
|
||||
using var activeColor = ImRaii.PushColor(ImGuiCol.ButtonActive, AccentActiveColor);
|
||||
|
||||
var drawList = ImGui.GetWindowDrawList();
|
||||
var buttonTopLeft = ImGui.GetCursorScreenPos();
|
||||
bool clicked = ImGui.Button(string.Empty, new Vector2(buttonWidth, frameHeight));
|
||||
|
||||
var textColor = ImGui.GetColorU32(ImGuiCol.Text);
|
||||
|
||||
float h = frameHeight * 0.55f; // bar height
|
||||
float w = MathF.Max(1f, frameHeight * 0.16f); // bar width
|
||||
float gap = MathF.Max(1f, w * 0.9f); // gap between bars
|
||||
float total = 2f * w + gap;
|
||||
|
||||
float startX = buttonTopLeft.X + (buttonWidth - total) / 2f;
|
||||
float startY = buttonTopLeft.Y + (frameHeight - h) / 2f;
|
||||
float rounding = w * 0.35f;
|
||||
|
||||
drawList.AddRectFilled(new Vector2(startX, startY), new Vector2(startX + w, startY + h), textColor, rounding);
|
||||
float rightX = startX + w + gap;
|
||||
drawList.AddRectFilled(new Vector2(rightX, startY), new Vector2(rightX + w, startY + h), textColor, rounding);
|
||||
|
||||
ImGui.PopID();
|
||||
return clicked;
|
||||
}
|
||||
|
||||
public bool IconPlusButtonCentered(float? height = null)
|
||||
{
|
||||
ImGui.PushID("centered-plus-custom");
|
||||
Vector2 glyphSize;
|
||||
using (IconFont.Push())
|
||||
glyphSize = ImGui.CalcTextSize(FontAwesomeIcon.Plus.ToIconString());
|
||||
float frameHeight = height ?? ImGui.GetFrameHeight();
|
||||
float buttonWidth = glyphSize.X + ImGui.GetStyle().FramePadding.X * 2f;
|
||||
|
||||
using var hoverColor = ImRaii.PushColor(ImGuiCol.ButtonHovered, AccentHoverColor);
|
||||
using var activeColor = ImRaii.PushColor(ImGuiCol.ButtonActive, AccentActiveColor);
|
||||
|
||||
var drawList = ImGui.GetWindowDrawList();
|
||||
var buttonTopLeft = ImGui.GetCursorScreenPos();
|
||||
bool clicked = ImGui.Button(string.Empty, new Vector2(buttonWidth, frameHeight));
|
||||
|
||||
var color = ImGui.GetColorU32(ImGuiCol.Text);
|
||||
|
||||
float armThickness = MathF.Max(1f, frameHeight * 0.14f);
|
||||
float crossSize = frameHeight * 0.55f; // total length of vertical/horizontal arms
|
||||
float startX = buttonTopLeft.X + (buttonWidth - crossSize) / 2f;
|
||||
float startY = buttonTopLeft.Y + (frameHeight - crossSize) / 2f;
|
||||
float endX = startX + crossSize;
|
||||
float endY = startY + crossSize;
|
||||
float r = armThickness * 0.35f;
|
||||
|
||||
float hY1 = startY + (crossSize - armThickness) / 2f;
|
||||
drawList.AddRectFilled(new Vector2(startX, hY1), new Vector2(endX, hY1 + armThickness), color, r);
|
||||
float vX1 = startX + (crossSize - armThickness) / 2f;
|
||||
drawList.AddRectFilled(new Vector2(vX1, startY), new Vector2(vX1 + armThickness, endY), color, r);
|
||||
|
||||
ImGui.PopID();
|
||||
return clicked;
|
||||
}
|
||||
|
||||
private bool IconTextButtonInternal(FontAwesomeIcon icon, string text, Vector4? defaultColor = null, float? width = null, bool useAccentHover = true)
|
||||
{
|
||||
|
||||
@@ -103,6 +103,21 @@ public partial class ApiController
|
||||
await _mareHub!.SendAsync(nameof(UserSetTypingState), isTyping).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task UserSetTypingState(bool isTyping, MareSynchronos.API.Data.Enum.TypingScope scope)
|
||||
{
|
||||
CheckConnection();
|
||||
try
|
||||
{
|
||||
await _mareHub!.SendAsync(nameof(UserSetTypingState), isTyping, scope).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// fallback for older servers without scope support
|
||||
Logger.LogDebug(ex, "UserSetTypingState(scope) not supported on server, falling back to legacy call");
|
||||
await _mareHub!.SendAsync(nameof(UserSetTypingState), isTyping).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task PushCharacterDataInternal(CharacterData character, List<UserData> visibleCharacters)
|
||||
{
|
||||
Logger.LogInformation("Pushing character data for {hash} to {charas}", character.DataHash.Value, string.Join(", ", visibleCharacters.Select(c => c.AliasOrUID)));
|
||||
|
||||
@@ -180,7 +180,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM
|
||||
InitializeApiHooks();
|
||||
|
||||
await _mareHub.StartAsync(token).ConfigureAwait(false);
|
||||
|
||||
|
||||
_connectionDto = await GetConnectionDto().ConfigureAwait(false);
|
||||
|
||||
ServerState = ServerState.Connected;
|
||||
|
||||
@@ -3,8 +3,6 @@ using MareSynchronos.Services;
|
||||
using MareSynchronos.Services.Mediator;
|
||||
using MareSynchronos.Services.ServerConfiguration;
|
||||
using MareSynchronos.WebAPI.SignalR.Utils;
|
||||
using MessagePack;
|
||||
using MessagePack.Resolvers;
|
||||
using Microsoft.AspNetCore.Http.Connections;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -172,25 +170,7 @@ public class HubFactory : MediatorSubscriberBase
|
||||
options.SkipNegotiation = hubConfig.SkipNegotiation && (transports == HttpTransportType.WebSockets);
|
||||
options.Transports = transports;
|
||||
})
|
||||
.AddMessagePackProtocol(opt =>
|
||||
{
|
||||
var resolver = CompositeResolver.Create(StandardResolverAllowPrivate.Instance,
|
||||
BuiltinResolver.Instance,
|
||||
AttributeFormatterResolver.Instance,
|
||||
// replace enum resolver
|
||||
DynamicEnumAsStringResolver.Instance,
|
||||
DynamicGenericResolver.Instance,
|
||||
DynamicUnionResolver.Instance,
|
||||
DynamicObjectResolver.Instance,
|
||||
PrimitiveObjectResolver.Instance,
|
||||
// final fallback(last priority)
|
||||
StandardResolver.Instance);
|
||||
|
||||
opt.SerializerOptions =
|
||||
MessagePackSerializerOptions.Standard
|
||||
.WithCompression(MessagePackCompression.Lz4Block)
|
||||
.WithResolver(resolver);
|
||||
})
|
||||
.AddJsonProtocol()
|
||||
.WithAutomaticReconnect(new ForeverRetryPolicy(Mediator))
|
||||
.ConfigureLogging(a =>
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user