Compare commits
6 Commits
513845b811
...
2.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
d777dc599f
|
|||
|
e3d9300ca3
|
|||
|
699678f641
|
|||
|
7a391e6253
|
|||
|
620ebf9195
|
|||
|
8cc4f34c55
|
3
.gitignore
vendored
3
.gitignore
vendored
@@ -3,6 +3,7 @@
|
|||||||
##
|
##
|
||||||
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
|
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
|
||||||
.idea
|
.idea
|
||||||
|
qodana.yaml
|
||||||
# User-specific files
|
# User-specific files
|
||||||
*.rsuser
|
*.rsuser
|
||||||
*.suo
|
*.suo
|
||||||
@@ -13,6 +14,8 @@
|
|||||||
MareSynchronos/.DS_Store
|
MareSynchronos/.DS_Store
|
||||||
*.zip
|
*.zip
|
||||||
UmbraServer_extracted/
|
UmbraServer_extracted/
|
||||||
|
NuGet.config
|
||||||
|
Directory.Build.props
|
||||||
|
|
||||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||||
*.userprefs
|
*.userprefs
|
||||||
|
|||||||
2
MareAPI
2
MareAPI
Submodule MareAPI updated: 0abb078c21...d105d20507
@@ -87,6 +87,8 @@ public class MareConfig : IMareConfiguration
|
|||||||
public bool ExtraChatTags { get; set; } = false;
|
public bool ExtraChatTags { get; set; } = false;
|
||||||
public bool TypingIndicatorShowOnNameplates { get; set; } = true;
|
public bool TypingIndicatorShowOnNameplates { get; set; } = true;
|
||||||
public bool TypingIndicatorShowOnPartyList { 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 TypingIndicatorBubbleSize TypingIndicatorBubbleSize { get; set; } = TypingIndicatorBubbleSize.Large;
|
||||||
|
|
||||||
public bool MareAPI { get; set; } = true;
|
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>
|
<PropertyGroup>
|
||||||
<SourceRevisionId>build$([System.DateTime]::UtcNow.ToString("yyyy-MM-ddTHH:mm:ss:fffZ"))</SourceRevisionId>
|
<SourceRevisionId>build$([System.DateTime]::UtcNow.ToString("yyyy-MM-ddTHH:mm:ss:fffZ"))</SourceRevisionId>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<DisablePackageVulnerabilityAnalysis>true</DisablePackageVulnerabilityAnalysis>
|
||||||
|
<NoWarn>$(NoWarn);NU1900</NoWarn>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ using MareSynchronos.Services;
|
|||||||
using MareSynchronos.Services.Events;
|
using MareSynchronos.Services.Events;
|
||||||
using MareSynchronos.Services.Mediator;
|
using MareSynchronos.Services.Mediator;
|
||||||
using MareSynchronos.Services.ServerConfiguration;
|
using MareSynchronos.Services.ServerConfiguration;
|
||||||
|
using MareSynchronos.Services.Notifications;
|
||||||
using MareSynchronos.UI;
|
using MareSynchronos.UI;
|
||||||
using MareSynchronos.UI.Components;
|
using MareSynchronos.UI.Components;
|
||||||
using MareSynchronos.UI.Components.Popup;
|
using MareSynchronos.UI.Components.Popup;
|
||||||
@@ -102,6 +103,7 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
collection.AddSingleton<MareSynchronos.Services.AutoDetect.NearbyDiscoveryService>();
|
collection.AddSingleton<MareSynchronos.Services.AutoDetect.NearbyDiscoveryService>();
|
||||||
collection.AddSingleton<MareSynchronos.Services.AutoDetect.NearbyPendingService>();
|
collection.AddSingleton<MareSynchronos.Services.AutoDetect.NearbyPendingService>();
|
||||||
collection.AddSingleton<MareSynchronos.Services.AutoDetect.AutoDetectSuppressionService>();
|
collection.AddSingleton<MareSynchronos.Services.AutoDetect.AutoDetectSuppressionService>();
|
||||||
|
collection.AddSingleton<MareSynchronos.Services.AutoDetect.SyncshellDiscoveryService>();
|
||||||
collection.AddSingleton<MarePlugin>();
|
collection.AddSingleton<MarePlugin>();
|
||||||
collection.AddSingleton<MareProfileManager>();
|
collection.AddSingleton<MareProfileManager>();
|
||||||
collection.AddSingleton<GameObjectHandlerFactory>();
|
collection.AddSingleton<GameObjectHandlerFactory>();
|
||||||
@@ -126,6 +128,7 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
collection.AddSingleton<CharaDataCharacterHandler>();
|
collection.AddSingleton<CharaDataCharacterHandler>();
|
||||||
collection.AddSingleton<CharaDataNearbyManager>();
|
collection.AddSingleton<CharaDataNearbyManager>();
|
||||||
collection.AddSingleton<CharaDataGposeTogetherManager>();
|
collection.AddSingleton<CharaDataGposeTogetherManager>();
|
||||||
|
collection.AddSingleton<McdfShareManager>();
|
||||||
|
|
||||||
collection.AddSingleton<VfxSpawnManager>();
|
collection.AddSingleton<VfxSpawnManager>();
|
||||||
collection.AddSingleton<BlockedCharacterHandler>();
|
collection.AddSingleton<BlockedCharacterHandler>();
|
||||||
@@ -151,6 +154,7 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
collection.AddSingleton<PartyListTypingService>();
|
collection.AddSingleton<PartyListTypingService>();
|
||||||
collection.AddSingleton<TypingIndicatorStateService>();
|
collection.AddSingleton<TypingIndicatorStateService>();
|
||||||
collection.AddSingleton<ChatTwoCompatibilityService>();
|
collection.AddSingleton<ChatTwoCompatibilityService>();
|
||||||
|
collection.AddSingleton<NotificationTracker>();
|
||||||
|
|
||||||
collection.AddSingleton((s) => new MareConfigService(pluginInterface.ConfigDirectory.FullName));
|
collection.AddSingleton((s) => new MareConfigService(pluginInterface.ConfigDirectory.FullName));
|
||||||
collection.AddSingleton((s) => new ServerConfigService(pluginInterface.ConfigDirectory.FullName));
|
collection.AddSingleton((s) => new ServerConfigService(pluginInterface.ConfigDirectory.FullName));
|
||||||
@@ -163,6 +167,7 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
collection.AddSingleton((s) => new ServerBlockConfigService(pluginInterface.ConfigDirectory.FullName));
|
collection.AddSingleton((s) => new ServerBlockConfigService(pluginInterface.ConfigDirectory.FullName));
|
||||||
collection.AddSingleton((s) => new CharaDataConfigService(pluginInterface.ConfigDirectory.FullName));
|
collection.AddSingleton((s) => new CharaDataConfigService(pluginInterface.ConfigDirectory.FullName));
|
||||||
collection.AddSingleton((s) => new RemoteConfigCacheService(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<MareConfigService>());
|
||||||
collection.AddSingleton<IConfigService<IMareConfiguration>>(s => s.GetRequiredService<ServerConfigService>());
|
collection.AddSingleton<IConfigService<IMareConfiguration>>(s => s.GetRequiredService<ServerConfigService>());
|
||||||
collection.AddSingleton<IConfigService<IMareConfiguration>>(s => s.GetRequiredService<NotesConfigService>());
|
collection.AddSingleton<IConfigService<IMareConfiguration>>(s => s.GetRequiredService<NotesConfigService>());
|
||||||
@@ -174,6 +179,7 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
collection.AddSingleton<IConfigService<IMareConfiguration>>(s => s.GetRequiredService<ServerBlockConfigService>());
|
collection.AddSingleton<IConfigService<IMareConfiguration>>(s => s.GetRequiredService<ServerBlockConfigService>());
|
||||||
collection.AddSingleton<IConfigService<IMareConfiguration>>(s => s.GetRequiredService<CharaDataConfigService>());
|
collection.AddSingleton<IConfigService<IMareConfiguration>>(s => s.GetRequiredService<CharaDataConfigService>());
|
||||||
collection.AddSingleton<IConfigService<IMareConfiguration>>(s => s.GetRequiredService<RemoteConfigCacheService>());
|
collection.AddSingleton<IConfigService<IMareConfiguration>>(s => s.GetRequiredService<RemoteConfigCacheService>());
|
||||||
|
collection.AddSingleton<IConfigService<IMareConfiguration>>(s => s.GetRequiredService<NotificationsConfigService>());
|
||||||
collection.AddSingleton<ConfigurationMigrator>();
|
collection.AddSingleton<ConfigurationMigrator>();
|
||||||
collection.AddSingleton<ConfigurationSaveService>();
|
collection.AddSingleton<ConfigurationSaveService>();
|
||||||
|
|
||||||
@@ -186,6 +192,7 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
collection.AddScoped<CompactUi>();
|
collection.AddScoped<CompactUi>();
|
||||||
collection.AddScoped<EditProfileUi>();
|
collection.AddScoped<EditProfileUi>();
|
||||||
collection.AddScoped<DataAnalysisUi>();
|
collection.AddScoped<DataAnalysisUi>();
|
||||||
|
collection.AddScoped<CharaDataHubUi>();
|
||||||
collection.AddScoped<AutoDetectUi>();
|
collection.AddScoped<AutoDetectUi>();
|
||||||
collection.AddScoped<WindowMediatorSubscriberBase>(sp => sp.GetRequiredService<SettingsUi>());
|
collection.AddScoped<WindowMediatorSubscriberBase>(sp => sp.GetRequiredService<SettingsUi>());
|
||||||
collection.AddScoped<WindowMediatorSubscriberBase>(sp => sp.GetRequiredService<CompactUi>());
|
collection.AddScoped<WindowMediatorSubscriberBase>(sp => sp.GetRequiredService<CompactUi>());
|
||||||
@@ -227,6 +234,7 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
collection.AddHostedService(p => p.GetRequiredService<MarePlugin>());
|
collection.AddHostedService(p => p.GetRequiredService<MarePlugin>());
|
||||||
collection.AddHostedService(p => p.GetRequiredService<IpcProvider>());
|
collection.AddHostedService(p => p.GetRequiredService<IpcProvider>());
|
||||||
collection.AddHostedService(p => p.GetRequiredService<MareSynchronos.Services.AutoDetect.NearbyDiscoveryService>());
|
collection.AddHostedService(p => p.GetRequiredService<MareSynchronos.Services.AutoDetect.NearbyDiscoveryService>());
|
||||||
|
collection.AddHostedService(p => p.GetRequiredService<MareSynchronos.Services.AutoDetect.SyncshellDiscoveryService>());
|
||||||
collection.AddHostedService(p => p.GetRequiredService<ChatTwoCompatibilityService>());
|
collection.AddHostedService(p => p.GetRequiredService<ChatTwoCompatibilityService>());
|
||||||
collection.AddHostedService(p => p.GetRequiredService<MareSynchronos.Services.AutoDetect.AutoDetectSuppressionService>());
|
collection.AddHostedService(p => p.GetRequiredService<MareSynchronos.Services.AutoDetect.AutoDetectSuppressionService>());
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System.Collections.Concurrent;
|
|||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using MareSynchronos.MareConfiguration.Models;
|
using MareSynchronos.MareConfiguration.Models;
|
||||||
using MareSynchronos.Services.Mediator;
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using MareSynchronos.Services.Notifications;
|
||||||
using MareSynchronos.WebAPI;
|
using MareSynchronos.WebAPI;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
@@ -14,17 +15,19 @@ public sealed class NearbyPendingService : IMediatorSubscriber
|
|||||||
private readonly MareMediator _mediator;
|
private readonly MareMediator _mediator;
|
||||||
private readonly ApiController _api;
|
private readonly ApiController _api;
|
||||||
private readonly AutoDetectRequestService _requestService;
|
private readonly AutoDetectRequestService _requestService;
|
||||||
|
private readonly NotificationTracker _notificationTracker;
|
||||||
private readonly ConcurrentDictionary<string, string> _pending = new(StringComparer.Ordinal);
|
private readonly ConcurrentDictionary<string, string> _pending = new(StringComparer.Ordinal);
|
||||||
private static readonly TimeSpan RegexTimeout = TimeSpan.FromSeconds(1);
|
private static readonly TimeSpan RegexTimeout = TimeSpan.FromSeconds(1);
|
||||||
private static readonly Regex ReqRegex = new(@"^Nearby Request: .+ \[(?<uid>[A-Z0-9]+)\]$", RegexOptions.Compiled | RegexOptions.ExplicitCapture, RegexTimeout);
|
private static readonly Regex ReqRegex = new(@"^Nearby Request: .+ \[(?<uid>[A-Z0-9]+)\]$", RegexOptions.Compiled | RegexOptions.ExplicitCapture, RegexTimeout);
|
||||||
private static readonly Regex AcceptRegex = new(@"^Nearby Accept: .+ \[(?<uid>[A-Z0-9]+)\]$", RegexOptions.Compiled | RegexOptions.ExplicitCapture, RegexTimeout);
|
private static readonly Regex AcceptRegex = new(@"^Nearby Accept: .+ \[(?<uid>[A-Z0-9]+)\]$", RegexOptions.Compiled | RegexOptions.ExplicitCapture, RegexTimeout);
|
||||||
|
|
||||||
public NearbyPendingService(ILogger<NearbyPendingService> logger, MareMediator mediator, ApiController api, AutoDetectRequestService requestService)
|
public NearbyPendingService(ILogger<NearbyPendingService> logger, MareMediator mediator, ApiController api, AutoDetectRequestService requestService, NotificationTracker notificationTracker)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_mediator = mediator;
|
_mediator = mediator;
|
||||||
_api = api;
|
_api = api;
|
||||||
_requestService = requestService;
|
_requestService = requestService;
|
||||||
|
_notificationTracker = notificationTracker;
|
||||||
_mediator.Subscribe<NotificationMessage>(this, OnNotification);
|
_mediator.Subscribe<NotificationMessage>(this, OnNotification);
|
||||||
_mediator.Subscribe<ManualPairInviteMessage>(this, OnManualPairInvite);
|
_mediator.Subscribe<ManualPairInviteMessage>(this, OnManualPairInvite);
|
||||||
}
|
}
|
||||||
@@ -46,6 +49,7 @@ public sealed class NearbyPendingService : IMediatorSubscriber
|
|||||||
_ = _api.UserAddPair(new MareSynchronos.API.Dto.User.UserDto(new MareSynchronos.API.Data.UserData(uidA)));
|
_ = _api.UserAddPair(new MareSynchronos.API.Dto.User.UserDto(new MareSynchronos.API.Data.UserData(uidA)));
|
||||||
_pending.TryRemove(uidA, out _);
|
_pending.TryRemove(uidA, out _);
|
||||||
_requestService.RemovePendingRequestByUid(uidA);
|
_requestService.RemovePendingRequestByUid(uidA);
|
||||||
|
_notificationTracker.Remove(NotificationCategory.AutoDetect, uidA);
|
||||||
_logger.LogInformation("NearbyPending: auto-accepted pairing with {uid}", uidA);
|
_logger.LogInformation("NearbyPending: auto-accepted pairing with {uid}", uidA);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -67,6 +71,7 @@ public sealed class NearbyPendingService : IMediatorSubscriber
|
|||||||
catch { name = uid; }
|
catch { name = uid; }
|
||||||
_pending[uid] = name;
|
_pending[uid] = name;
|
||||||
_logger.LogInformation("NearbyPending: received request from {uid} ({name})", uid, name);
|
_logger.LogInformation("NearbyPending: received request from {uid} ({name})", uid, name);
|
||||||
|
_notificationTracker.Upsert(NotificationEntry.AutoDetect(uid, name));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnManualPairInvite(ManualPairInviteMessage msg)
|
private void OnManualPairInvite(ManualPairInviteMessage msg)
|
||||||
@@ -81,12 +86,14 @@ public sealed class NearbyPendingService : IMediatorSubscriber
|
|||||||
_pending[msg.SourceUid] = display;
|
_pending[msg.SourceUid] = display;
|
||||||
_logger.LogInformation("NearbyPending: received manual invite from {uid} ({name})", msg.SourceUid, display);
|
_logger.LogInformation("NearbyPending: received manual invite from {uid} ({name})", msg.SourceUid, display);
|
||||||
_mediator.Publish(new NotificationMessage("Nearby request", $"{display} vous a envoyé une invitation de pair.", NotificationType.Info, TimeSpan.FromSeconds(5)));
|
_mediator.Publish(new NotificationMessage("Nearby request", $"{display} vous a envoyé une invitation de pair.", NotificationType.Info, TimeSpan.FromSeconds(5)));
|
||||||
|
_notificationTracker.Upsert(NotificationEntry.AutoDetect(msg.SourceUid, display));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Remove(string uid)
|
public void Remove(string uid)
|
||||||
{
|
{
|
||||||
_pending.TryRemove(uid, out _);
|
_pending.TryRemove(uid, out _);
|
||||||
_requestService.RemovePendingRequestByUid(uid);
|
_requestService.RemovePendingRequestByUid(uid);
|
||||||
|
_notificationTracker.Remove(NotificationCategory.AutoDetect, uid);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> AcceptAsync(string uid)
|
public async Task<bool> AcceptAsync(string uid)
|
||||||
@@ -97,6 +104,7 @@ public sealed class NearbyPendingService : IMediatorSubscriber
|
|||||||
_pending.TryRemove(uid, out _);
|
_pending.TryRemove(uid, out _);
|
||||||
_requestService.RemovePendingRequestByUid(uid);
|
_requestService.RemovePendingRequestByUid(uid);
|
||||||
_ = _requestService.SendAcceptNotifyAsync(uid);
|
_ = _requestService.SendAcceptNotifyAsync(uid);
|
||||||
|
_notificationTracker.Remove(NotificationCategory.AutoDetect, uid);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|||||||
158
MareSynchronos/Services/AutoDetect/SyncshellDiscoveryService.cs
Normal file
158
MareSynchronos/Services/AutoDetect/SyncshellDiscoveryService.cs
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using MareSynchronos.API.Data;
|
||||||
|
using MareSynchronos.API.Dto.Group;
|
||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using MareSynchronos.WebAPI;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace MareSynchronos.Services.AutoDetect;
|
||||||
|
|
||||||
|
public sealed class SyncshellDiscoveryService : IHostedService, IMediatorSubscriber
|
||||||
|
{
|
||||||
|
private readonly ILogger<SyncshellDiscoveryService> _logger;
|
||||||
|
private readonly MareMediator _mediator;
|
||||||
|
private readonly ApiController _apiController;
|
||||||
|
private readonly SemaphoreSlim _refreshSemaphore = new(1, 1);
|
||||||
|
private readonly object _entriesLock = new();
|
||||||
|
private List<SyncshellDiscoveryEntryDto> _entries = [];
|
||||||
|
private string? _lastError;
|
||||||
|
private bool _isRefreshing;
|
||||||
|
|
||||||
|
public SyncshellDiscoveryService(ILogger<SyncshellDiscoveryService> logger, MareMediator mediator, ApiController apiController)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_mediator = mediator;
|
||||||
|
_apiController = apiController;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MareMediator Mediator => _mediator;
|
||||||
|
|
||||||
|
public IReadOnlyList<SyncshellDiscoveryEntryDto> Entries
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_entriesLock)
|
||||||
|
{
|
||||||
|
return _entries.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsRefreshing => _isRefreshing;
|
||||||
|
public string? LastError => _lastError;
|
||||||
|
|
||||||
|
public async Task<bool> JoinAsync(string gid, CancellationToken ct)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await _apiController.SyncshellDiscoveryJoin(new GroupDto(new GroupData(gid))).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Join syncshell discovery failed for {gid}", gid);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<SyncshellDiscoveryStateDto?> GetStateAsync(string gid, CancellationToken ct)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await _apiController.SyncshellDiscoveryGetState(new GroupDto(new GroupData(gid))).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to fetch syncshell discovery state for {gid}", gid);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
{
|
||||||
|
var request = new SyncshellDiscoveryVisibilityRequestDto
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
|
||||||
|
var state = await GetStateAsync(gid, ct).ConfigureAwait(false);
|
||||||
|
if (state != null)
|
||||||
|
{
|
||||||
|
_mediator.Publish(new SyncshellAutoDetectStateChanged(state.GID, state.AutoDetectVisible, state.PasswordTemporarilyDisabled));
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to set syncshell visibility for {gid}", gid);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RefreshAsync(CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (!await _refreshSemaphore.WaitAsync(0, ct).ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_isRefreshing = true;
|
||||||
|
var discovered = await _apiController.SyncshellDiscoveryList().ConfigureAwait(false);
|
||||||
|
lock (_entriesLock)
|
||||||
|
{
|
||||||
|
_entries = discovered ?? [];
|
||||||
|
}
|
||||||
|
_lastError = null;
|
||||||
|
_mediator.Publish(new SyncshellDiscoveryUpdated(Entries.ToList()));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to refresh syncshell discovery list");
|
||||||
|
_lastError = ex.Message;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_isRefreshing = false;
|
||||||
|
_refreshSemaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_mediator.Subscribe<ConnectedMessage>(this, msg =>
|
||||||
|
{
|
||||||
|
_ = RefreshAsync(CancellationToken.None);
|
||||||
|
});
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StopAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_mediator.UnsubscribeAll(this);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ using MareSynchronos.Services.CharaData.Models;
|
|||||||
using MareSynchronos.Utils;
|
using MareSynchronos.Utils;
|
||||||
using MareSynchronos.WebAPI.Files;
|
using MareSynchronos.WebAPI.Files;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
namespace MareSynchronos.Services;
|
namespace MareSynchronos.Services;
|
||||||
|
|
||||||
@@ -295,6 +296,32 @@ public sealed class CharaDataFileHandler : IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal async Task<byte[]?> CreateCharaFileBytesAsync(string description, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
var tempFilePath = Path.Combine(Path.GetTempPath(), "umbra_mcdfshare_" + Guid.NewGuid().ToString("N") + ".mcdf");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await SaveCharaFileAsync(description, tempFilePath).ConfigureAwait(false);
|
||||||
|
if (!File.Exists(tempFilePath)) return null;
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
return await File.ReadAllBytesAsync(tempFilePath, token).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (File.Exists(tempFilePath))
|
||||||
|
{
|
||||||
|
File.Delete(tempFilePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
internal async Task<List<string>> UploadFiles(List<string> fileList, ValueProgress<string> uploadProgress, CancellationToken token)
|
internal async Task<List<string>> UploadFiles(List<string> fileList, ValueProgress<string> uploadProgress, CancellationToken token)
|
||||||
{
|
{
|
||||||
return await _fileUploadManager.UploadFiles(fileList, uploadProgress, token).ConfigureAwait(false);
|
return await _fileUploadManager.UploadFiles(fileList, uploadProgress, token).ConfigureAwait(false);
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ using MareSynchronos.Utils;
|
|||||||
using MareSynchronos.WebAPI;
|
using MareSynchronos.WebAPI;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
using System.IO;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
namespace MareSynchronos.Services;
|
namespace MareSynchronos.Services;
|
||||||
|
|
||||||
@@ -457,6 +459,14 @@ public sealed partial class CharaDataManager : DisposableMediatorSubscriberBase
|
|||||||
LoadedMcdfHeader = _fileHandler.LoadCharaFileHeader(filePath);
|
LoadedMcdfHeader = _fileHandler.LoadCharaFileHeader(filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<string> LoadMcdfFromBytes(byte[] data, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
var tempFilePath = Path.Combine(Path.GetTempPath(), "umbra_mcdfshare_" + Guid.NewGuid().ToString("N") + ".mcdf");
|
||||||
|
await File.WriteAllBytesAsync(tempFilePath, data, token).ConfigureAwait(false);
|
||||||
|
LoadedMcdfHeader = _fileHandler.LoadCharaFileHeader(tempFilePath);
|
||||||
|
return tempFilePath;
|
||||||
|
}
|
||||||
|
|
||||||
public void McdfApplyToTarget(string charaName)
|
public void McdfApplyToTarget(string charaName)
|
||||||
{
|
{
|
||||||
if (LoadedMcdfHeader == null || !LoadedMcdfHeader.IsCompletedSuccessfully) return;
|
if (LoadedMcdfHeader == null || !LoadedMcdfHeader.IsCompletedSuccessfully) return;
|
||||||
|
|||||||
309
MareSynchronos/Services/CharaData/McdfShareManager.cs
Normal file
309
MareSynchronos/Services/CharaData/McdfShareManager.cs
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using MareSynchronos.API.Dto.McdfShare;
|
||||||
|
using MareSynchronos.MareConfiguration.Models;
|
||||||
|
using MareSynchronos.Services.ServerConfiguration;
|
||||||
|
using MareSynchronos.WebAPI;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace MareSynchronos.Services.CharaData;
|
||||||
|
|
||||||
|
public sealed class McdfShareManager
|
||||||
|
{
|
||||||
|
private readonly ILogger<McdfShareManager> _logger;
|
||||||
|
private readonly ApiController _apiController;
|
||||||
|
private readonly CharaDataFileHandler _fileHandler;
|
||||||
|
private readonly CharaDataManager _charaDataManager;
|
||||||
|
private readonly ServerConfigurationManager _serverConfigurationManager;
|
||||||
|
private readonly SemaphoreSlim _operationSemaphore = new(1, 1);
|
||||||
|
private readonly List<McdfShareEntryDto> _ownShares = new();
|
||||||
|
private readonly List<McdfShareEntryDto> _sharedWithMe = new();
|
||||||
|
private Task? _currentTask;
|
||||||
|
|
||||||
|
public McdfShareManager(ILogger<McdfShareManager> logger, ApiController apiController,
|
||||||
|
CharaDataFileHandler fileHandler, CharaDataManager charaDataManager,
|
||||||
|
ServerConfigurationManager serverConfigurationManager)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_apiController = apiController;
|
||||||
|
_fileHandler = fileHandler;
|
||||||
|
_charaDataManager = charaDataManager;
|
||||||
|
_serverConfigurationManager = serverConfigurationManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<McdfShareEntryDto> OwnShares => _ownShares;
|
||||||
|
public IReadOnlyList<McdfShareEntryDto> SharedShares => _sharedWithMe;
|
||||||
|
public bool IsBusy => _currentTask is { IsCompleted: false };
|
||||||
|
public string? LastError { get; private set; }
|
||||||
|
public string? LastSuccess { get; private set; }
|
||||||
|
|
||||||
|
public Task RefreshAsync(CancellationToken token)
|
||||||
|
{
|
||||||
|
return RunOperation(() => InternalRefreshAsync(token));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task CreateShareAsync(string description, IReadOnlyList<string> allowedIndividuals, IReadOnlyList<string> allowedSyncshells, DateTime? expiresAtUtc, CancellationToken token)
|
||||||
|
{
|
||||||
|
return RunOperation(async () =>
|
||||||
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
var mcdfBytes = await _fileHandler.CreateCharaFileBytesAsync(description, token).ConfigureAwait(false);
|
||||||
|
if (mcdfBytes == null || mcdfBytes.Length == 0)
|
||||||
|
{
|
||||||
|
LastError = "Impossible de préparer les données MCDF.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var secretKey = _serverConfigurationManager.GetSecretKey(out bool hasMultiple);
|
||||||
|
if (hasMultiple)
|
||||||
|
{
|
||||||
|
LastError = "Plusieurs clés secrètes sont configurées pour ce personnage. Corrigez cela dans les paramètres.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(secretKey))
|
||||||
|
{
|
||||||
|
LastError = "Aucune clé secrète n'est configurée pour ce personnage.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var shareId = Guid.NewGuid();
|
||||||
|
byte[] salt = RandomNumberGenerator.GetBytes(16);
|
||||||
|
byte[] nonce = RandomNumberGenerator.GetBytes(12);
|
||||||
|
byte[] key = DeriveKey(secretKey, shareId, salt);
|
||||||
|
|
||||||
|
byte[] cipher = new byte[mcdfBytes.Length];
|
||||||
|
byte[] tag = new byte[16];
|
||||||
|
|
||||||
|
using (var aes = new AesGcm(key, 16))
|
||||||
|
{
|
||||||
|
aes.Encrypt(nonce, mcdfBytes, cipher, tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
var uploadDto = new McdfShareUploadRequestDto
|
||||||
|
{
|
||||||
|
ShareId = shareId,
|
||||||
|
Description = description,
|
||||||
|
CipherData = cipher,
|
||||||
|
Nonce = nonce,
|
||||||
|
Salt = salt,
|
||||||
|
Tag = tag,
|
||||||
|
ExpiresAtUtc = expiresAtUtc,
|
||||||
|
AllowedIndividuals = allowedIndividuals.ToList(),
|
||||||
|
AllowedSyncshells = allowedSyncshells.ToList()
|
||||||
|
};
|
||||||
|
|
||||||
|
await _apiController.McdfShareUpload(uploadDto).ConfigureAwait(false);
|
||||||
|
await InternalRefreshAsync(token).ConfigureAwait(false);
|
||||||
|
LastSuccess = "Partage MCDF créé.";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task DeleteShareAsync(Guid shareId)
|
||||||
|
{
|
||||||
|
return RunOperation(async () =>
|
||||||
|
{
|
||||||
|
var result = await _apiController.McdfShareDelete(shareId).ConfigureAwait(false);
|
||||||
|
if (!result)
|
||||||
|
{
|
||||||
|
LastError = "Le serveur a refusé de supprimer le partage MCDF.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ownShares.RemoveAll(s => s.Id == shareId);
|
||||||
|
_sharedWithMe.RemoveAll(s => s.Id == shareId);
|
||||||
|
await InternalRefreshAsync(CancellationToken.None).ConfigureAwait(false);
|
||||||
|
LastSuccess = "Partage MCDF supprimé.";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task UpdateShareAsync(McdfShareUpdateRequestDto updateRequest)
|
||||||
|
{
|
||||||
|
return RunOperation(async () =>
|
||||||
|
{
|
||||||
|
var updated = await _apiController.McdfShareUpdate(updateRequest).ConfigureAwait(false);
|
||||||
|
if (updated == null)
|
||||||
|
{
|
||||||
|
LastError = "Le serveur a refusé de mettre à jour le partage MCDF.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var idx = _ownShares.FindIndex(s => s.Id == updated.Id);
|
||||||
|
if (idx >= 0)
|
||||||
|
{
|
||||||
|
_ownShares[idx] = updated;
|
||||||
|
}
|
||||||
|
LastSuccess = "Partage MCDF mis à jour.";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task ApplyShareAsync(Guid shareId, CancellationToken token)
|
||||||
|
{
|
||||||
|
return RunOperation(async () =>
|
||||||
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
var plainBytes = await DownloadAndDecryptShareAsync(shareId, token).ConfigureAwait(false);
|
||||||
|
if (plainBytes == null)
|
||||||
|
{
|
||||||
|
LastError ??= "Échec du téléchargement du partage MCDF.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tempPath = await _charaDataManager.LoadMcdfFromBytes(plainBytes, token).ConfigureAwait(false);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _charaDataManager.McdfApplyToGposeTarget().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (File.Exists(tempPath))
|
||||||
|
{
|
||||||
|
File.Delete(tempPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LastSuccess = "Partage MCDF appliqué sur la cible GPose.";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task ExportShareAsync(Guid shareId, string filePath, CancellationToken token)
|
||||||
|
{
|
||||||
|
return RunOperation(async () =>
|
||||||
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
var plainBytes = await DownloadAndDecryptShareAsync(shareId, token).ConfigureAwait(false);
|
||||||
|
if (plainBytes == null)
|
||||||
|
{
|
||||||
|
LastError ??= "Échec du téléchargement du partage MCDF.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var directory = Path.GetDirectoryName(filePath);
|
||||||
|
if (!string.IsNullOrEmpty(directory))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(directory);
|
||||||
|
}
|
||||||
|
|
||||||
|
await File.WriteAllBytesAsync(filePath, plainBytes, token).ConfigureAwait(false);
|
||||||
|
LastSuccess = "Partage MCDF exporté.";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task DownloadShareToFileAsync(McdfShareEntryDto entry, string filePath, CancellationToken token)
|
||||||
|
{
|
||||||
|
return ExportShareAsync(entry.Id, filePath, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<byte[]?> DownloadAndDecryptShareAsync(Guid shareId, CancellationToken token)
|
||||||
|
{
|
||||||
|
var payload = await _apiController.McdfShareDownload(shareId).ConfigureAwait(false);
|
||||||
|
if (payload == null)
|
||||||
|
{
|
||||||
|
LastError = "Partage indisponible.";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var secretKey = _serverConfigurationManager.GetSecretKey(out bool hasMultiple);
|
||||||
|
if (hasMultiple)
|
||||||
|
{
|
||||||
|
LastError = "Plusieurs clés secrètes sont configurées pour ce personnage.";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(secretKey))
|
||||||
|
{
|
||||||
|
LastError = "Aucune clé secrète n'est configurée pour ce personnage.";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] key = DeriveKey(secretKey, payload.ShareId, payload.Salt);
|
||||||
|
byte[] plaintext = new byte[payload.CipherData.Length];
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var aes = new AesGcm(key, 16);
|
||||||
|
aes.Decrypt(payload.Nonce, payload.CipherData, payload.Tag, plaintext);
|
||||||
|
}
|
||||||
|
catch (CryptographicException ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to decrypt MCDF share {ShareId}", shareId);
|
||||||
|
LastError = "Impossible de déchiffrer le partage MCDF.";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
return plaintext;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task InternalRefreshAsync(CancellationToken token)
|
||||||
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
var own = await _apiController.McdfShareGetOwn().ConfigureAwait(false);
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
var shared = await _apiController.McdfShareGetShared().ConfigureAwait(false);
|
||||||
|
_ownShares.Clear();
|
||||||
|
_ownShares.AddRange(own);
|
||||||
|
_sharedWithMe.Clear();
|
||||||
|
_sharedWithMe.AddRange(shared);
|
||||||
|
LastSuccess = "Partages MCDF actualisés.";
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task RunOperation(Func<Task> operation)
|
||||||
|
{
|
||||||
|
async Task Wrapper()
|
||||||
|
{
|
||||||
|
await _operationSemaphore.WaitAsync().ConfigureAwait(false);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
LastError = null;
|
||||||
|
LastSuccess = null;
|
||||||
|
await operation().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error during MCDF share operation");
|
||||||
|
LastError = ex.Message;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_operationSemaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var task = Wrapper();
|
||||||
|
_currentTask = task;
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] DeriveKey(string secretKey, Guid shareId, byte[] salt)
|
||||||
|
{
|
||||||
|
byte[] secretBytes;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
secretBytes = Convert.FromHexString(secretKey);
|
||||||
|
}
|
||||||
|
catch (FormatException)
|
||||||
|
{
|
||||||
|
// fallback to UTF8 if not hex
|
||||||
|
secretBytes = System.Text.Encoding.UTF8.GetBytes(secretKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] shareBytes = shareId.ToByteArray();
|
||||||
|
byte[] material = new byte[secretBytes.Length + shareBytes.Length + salt.Length];
|
||||||
|
Buffer.BlockCopy(secretBytes, 0, material, 0, secretBytes.Length);
|
||||||
|
Buffer.BlockCopy(shareBytes, 0, material, secretBytes.Length, shareBytes.Length);
|
||||||
|
Buffer.BlockCopy(salt, 0, material, secretBytes.Length + shareBytes.Length, salt.Length);
|
||||||
|
return SHA256.HashData(material);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ using Dalamud.Plugin.Services;
|
|||||||
using MareSynchronos.API.Data;
|
using MareSynchronos.API.Data;
|
||||||
using MareSynchronos.Interop;
|
using MareSynchronos.Interop;
|
||||||
using MareSynchronos.MareConfiguration;
|
using MareSynchronos.MareConfiguration;
|
||||||
|
using MareSynchronos.API.Data.Enum;
|
||||||
using MareSynchronos.MareConfiguration.Models;
|
using MareSynchronos.MareConfiguration.Models;
|
||||||
using MareSynchronos.PlayerData.Pairs;
|
using MareSynchronos.PlayerData.Pairs;
|
||||||
using MareSynchronos.Services.Mediator;
|
using MareSynchronos.Services.Mediator;
|
||||||
@@ -38,6 +39,7 @@ public class ChatService : DisposableMediatorSubscriberBase
|
|||||||
private CancellationTokenSource? _typingCts;
|
private CancellationTokenSource? _typingCts;
|
||||||
private bool _isTypingAnnounced;
|
private bool _isTypingAnnounced;
|
||||||
private DateTime _lastTypingSent = DateTime.MinValue;
|
private DateTime _lastTypingSent = DateTime.MinValue;
|
||||||
|
private TypingScope _lastScope = TypingScope.Unknown;
|
||||||
private static readonly TimeSpan TypingIdle = TimeSpan.FromSeconds(2);
|
private static readonly TimeSpan TypingIdle = TimeSpan.FromSeconds(2);
|
||||||
private static readonly TimeSpan TypingResendInterval = TimeSpan.FromMilliseconds(750);
|
private static readonly TimeSpan TypingResendInterval = TimeSpan.FromMilliseconds(750);
|
||||||
|
|
||||||
@@ -79,7 +81,7 @@ public class ChatService : DisposableMediatorSubscriberBase
|
|||||||
if (_gameChatHooks.IsValueCreated)
|
if (_gameChatHooks.IsValueCreated)
|
||||||
_gameChatHooks.Value!.Dispose();
|
_gameChatHooks.Value!.Dispose();
|
||||||
}
|
}
|
||||||
public void NotifyTypingKeystroke()
|
public void NotifyTypingKeystroke(TypingScope scope)
|
||||||
{
|
{
|
||||||
lock (_typingLock)
|
lock (_typingLock)
|
||||||
{
|
{
|
||||||
@@ -88,11 +90,12 @@ public class ChatService : DisposableMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
_ = Task.Run(async () =>
|
_ = 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"); }
|
catch (Exception ex) { _logger.LogDebug(ex, "NotifyTypingKeystroke: failed to send typing=true"); }
|
||||||
});
|
});
|
||||||
_isTypingAnnounced = true;
|
_isTypingAnnounced = true;
|
||||||
_lastTypingSent = now;
|
_lastTypingSent = now;
|
||||||
|
_lastScope = scope;
|
||||||
}
|
}
|
||||||
|
|
||||||
_typingCts?.Cancel();
|
_typingCts?.Cancel();
|
||||||
@@ -105,7 +108,7 @@ public class ChatService : DisposableMediatorSubscriberBase
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
await Task.Delay(TypingIdle, token).ConfigureAwait(false);
|
await Task.Delay(TypingIdle, token).ConfigureAwait(false);
|
||||||
await _apiController.UserSetTypingState(false).ConfigureAwait(false);
|
await _apiController.UserSetTypingState(false, _lastScope).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (TaskCanceledException)
|
catch (TaskCanceledException)
|
||||||
{
|
{
|
||||||
@@ -140,7 +143,7 @@ public class ChatService : DisposableMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
_ = Task.Run(async () =>
|
_ = 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"); }
|
catch (Exception ex) { _logger.LogDebug(ex, "ClearTypingState: failed to send typing=false"); }
|
||||||
});
|
});
|
||||||
_isTypingAnnounced = false;
|
_isTypingAnnounced = false;
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ using FFXIVClientStructs.FFXIV.Client.UI.Shell;
|
|||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using MareSynchronos.PlayerData.Pairs;
|
using MareSynchronos.PlayerData.Pairs;
|
||||||
using MareSynchronos.WebAPI;
|
using MareSynchronos.WebAPI;
|
||||||
|
using MareSynchronos.MareConfiguration;
|
||||||
|
using MareSynchronos.API.Data.Enum;
|
||||||
|
|
||||||
namespace MareSynchronos.Services;
|
namespace MareSynchronos.Services;
|
||||||
|
|
||||||
@@ -24,16 +26,18 @@ public sealed class ChatTypingDetectionService : IDisposable
|
|||||||
private readonly ApiController _apiController;
|
private readonly ApiController _apiController;
|
||||||
private readonly PairManager _pairManager;
|
private readonly PairManager _pairManager;
|
||||||
private readonly IPartyList _partyList;
|
private readonly IPartyList _partyList;
|
||||||
|
private readonly MareConfigService _configService;
|
||||||
|
|
||||||
private string _lastChatText = string.Empty;
|
private string _lastChatText = string.Empty;
|
||||||
private bool _isTyping;
|
private bool _isTyping;
|
||||||
private bool _notifyingRemote;
|
private bool _notifyingRemote;
|
||||||
private bool _serverSupportWarnLogged;
|
private bool _serverSupportWarnLogged;
|
||||||
private bool _remoteNotificationsEnabled;
|
private bool _remoteNotificationsEnabled;
|
||||||
|
private bool _subscribed;
|
||||||
|
|
||||||
public ChatTypingDetectionService(ILogger<ChatTypingDetectionService> logger, IFramework framework,
|
public ChatTypingDetectionService(ILogger<ChatTypingDetectionService> logger, IFramework framework,
|
||||||
IClientState clientState, IGameGui gameGui, ChatService chatService, PairManager pairManager, IPartyList partyList,
|
IClientState clientState, IGameGui gameGui, ChatService chatService, PairManager pairManager, IPartyList partyList,
|
||||||
TypingIndicatorStateService typingStateService, ApiController apiController)
|
TypingIndicatorStateService typingStateService, ApiController apiController, MareConfigService configService)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_framework = framework;
|
_framework = framework;
|
||||||
@@ -44,17 +48,50 @@ public sealed class ChatTypingDetectionService : IDisposable
|
|||||||
_partyList = partyList;
|
_partyList = partyList;
|
||||||
_typingStateService = typingStateService;
|
_typingStateService = typingStateService;
|
||||||
_apiController = apiController;
|
_apiController = apiController;
|
||||||
|
_configService = configService;
|
||||||
|
|
||||||
_framework.Update += OnFrameworkUpdate;
|
Subscribe();
|
||||||
_logger.LogInformation("ChatTypingDetectionService initialized");
|
_logger.LogInformation("ChatTypingDetectionService initialized");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_framework.Update -= OnFrameworkUpdate;
|
Unsubscribe();
|
||||||
ResetTypingState();
|
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)
|
private void OnFrameworkUpdate(IFramework framework)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -65,6 +102,13 @@ public sealed class ChatTypingDetectionService : IDisposable
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!_configService.Current.TypingIndicatorEnabled)
|
||||||
|
{
|
||||||
|
ResetTypingState();
|
||||||
|
_chatService.ClearTypingState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!TryGetChatInput(out var chatText) || string.IsNullOrEmpty(chatText))
|
if (!TryGetChatInput(out var chatText) || string.IsNullOrEmpty(chatText))
|
||||||
{
|
{
|
||||||
ResetTypingState();
|
ResetTypingState();
|
||||||
@@ -89,7 +133,8 @@ public sealed class ChatTypingDetectionService : IDisposable
|
|||||||
{
|
{
|
||||||
if (notifyRemote)
|
if (notifyRemote)
|
||||||
{
|
{
|
||||||
_chatService.NotifyTypingKeystroke();
|
var scope = GetCurrentTypingScope();
|
||||||
|
_chatService.NotifyTypingKeystroke(scope);
|
||||||
_notifyingRemote = true;
|
_notifyingRemote = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,6 +165,35 @@ public sealed class ChatTypingDetectionService : IDisposable
|
|||||||
_typingStateService.SetSelfTypingLocal(false);
|
_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)
|
private static bool IsIgnoredCommand(string chatText)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(chatText))
|
if (string.IsNullOrWhiteSpace(chatText))
|
||||||
@@ -146,6 +220,11 @@ public sealed class ChatTypingDetectionService : IDisposable
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
if (!_configService.Current.TypingIndicatorEnabled)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
var supportsTypingState = _apiController.SystemInfoDto.SupportsTypingState;
|
var supportsTypingState = _apiController.SystemInfoDto.SupportsTypingState;
|
||||||
var connected = _apiController.IsConnected;
|
var connected = _apiController.IsConnected;
|
||||||
if (!connected || !supportsTypingState)
|
if (!connected || !supportsTypingState)
|
||||||
|
|||||||
@@ -109,9 +109,23 @@ public sealed class MareMediator : IHostedService, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_loopCts.Cancel();
|
if (_disposed) return;
|
||||||
|
_disposed = true;
|
||||||
|
if (!_loopCts.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_loopCts.Cancel();
|
||||||
|
}
|
||||||
|
catch (ObjectDisposedException)
|
||||||
|
{
|
||||||
|
// already disposed, swallow
|
||||||
|
}
|
||||||
|
}
|
||||||
_loopCts.Dispose();
|
_loopCts.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -115,12 +115,15 @@ public record NearbyEntry(string Name, ushort WorldId, float Distance, bool IsMa
|
|||||||
public record DiscoveryListUpdated(List<NearbyEntry> Entries) : MessageBase;
|
public record DiscoveryListUpdated(List<NearbyEntry> Entries) : MessageBase;
|
||||||
public record NearbyDetectionToggled(bool Enabled) : MessageBase;
|
public record NearbyDetectionToggled(bool Enabled) : MessageBase;
|
||||||
public record AllowPairRequestsToggled(bool Enabled) : MessageBase;
|
public record AllowPairRequestsToggled(bool Enabled) : MessageBase;
|
||||||
|
public record SyncshellDiscoveryUpdated(List<SyncshellDiscoveryEntryDto> Entries) : MessageBase;
|
||||||
|
public record SyncshellAutoDetectStateChanged(string Gid, bool Visible, bool PasswordTemporarilyDisabled) : MessageBase;
|
||||||
public record ManualPairInviteMessage(string SourceUid, string SourceAlias, string TargetUid, string? DisplayName, string InviteId) : MessageBase;
|
public record ManualPairInviteMessage(string SourceUid, string SourceAlias, string TargetUid, string? DisplayName, string InviteId) : MessageBase;
|
||||||
public record ApplyDefaultPairPermissionsMessage(UserPairDto Pair) : MessageBase;
|
public record ApplyDefaultPairPermissionsMessage(UserPairDto Pair) : MessageBase;
|
||||||
public record ApplyDefaultGroupPermissionsMessage(GroupPairFullInfoDto GroupPair) : MessageBase;
|
public record ApplyDefaultGroupPermissionsMessage(GroupPairFullInfoDto GroupPair) : MessageBase;
|
||||||
public record ApplyDefaultsToAllSyncsMessage(string? Context = null, bool? Disabled = null) : MessageBase;
|
public record ApplyDefaultsToAllSyncsMessage(string? Context = null, bool? Disabled = null) : MessageBase;
|
||||||
public record PairSyncOverrideChanged(string Uid, bool? DisableSounds, bool? DisableAnimations, bool? DisableVfx) : MessageBase;
|
public record PairSyncOverrideChanged(string Uid, bool? DisableSounds, bool? DisableAnimations, bool? DisableVfx) : MessageBase;
|
||||||
public record GroupSyncOverrideChanged(string Gid, bool? DisableSounds, bool? DisableAnimations, bool? DisableVfx) : MessageBase;
|
public record GroupSyncOverrideChanged(string Gid, bool? DisableSounds, bool? DisableAnimations, bool? DisableVfx) : MessageBase;
|
||||||
|
public record NotificationStateChanged(int TotalCount) : MessageBase;
|
||||||
|
|
||||||
public record PluginChangeMessage(string InternalName, Version Version, bool IsLoaded) : KeyedMessage(InternalName);
|
public record PluginChangeMessage(string InternalName, Version Version, bool IsLoaded) : KeyedMessage(InternalName);
|
||||||
#pragma warning restore S2094
|
#pragma warning restore S2094
|
||||||
|
|||||||
144
MareSynchronos/Services/Notification/NotificationTracker.cs
Normal file
144
MareSynchronos/Services/Notification/NotificationTracker.cs
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
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, NotificationsConfigService configService)
|
||||||
|
{
|
||||||
|
_mediator = mediator;
|
||||||
|
_configService = configService;
|
||||||
|
LoadPersisted();
|
||||||
|
PublishState();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Upsert(NotificationEntry entry)
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
_entries[(entry.Category, entry.Id)] = entry;
|
||||||
|
TrimIfNecessary_NoLock();
|
||||||
|
Persist_NoLock();
|
||||||
|
}
|
||||||
|
PublishState();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Remove(NotificationCategory category, string id)
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
_entries.Remove((category, id));
|
||||||
|
Persist_NoLock();
|
||||||
|
}
|
||||||
|
PublishState();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<NotificationEntry> GetEntries()
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
return _entries.Values
|
||||||
|
.OrderBy(e => e.CreatedAt)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Count
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
return _entries.Count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PublishState()
|
||||||
|
{
|
||||||
|
_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;
|
||||||
|
using System.Linq;
|
||||||
using Dalamud.Game.Text.SeStringHandling;
|
using Dalamud.Game.Text.SeStringHandling;
|
||||||
using Dalamud.Interface.ImGuiNotification;
|
using Dalamud.Interface.ImGuiNotification;
|
||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
@@ -17,22 +18,29 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
|
|||||||
private readonly INotificationManager _notificationManager;
|
private readonly INotificationManager _notificationManager;
|
||||||
private readonly IChatGui _chatGui;
|
private readonly IChatGui _chatGui;
|
||||||
private readonly MareConfigService _configurationService;
|
private readonly MareConfigService _configurationService;
|
||||||
|
private readonly Services.Notifications.NotificationTracker _notificationTracker;
|
||||||
|
private readonly PlayerData.Pairs.PairManager _pairManager;
|
||||||
|
|
||||||
public NotificationService(ILogger<NotificationService> logger, MareMediator mediator,
|
public NotificationService(ILogger<NotificationService> logger, MareMediator mediator,
|
||||||
DalamudUtilService dalamudUtilService,
|
DalamudUtilService dalamudUtilService,
|
||||||
INotificationManager notificationManager,
|
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;
|
_dalamudUtilService = dalamudUtilService;
|
||||||
_notificationManager = notificationManager;
|
_notificationManager = notificationManager;
|
||||||
_chatGui = chatGui;
|
_chatGui = chatGui;
|
||||||
_configurationService = configurationService;
|
_configurationService = configurationService;
|
||||||
|
_notificationTracker = notificationTracker;
|
||||||
|
_pairManager = pairManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task StartAsync(CancellationToken cancellationToken)
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
Mediator.Subscribe<NotificationMessage>(this, ShowNotification);
|
Mediator.Subscribe<NotificationMessage>(this, ShowNotification);
|
||||||
Mediator.Subscribe<DualNotificationMessage>(this, ShowDualNotification);
|
Mediator.Subscribe<DualNotificationMessage>(this, ShowDualNotification);
|
||||||
|
Mediator.Subscribe<Services.Mediator.SyncshellAutoDetectStateChanged>(this, OnSyncshellAutoDetectStateChanged);
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,6 +121,31 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
|
|||||||
ShowChat(baseMsg);
|
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)
|
private static bool ShouldForceChat(NotificationMessage msg, out bool appendInstruction)
|
||||||
{
|
{
|
||||||
appendInstruction = false;
|
appendInstruction = false;
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ public class PartyListTypingService : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
public void Draw()
|
public void Draw()
|
||||||
{
|
{
|
||||||
|
if (!_configService.Current.TypingIndicatorEnabled) return;
|
||||||
if (!_configService.Current.TypingIndicatorShowOnPartyList) return;
|
if (!_configService.Current.TypingIndicatorShowOnPartyList) return;
|
||||||
// Build map of visible users by AliasOrUID -> UID (case-insensitive)
|
|
||||||
var visibleByAlias = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
var visibleByAlias = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,24 +5,27 @@ using MareSynchronos.Services.Mediator;
|
|||||||
using MareSynchronos.WebAPI;
|
using MareSynchronos.WebAPI;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using MareSynchronos.API.Data;
|
using MareSynchronos.API.Data;
|
||||||
|
using MareSynchronos.MareConfiguration;
|
||||||
|
|
||||||
namespace MareSynchronos.Services;
|
namespace MareSynchronos.Services;
|
||||||
|
|
||||||
public sealed class TypingIndicatorStateService : IMediatorSubscriber, IDisposable
|
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 ConcurrentDictionary<string, TypingEntry> _typingUsers = new(StringComparer.Ordinal);
|
||||||
private readonly ApiController _apiController;
|
private readonly ApiController _apiController;
|
||||||
private readonly ILogger<TypingIndicatorStateService> _logger;
|
private readonly ILogger<TypingIndicatorStateService> _logger;
|
||||||
|
private readonly MareConfigService _configService;
|
||||||
private DateTime _selfTypingLast = DateTime.MinValue;
|
private DateTime _selfTypingLast = DateTime.MinValue;
|
||||||
private DateTime _selfTypingStart = DateTime.MinValue;
|
private DateTime _selfTypingStart = DateTime.MinValue;
|
||||||
private bool _selfTypingActive;
|
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;
|
_logger = logger;
|
||||||
_apiController = apiController;
|
_apiController = apiController;
|
||||||
|
_configService = configService;
|
||||||
Mediator = mediator;
|
Mediator = mediator;
|
||||||
|
|
||||||
mediator.Subscribe<UserTypingStateMessage>(this, OnTypingState);
|
mediator.Subscribe<UserTypingStateMessage>(this, OnTypingState);
|
||||||
@@ -51,8 +54,19 @@ public sealed class TypingIndicatorStateService : IMediatorSubscriber, IDisposab
|
|||||||
_selfTypingActive = isTyping;
|
_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)
|
private void OnTypingState(UserTypingStateMessage msg)
|
||||||
{
|
{
|
||||||
|
if (!_configService.Current.TypingIndicatorEnabled)
|
||||||
|
return;
|
||||||
var uid = msg.Typing.User.UID;
|
var uid = msg.Typing.User.UID;
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
|
|
||||||
@@ -74,8 +88,8 @@ public sealed class TypingIndicatorStateService : IMediatorSubscriber, IDisposab
|
|||||||
else if (msg.Typing.IsTyping)
|
else if (msg.Typing.IsTyping)
|
||||||
{
|
{
|
||||||
_typingUsers.AddOrUpdate(uid,
|
_typingUsers.AddOrUpdate(uid,
|
||||||
_ => new TypingEntry(msg.Typing.User, now, now),
|
_ => new TypingEntry(msg.Typing.User, now, now, msg.Typing.Scope),
|
||||||
(_, existing) => new TypingEntry(msg.Typing.User, existing.FirstSeen, now));
|
(_, existing) => new TypingEntry(msg.Typing.User, existing.FirstSeen, now, msg.Typing.Scope));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -101,7 +115,7 @@ public sealed class TypingIndicatorStateService : IMediatorSubscriber, IDisposab
|
|||||||
return true;
|
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;
|
var now = DateTime.UtcNow;
|
||||||
foreach (var kvp in _typingUsers.ToArray())
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
using MareSynchronos.API.Dto.Group;
|
using MareSynchronos.API.Dto.Group;
|
||||||
using MareSynchronos.PlayerData.Pairs;
|
using MareSynchronos.PlayerData.Pairs;
|
||||||
|
using MareSynchronos.Services.AutoDetect;
|
||||||
using MareSynchronos.Services.Mediator;
|
using MareSynchronos.Services.Mediator;
|
||||||
using MareSynchronos.Services.ServerConfiguration;
|
using MareSynchronos.Services.ServerConfiguration;
|
||||||
|
using MareSynchronos.Services.Notifications;
|
||||||
using MareSynchronos.UI;
|
using MareSynchronos.UI;
|
||||||
using MareSynchronos.UI.Components.Popup;
|
using MareSynchronos.UI.Components.Popup;
|
||||||
using MareSynchronos.WebAPI;
|
using MareSynchronos.WebAPI;
|
||||||
@@ -19,25 +21,29 @@ public class UiFactory
|
|||||||
private readonly ServerConfigurationManager _serverConfigManager;
|
private readonly ServerConfigurationManager _serverConfigManager;
|
||||||
private readonly MareProfileManager _mareProfileManager;
|
private readonly MareProfileManager _mareProfileManager;
|
||||||
private readonly PerformanceCollectorService _performanceCollectorService;
|
private readonly PerformanceCollectorService _performanceCollectorService;
|
||||||
|
private readonly SyncshellDiscoveryService _syncshellDiscoveryService;
|
||||||
|
private readonly NotificationTracker _notificationTracker;
|
||||||
|
|
||||||
public UiFactory(ILoggerFactory loggerFactory, MareMediator mareMediator, ApiController apiController,
|
public UiFactory(ILoggerFactory loggerFactory, MareMediator mareMediator, ApiController apiController,
|
||||||
UiSharedService uiSharedService, PairManager pairManager, ServerConfigurationManager serverConfigManager,
|
UiSharedService uiSharedService, PairManager pairManager, SyncshellDiscoveryService syncshellDiscoveryService, ServerConfigurationManager serverConfigManager,
|
||||||
MareProfileManager mareProfileManager, PerformanceCollectorService performanceCollectorService)
|
MareProfileManager mareProfileManager, PerformanceCollectorService performanceCollectorService, NotificationTracker notificationTracker)
|
||||||
{
|
{
|
||||||
_loggerFactory = loggerFactory;
|
_loggerFactory = loggerFactory;
|
||||||
_mareMediator = mareMediator;
|
_mareMediator = mareMediator;
|
||||||
_apiController = apiController;
|
_apiController = apiController;
|
||||||
_uiSharedService = uiSharedService;
|
_uiSharedService = uiSharedService;
|
||||||
_pairManager = pairManager;
|
_pairManager = pairManager;
|
||||||
|
_syncshellDiscoveryService = syncshellDiscoveryService;
|
||||||
_serverConfigManager = serverConfigManager;
|
_serverConfigManager = serverConfigManager;
|
||||||
_mareProfileManager = mareProfileManager;
|
_mareProfileManager = mareProfileManager;
|
||||||
_performanceCollectorService = performanceCollectorService;
|
_performanceCollectorService = performanceCollectorService;
|
||||||
|
_notificationTracker = notificationTracker;
|
||||||
}
|
}
|
||||||
|
|
||||||
public SyncshellAdminUI CreateSyncshellAdminUi(GroupFullInfoDto dto)
|
public SyncshellAdminUI CreateSyncshellAdminUi(GroupFullInfoDto dto)
|
||||||
{
|
{
|
||||||
return new SyncshellAdminUI(_loggerFactory.CreateLogger<SyncshellAdminUI>(), _mareMediator,
|
return new SyncshellAdminUI(_loggerFactory.CreateLogger<SyncshellAdminUI>(), _mareMediator,
|
||||||
_apiController, _uiSharedService, _pairManager, dto, _performanceCollectorService);
|
_apiController, _uiSharedService, _pairManager, _syncshellDiscoveryService, dto, _performanceCollectorService, _notificationTracker);
|
||||||
}
|
}
|
||||||
|
|
||||||
public StandaloneProfileUi CreateStandaloneProfileUi(Pair pair)
|
public StandaloneProfileUi CreateStandaloneProfileUi(Pair pair)
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Dalamud.Bindings.ImGui;
|
using Dalamud.Bindings.ImGui;
|
||||||
using Dalamud.Interface.Colors;
|
using Dalamud.Interface.Colors;
|
||||||
using Dalamud.Interface.Utility;
|
using Dalamud.Interface.Utility;
|
||||||
using Dalamud.Interface.Utility.Raii;
|
using Dalamud.Interface.Utility.Raii;
|
||||||
|
using MareSynchronos.API.Dto.Group;
|
||||||
using MareSynchronos.MareConfiguration;
|
using MareSynchronos.MareConfiguration;
|
||||||
using MareSynchronos.PlayerData.Pairs;
|
using MareSynchronos.PlayerData.Pairs;
|
||||||
using MareSynchronos.Services.Mediator;
|
using MareSynchronos.Services.Mediator;
|
||||||
@@ -29,11 +31,16 @@ public class AutoDetectUi : WindowMediatorSubscriberBase
|
|||||||
private readonly PairManager _pairManager;
|
private readonly PairManager _pairManager;
|
||||||
private List<Services.Mediator.NearbyEntry> _entries;
|
private List<Services.Mediator.NearbyEntry> _entries;
|
||||||
private readonly HashSet<string> _acceptInFlight = new(StringComparer.Ordinal);
|
private readonly HashSet<string> _acceptInFlight = new(StringComparer.Ordinal);
|
||||||
|
private readonly SyncshellDiscoveryService _syncshellDiscoveryService;
|
||||||
|
private List<SyncshellDiscoveryEntryDto> _syncshellEntries = [];
|
||||||
|
private bool _syncshellInitialized;
|
||||||
|
private readonly HashSet<string> _syncshellJoinInFlight = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
private string? _syncshellLastError;
|
||||||
|
|
||||||
public AutoDetectUi(ILogger<AutoDetectUi> logger, MareMediator mediator,
|
public AutoDetectUi(ILogger<AutoDetectUi> logger, MareMediator mediator,
|
||||||
MareConfigService configService, DalamudUtilService dalamudUtilService,
|
MareConfigService configService, DalamudUtilService dalamudUtilService,
|
||||||
AutoDetectRequestService requestService, NearbyPendingService pendingService, PairManager pairManager,
|
AutoDetectRequestService requestService, NearbyPendingService pendingService, PairManager pairManager,
|
||||||
NearbyDiscoveryService discoveryService,
|
NearbyDiscoveryService discoveryService, SyncshellDiscoveryService syncshellDiscoveryService,
|
||||||
PerformanceCollectorService performanceCollectorService)
|
PerformanceCollectorService performanceCollectorService)
|
||||||
: base(logger, mediator, "AutoDetect", performanceCollectorService)
|
: base(logger, mediator, "AutoDetect", performanceCollectorService)
|
||||||
{
|
{
|
||||||
@@ -43,7 +50,9 @@ public class AutoDetectUi : WindowMediatorSubscriberBase
|
|||||||
_pendingService = pendingService;
|
_pendingService = pendingService;
|
||||||
_pairManager = pairManager;
|
_pairManager = pairManager;
|
||||||
_discoveryService = discoveryService;
|
_discoveryService = discoveryService;
|
||||||
|
_syncshellDiscoveryService = syncshellDiscoveryService;
|
||||||
Mediator.Subscribe<Services.Mediator.DiscoveryListUpdated>(this, OnDiscoveryUpdated);
|
Mediator.Subscribe<Services.Mediator.DiscoveryListUpdated>(this, OnDiscoveryUpdated);
|
||||||
|
Mediator.Subscribe<SyncshellDiscoveryUpdated>(this, OnSyncshellDiscoveryUpdated);
|
||||||
_entries = _discoveryService.SnapshotEntries();
|
_entries = _discoveryService.SnapshotEntries();
|
||||||
|
|
||||||
Flags |= ImGuiWindowFlags.NoScrollbar;
|
Flags |= ImGuiWindowFlags.NoScrollbar;
|
||||||
@@ -81,14 +90,7 @@ public class AutoDetectUi : WindowMediatorSubscriberBase
|
|||||||
});
|
});
|
||||||
|
|
||||||
DrawStyledTab("Proximité", accent, inactiveTab, hoverTab, DrawNearbyTab);
|
DrawStyledTab("Proximité", accent, inactiveTab, hoverTab, DrawNearbyTab);
|
||||||
|
DrawStyledTab("Syncshell", accent, inactiveTab, hoverTab, DrawSyncshellTab);
|
||||||
using (ImRaii.Disabled(true))
|
|
||||||
{
|
|
||||||
DrawStyledTab("Syncshell", accent, inactiveTab, hoverTab, () =>
|
|
||||||
{
|
|
||||||
UiSharedService.ColorTextWrapped("Disponible prochainement.", ImGuiColors.DalamudGrey3);
|
|
||||||
}, true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void DrawInline()
|
public void DrawInline()
|
||||||
@@ -221,6 +223,7 @@ public class AutoDetectUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
var sourceEntries = _entries.Count > 0 ? _entries : _discoveryService.SnapshotEntries();
|
var sourceEntries = _entries.Count > 0 ? _entries : _discoveryService.SnapshotEntries();
|
||||||
var orderedEntries = sourceEntries
|
var orderedEntries = sourceEntries
|
||||||
|
.Where(e => e.IsMatch)
|
||||||
.OrderBy(e => float.IsNaN(e.Distance) ? float.MaxValue : e.Distance)
|
.OrderBy(e => float.IsNaN(e.Distance) ? float.MaxValue : e.Distance)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
@@ -245,10 +248,9 @@ public class AutoDetectUi : WindowMediatorSubscriberBase
|
|||||||
for (int i = 0; i < orderedEntries.Count; i++)
|
for (int i = 0; i < orderedEntries.Count; i++)
|
||||||
{
|
{
|
||||||
var entry = orderedEntries[i];
|
var entry = orderedEntries[i];
|
||||||
bool isMatch = entry.IsMatch;
|
|
||||||
bool alreadyPaired = IsAlreadyPairedByUidOrAlias(entry);
|
bool alreadyPaired = IsAlreadyPairedByUidOrAlias(entry);
|
||||||
bool overDistance = !float.IsNaN(entry.Distance) && entry.Distance > maxDist;
|
bool overDistance = !float.IsNaN(entry.Distance) && entry.Distance > maxDist;
|
||||||
bool canRequest = isMatch && entry.AcceptPairRequests && !string.IsNullOrEmpty(entry.Token) && !alreadyPaired;
|
bool canRequest = entry.AcceptPairRequests && !string.IsNullOrEmpty(entry.Token) && !alreadyPaired;
|
||||||
|
|
||||||
string displayName = entry.DisplayName ?? entry.Name;
|
string displayName = entry.DisplayName ?? entry.Name;
|
||||||
string worldName = entry.WorldId == 0
|
string worldName = entry.WorldId == 0
|
||||||
@@ -260,13 +262,11 @@ public class AutoDetectUi : WindowMediatorSubscriberBase
|
|||||||
? "Déjà appairé"
|
? "Déjà appairé"
|
||||||
: overDistance
|
: overDistance
|
||||||
? $"Hors portée (> {maxDist} m)"
|
? $"Hors portée (> {maxDist} m)"
|
||||||
: !isMatch
|
: !entry.AcceptPairRequests
|
||||||
? "Umbra non activé"
|
? "Invitations refusées"
|
||||||
: !entry.AcceptPairRequests
|
: string.IsNullOrEmpty(entry.Token)
|
||||||
? "Invitations refusées"
|
? "Indisponible"
|
||||||
: string.IsNullOrEmpty(entry.Token)
|
: "Disponible";
|
||||||
? "Indisponible"
|
|
||||||
: "Disponible";
|
|
||||||
|
|
||||||
ImGui.TableNextColumn();
|
ImGui.TableNextColumn();
|
||||||
ImGui.TextUnformatted(displayName);
|
ImGui.TextUnformatted(displayName);
|
||||||
@@ -297,13 +297,11 @@ public class AutoDetectUi : WindowMediatorSubscriberBase
|
|||||||
? "Vous êtes déjà appairé avec ce joueur."
|
? "Vous êtes déjà appairé avec ce joueur."
|
||||||
: overDistance
|
: overDistance
|
||||||
? $"Ce joueur est au-delà de la distance maximale configurée ({maxDist} m)."
|
? $"Ce joueur est au-delà de la distance maximale configurée ({maxDist} m)."
|
||||||
: !isMatch
|
: !entry.AcceptPairRequests
|
||||||
? "Ce joueur n'utilise pas UmbraSync ou ne s'est pas rendu détectable."
|
? "Ce joueur a désactivé la réception automatique des invitations."
|
||||||
: !entry.AcceptPairRequests
|
: string.IsNullOrEmpty(entry.Token)
|
||||||
? "Ce joueur a désactivé la réception automatique des invitations."
|
? "Impossible d'obtenir un jeton d'invitation pour ce joueur."
|
||||||
: string.IsNullOrEmpty(entry.Token)
|
: string.Empty;
|
||||||
? "Impossible d'obtenir un jeton d'invitation pour ce joueur."
|
|
||||||
: string.Empty;
|
|
||||||
|
|
||||||
ImGui.TextDisabled(status);
|
ImGui.TextDisabled(status);
|
||||||
if (!string.IsNullOrEmpty(reason))
|
if (!string.IsNullOrEmpty(reason))
|
||||||
@@ -317,11 +315,147 @@ public class AutoDetectUi : WindowMediatorSubscriberBase
|
|||||||
ImGui.EndTable();
|
ImGui.EndTable();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task JoinSyncshellAsync(SyncshellDiscoveryEntryDto entry)
|
||||||
|
{
|
||||||
|
if (!_syncshellJoinInFlight.Add(entry.GID))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var joined = await _syncshellDiscoveryService.JoinAsync(entry.GID, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
if (joined)
|
||||||
|
{
|
||||||
|
Mediator.Publish(new NotificationMessage("AutoDetect Syncshell", $"Rejoint {entry.Alias ?? entry.GID}.", NotificationType.Info, TimeSpan.FromSeconds(5)));
|
||||||
|
await _syncshellDiscoveryService.RefreshAsync(CancellationToken.None).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_syncshellLastError = $"Impossible de rejoindre {entry.Alias ?? entry.GID}.";
|
||||||
|
Mediator.Publish(new NotificationMessage("AutoDetect Syncshell", _syncshellLastError, NotificationType.Warning, TimeSpan.FromSeconds(5)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_syncshellLastError = $"Erreur lors de l'adhésion : {ex.Message}";
|
||||||
|
Mediator.Publish(new NotificationMessage("AutoDetect Syncshell", _syncshellLastError, NotificationType.Error, TimeSpan.FromSeconds(5)));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_syncshellJoinInFlight.Remove(entry.GID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawSyncshellTab()
|
||||||
|
{
|
||||||
|
if (!_syncshellInitialized)
|
||||||
|
{
|
||||||
|
_syncshellInitialized = true;
|
||||||
|
_ = _syncshellDiscoveryService.RefreshAsync(CancellationToken.None);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isRefreshing = _syncshellDiscoveryService.IsRefreshing;
|
||||||
|
var serviceError = _syncshellDiscoveryService.LastError;
|
||||||
|
|
||||||
|
if (ImGui.Button("Actualiser la liste"))
|
||||||
|
{
|
||||||
|
_ = _syncshellDiscoveryService.RefreshAsync(CancellationToken.None);
|
||||||
|
}
|
||||||
|
UiSharedService.AttachToolTip("Met à jour la liste des Syncshells ayant activé l'AutoDetect.");
|
||||||
|
|
||||||
|
if (isRefreshing)
|
||||||
|
{
|
||||||
|
ImGui.SameLine();
|
||||||
|
ImGui.TextDisabled("Actualisation...");
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGuiHelpers.ScaledDummy(4);
|
||||||
|
UiSharedService.TextWrapped("Les Syncshells affichées ont temporairement désactivé leur mot de passe pour permettre un accès direct via AutoDetect. Rejoignez-les uniquement si vous faites confiance aux administrateurs.");
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(serviceError))
|
||||||
|
{
|
||||||
|
UiSharedService.ColorTextWrapped(serviceError, ImGuiColors.DalamudRed);
|
||||||
|
}
|
||||||
|
else if (!string.IsNullOrEmpty(_syncshellLastError))
|
||||||
|
{
|
||||||
|
UiSharedService.ColorTextWrapped(_syncshellLastError!, ImGuiColors.DalamudOrange);
|
||||||
|
}
|
||||||
|
|
||||||
|
var entries = _syncshellEntries.Count > 0 ? _syncshellEntries : _syncshellDiscoveryService.Entries.ToList();
|
||||||
|
if (entries.Count == 0)
|
||||||
|
{
|
||||||
|
ImGuiHelpers.ScaledDummy(4);
|
||||||
|
UiSharedService.ColorTextWrapped("Aucune Syncshell n'est actuellement visible dans AutoDetect.", ImGuiColors.DalamudGrey3);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ImGui.BeginTable("autodetect-syncshells", 5, ImGuiTableFlags.SizingStretchProp | ImGuiTableFlags.RowBg))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.TableSetupColumn("Nom");
|
||||||
|
ImGui.TableSetupColumn("Propriétaire");
|
||||||
|
ImGui.TableSetupColumn("Membres");
|
||||||
|
ImGui.TableSetupColumn("Invitations");
|
||||||
|
ImGui.TableSetupColumn("Action");
|
||||||
|
ImGui.TableHeadersRow();
|
||||||
|
|
||||||
|
foreach (var entry in entries.OrderBy(e => e.Alias ?? e.GID, StringComparer.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
bool alreadyMember = _pairManager.Groups.Keys.Any(g => string.Equals(g.GID, entry.GID, StringComparison.OrdinalIgnoreCase));
|
||||||
|
bool joining = _syncshellJoinInFlight.Contains(entry.GID);
|
||||||
|
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.TextUnformatted(string.IsNullOrEmpty(entry.Alias) ? entry.GID : $"{entry.Alias} ({entry.GID})");
|
||||||
|
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.TextUnformatted(string.IsNullOrEmpty(entry.OwnerAlias) ? entry.OwnerUID : $"{entry.OwnerAlias} ({entry.OwnerUID})");
|
||||||
|
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.TextUnformatted(entry.MemberCount.ToString(CultureInfo.InvariantCulture));
|
||||||
|
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
string inviteMode = entry.AutoAcceptPairs ? "Auto" : "Manuel";
|
||||||
|
ImGui.TextUnformatted(inviteMode);
|
||||||
|
if (!entry.AutoAcceptPairs)
|
||||||
|
{
|
||||||
|
UiSharedService.AttachToolTip("L'administrateur doit approuver manuellement les nouveaux membres.");
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
using (ImRaii.Disabled(alreadyMember || joining))
|
||||||
|
{
|
||||||
|
if (alreadyMember)
|
||||||
|
{
|
||||||
|
ImGui.TextDisabled("Déjà membre");
|
||||||
|
}
|
||||||
|
else if (joining)
|
||||||
|
{
|
||||||
|
ImGui.TextDisabled("Connexion...");
|
||||||
|
}
|
||||||
|
else if (ImGui.Button("Rejoindre"))
|
||||||
|
{
|
||||||
|
_syncshellLastError = null;
|
||||||
|
_ = JoinSyncshellAsync(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.EndTable();
|
||||||
|
}
|
||||||
|
|
||||||
private void OnDiscoveryUpdated(Services.Mediator.DiscoveryListUpdated msg)
|
private void OnDiscoveryUpdated(Services.Mediator.DiscoveryListUpdated msg)
|
||||||
{
|
{
|
||||||
_entries = msg.Entries;
|
_entries = msg.Entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnSyncshellDiscoveryUpdated(SyncshellDiscoveryUpdated msg)
|
||||||
|
{
|
||||||
|
_syncshellEntries = msg.Entries;
|
||||||
|
}
|
||||||
|
|
||||||
private bool IsAlreadyPairedByUidOrAlias(Services.Mediator.NearbyEntry e)
|
private bool IsAlreadyPairedByUidOrAlias(Services.Mediator.NearbyEntry e)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ using System.Text;
|
|||||||
|
|
||||||
namespace MareSynchronos.UI;
|
namespace MareSynchronos.UI;
|
||||||
|
|
||||||
internal sealed partial class CharaDataHubUi
|
public sealed partial class CharaDataHubUi
|
||||||
{
|
{
|
||||||
private static string GetAccessTypeString(AccessTypeDto dto) => dto switch
|
private static string GetAccessTypeString(AccessTypeDto dto) => dto switch
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ using MareSynchronos.Services.CharaData.Models;
|
|||||||
|
|
||||||
namespace MareSynchronos.UI;
|
namespace MareSynchronos.UI;
|
||||||
|
|
||||||
internal sealed partial class CharaDataHubUi
|
public sealed partial class CharaDataHubUi
|
||||||
{
|
{
|
||||||
private string _joinLobbyId = string.Empty;
|
private string _joinLobbyId = string.Empty;
|
||||||
private void DrawGposeTogether()
|
private void DrawGposeTogether()
|
||||||
@@ -90,7 +90,7 @@ internal sealed partial class CharaDataHubUi
|
|||||||
if (!_uiSharedService.IsInGpose)
|
if (!_uiSharedService.IsInGpose)
|
||||||
{
|
{
|
||||||
ImGuiHelpers.ScaledDummy(5);
|
ImGuiHelpers.ScaledDummy(5);
|
||||||
UiSharedService.DrawGroupedCenteredColorText("Assigning users to characters is only available in GPose.", ImGuiColors.DalamudYellow, 300);
|
UiSharedService.DrawGroupedCenteredColorText("Assigning users to characters is only available in GPose.", UiSharedService.AccentColor, 300);
|
||||||
}
|
}
|
||||||
UiSharedService.DistanceSeparator();
|
UiSharedService.DistanceSeparator();
|
||||||
ImGui.TextUnformatted("Users In Lobby");
|
ImGui.TextUnformatted("Users In Lobby");
|
||||||
@@ -104,7 +104,7 @@ internal sealed partial class CharaDataHubUi
|
|||||||
|
|
||||||
if (!_charaDataGposeTogetherManager.UsersInLobby.Any() && !string.IsNullOrEmpty(_charaDataGposeTogetherManager.CurrentGPoseLobbyId))
|
if (!_charaDataGposeTogetherManager.UsersInLobby.Any() && !string.IsNullOrEmpty(_charaDataGposeTogetherManager.CurrentGPoseLobbyId))
|
||||||
{
|
{
|
||||||
UiSharedService.DrawGroupedCenteredColorText("No other users in current GPose lobby", ImGuiColors.DalamudYellow);
|
UiSharedService.DrawGroupedCenteredColorText("No other users in current GPose lobby", UiSharedService.AccentColor);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ using System.Numerics;
|
|||||||
|
|
||||||
namespace MareSynchronos.UI;
|
namespace MareSynchronos.UI;
|
||||||
|
|
||||||
internal sealed partial class CharaDataHubUi
|
public sealed partial class CharaDataHubUi
|
||||||
{
|
{
|
||||||
private void DrawEditCharaData(CharaDataFullExtendedDto? dataDto)
|
private void DrawEditCharaData(CharaDataFullExtendedDto? dataDto)
|
||||||
{
|
{
|
||||||
@@ -18,7 +18,7 @@ internal sealed partial class CharaDataHubUi
|
|||||||
if (dataDto == null)
|
if (dataDto == null)
|
||||||
{
|
{
|
||||||
ImGuiHelpers.ScaledDummy(5);
|
ImGuiHelpers.ScaledDummy(5);
|
||||||
UiSharedService.DrawGroupedCenteredColorText("Select an entry above to edit its data.", ImGuiColors.DalamudYellow);
|
UiSharedService.DrawGroupedCenteredColorText("Select an entry above to edit its data.", UiSharedService.AccentColor);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ internal sealed partial class CharaDataHubUi
|
|||||||
|
|
||||||
if (updateDto == null)
|
if (updateDto == null)
|
||||||
{
|
{
|
||||||
UiSharedService.DrawGroupedCenteredColorText("Something went awfully wrong and there's no update DTO. Try updating Character Data via the button above.", ImGuiColors.DalamudYellow);
|
UiSharedService.DrawGroupedCenteredColorText("Something went awfully wrong and there's no update DTO. Try updating Character Data via the button above.", UiSharedService.AccentColor);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ internal sealed partial class CharaDataHubUi
|
|||||||
}
|
}
|
||||||
if (_charaDataManager.CharaUpdateTask != null && !_charaDataManager.CharaUpdateTask.IsCompleted)
|
if (_charaDataManager.CharaUpdateTask != null && !_charaDataManager.CharaUpdateTask.IsCompleted)
|
||||||
{
|
{
|
||||||
UiSharedService.ColorTextWrapped("Updating data on server, please wait.", ImGuiColors.DalamudYellow);
|
UiSharedService.ColorTextWrapped("Updating data on server, please wait.", UiSharedService.AccentColor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ internal sealed partial class CharaDataHubUi
|
|||||||
{
|
{
|
||||||
if (_charaDataManager.UploadProgress != null)
|
if (_charaDataManager.UploadProgress != null)
|
||||||
{
|
{
|
||||||
UiSharedService.ColorTextWrapped(_charaDataManager.UploadProgress.Value ?? string.Empty, ImGuiColors.DalamudYellow);
|
UiSharedService.ColorTextWrapped(_charaDataManager.UploadProgress.Value ?? string.Empty, UiSharedService.AccentColor);
|
||||||
}
|
}
|
||||||
if ((!_charaDataManager.UploadTask?.IsCompleted ?? false) && _uiSharedService.IconTextButton(FontAwesomeIcon.Ban, "Cancel Upload"))
|
if ((!_charaDataManager.UploadTask?.IsCompleted ?? false) && _uiSharedService.IconTextButton(FontAwesomeIcon.Ban, "Cancel Upload"))
|
||||||
{
|
{
|
||||||
@@ -230,7 +230,7 @@ internal sealed partial class CharaDataHubUi
|
|||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
ImGuiHelpers.ScaledDummy(20, 1);
|
ImGuiHelpers.ScaledDummy(20, 1);
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
UiSharedService.ColorTextWrapped("New data was set. It may contain files that require to be uploaded (will happen on Saving to server)", ImGuiColors.DalamudYellow);
|
UiSharedService.ColorTextWrapped("New data was set. It may contain files that require to be uploaded (will happen on Saving to server)", UiSharedService.AccentColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui.TextUnformatted("Contains Manipulation Data");
|
ImGui.TextUnformatted("Contains Manipulation Data");
|
||||||
@@ -385,7 +385,7 @@ internal sealed partial class CharaDataHubUi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudYellow, poseCount == maxPoses))
|
using (ImRaii.PushColor(ImGuiCol.Text, UiSharedService.AccentColor, poseCount == maxPoses))
|
||||||
ImGui.TextUnformatted($"{poseCount}/{maxPoses} poses attached");
|
ImGui.TextUnformatted($"{poseCount}/{maxPoses} poses attached");
|
||||||
ImGuiHelpers.ScaledDummy(5);
|
ImGuiHelpers.ScaledDummy(5);
|
||||||
|
|
||||||
@@ -395,7 +395,7 @@ internal sealed partial class CharaDataHubUi
|
|||||||
if (!_uiSharedService.IsInGpose && _charaDataManager.BrioAvailable)
|
if (!_uiSharedService.IsInGpose && _charaDataManager.BrioAvailable)
|
||||||
{
|
{
|
||||||
ImGuiHelpers.ScaledDummy(5);
|
ImGuiHelpers.ScaledDummy(5);
|
||||||
UiSharedService.DrawGroupedCenteredColorText("To attach pose and world data you need to be in GPose.", ImGuiColors.DalamudYellow);
|
UiSharedService.DrawGroupedCenteredColorText("To attach pose and world data you need to be in GPose.", UiSharedService.AccentColor);
|
||||||
ImGuiHelpers.ScaledDummy(5);
|
ImGuiHelpers.ScaledDummy(5);
|
||||||
}
|
}
|
||||||
else if (!_charaDataManager.BrioAvailable)
|
else if (!_charaDataManager.BrioAvailable)
|
||||||
@@ -414,7 +414,7 @@ internal sealed partial class CharaDataHubUi
|
|||||||
if (pose.Id == null)
|
if (pose.Id == null)
|
||||||
{
|
{
|
||||||
ImGui.SameLine(50);
|
ImGui.SameLine(50);
|
||||||
_uiSharedService.IconText(FontAwesomeIcon.Plus, ImGuiColors.DalamudYellow);
|
_uiSharedService.IconText(FontAwesomeIcon.Plus, UiSharedService.AccentColor);
|
||||||
UiSharedService.AttachToolTip("This pose has not been added to the server yet. Save changes to upload this Pose data.");
|
UiSharedService.AttachToolTip("This pose has not been added to the server yet. Save changes to upload this Pose data.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -422,14 +422,14 @@ internal sealed partial class CharaDataHubUi
|
|||||||
if (poseHasChanges)
|
if (poseHasChanges)
|
||||||
{
|
{
|
||||||
ImGui.SameLine(50);
|
ImGui.SameLine(50);
|
||||||
_uiSharedService.IconText(FontAwesomeIcon.ExclamationTriangle, ImGuiColors.DalamudYellow);
|
_uiSharedService.IconText(FontAwesomeIcon.ExclamationTriangle, UiSharedService.AccentColor);
|
||||||
UiSharedService.AttachToolTip("This pose has changes that have not been saved to the server yet.");
|
UiSharedService.AttachToolTip("This pose has changes that have not been saved to the server yet.");
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui.SameLine(75);
|
ImGui.SameLine(75);
|
||||||
if (pose.Description == null && pose.WorldData == null && pose.PoseData == null)
|
if (pose.Description == null && pose.WorldData == null && pose.PoseData == null)
|
||||||
{
|
{
|
||||||
UiSharedService.ColorText("Pose scheduled for deletion", ImGuiColors.DalamudYellow);
|
UiSharedService.ColorText("Pose scheduled for deletion", UiSharedService.AccentColor);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -586,7 +586,7 @@ internal sealed partial class CharaDataHubUi
|
|||||||
var idText = entry.FullId;
|
var idText = entry.FullId;
|
||||||
if (uDto?.HasChanges ?? false)
|
if (uDto?.HasChanges ?? false)
|
||||||
{
|
{
|
||||||
UiSharedService.ColorText(idText, ImGuiColors.DalamudYellow);
|
UiSharedService.ColorText(idText, UiSharedService.AccentColor);
|
||||||
UiSharedService.AttachToolTip("This entry has unsaved changes");
|
UiSharedService.AttachToolTip("This entry has unsaved changes");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -641,7 +641,7 @@ internal sealed partial class CharaDataHubUi
|
|||||||
FontAwesomeIcon eIcon = FontAwesomeIcon.None;
|
FontAwesomeIcon eIcon = FontAwesomeIcon.None;
|
||||||
if (!Equals(DateTime.MaxValue, entry.ExpiryDate))
|
if (!Equals(DateTime.MaxValue, entry.ExpiryDate))
|
||||||
eIcon = FontAwesomeIcon.Clock;
|
eIcon = FontAwesomeIcon.Clock;
|
||||||
_uiSharedService.IconText(eIcon, ImGuiColors.DalamudYellow);
|
_uiSharedService.IconText(eIcon, UiSharedService.AccentColor);
|
||||||
if (ImGui.IsItemClicked()) SelectedDtoId = entry.Id;
|
if (ImGui.IsItemClicked()) SelectedDtoId = entry.Id;
|
||||||
if (eIcon != FontAwesomeIcon.None)
|
if (eIcon != FontAwesomeIcon.None)
|
||||||
{
|
{
|
||||||
@@ -677,13 +677,13 @@ internal sealed partial class CharaDataHubUi
|
|||||||
if (_charaDataManager.OwnCharaData.Count == _charaDataManager.MaxCreatableCharaData)
|
if (_charaDataManager.OwnCharaData.Count == _charaDataManager.MaxCreatableCharaData)
|
||||||
{
|
{
|
||||||
ImGui.AlignTextToFramePadding();
|
ImGui.AlignTextToFramePadding();
|
||||||
UiSharedService.ColorTextWrapped("You have reached the maximum Character Data entries and cannot create more.", ImGuiColors.DalamudYellow);
|
UiSharedService.ColorTextWrapped("You have reached the maximum Character Data entries and cannot create more.", UiSharedService.AccentColor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_charaDataManager.DataCreationTask != null && !_charaDataManager.DataCreationTask.IsCompleted)
|
if (_charaDataManager.DataCreationTask != null && !_charaDataManager.DataCreationTask.IsCompleted)
|
||||||
{
|
{
|
||||||
UiSharedService.ColorTextWrapped("Creating new character data entry on server...", ImGuiColors.DalamudYellow);
|
UiSharedService.ColorTextWrapped("Creating new character data entry on server...", UiSharedService.AccentColor);
|
||||||
}
|
}
|
||||||
else if (_charaDataManager.DataCreationTask != null && _charaDataManager.DataCreationTask.IsCompleted)
|
else if (_charaDataManager.DataCreationTask != null && _charaDataManager.DataCreationTask.IsCompleted)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ using System.Numerics;
|
|||||||
|
|
||||||
namespace MareSynchronos.UI;
|
namespace MareSynchronos.UI;
|
||||||
|
|
||||||
internal partial class CharaDataHubUi
|
public sealed partial class CharaDataHubUi
|
||||||
{
|
{
|
||||||
private void DrawNearbyPoses()
|
private void DrawNearbyPoses()
|
||||||
{
|
{
|
||||||
@@ -86,7 +86,7 @@ internal partial class CharaDataHubUi
|
|||||||
if (!_uiSharedService.IsInGpose)
|
if (!_uiSharedService.IsInGpose)
|
||||||
{
|
{
|
||||||
ImGuiHelpers.ScaledDummy(5);
|
ImGuiHelpers.ScaledDummy(5);
|
||||||
UiSharedService.DrawGroupedCenteredColorText("Spawning and applying pose data is only available in GPose.", ImGuiColors.DalamudYellow);
|
UiSharedService.DrawGroupedCenteredColorText("Spawning and applying pose data is only available in GPose.", UiSharedService.AccentColor);
|
||||||
ImGuiHelpers.ScaledDummy(5);
|
ImGuiHelpers.ScaledDummy(5);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,7 +101,7 @@ internal partial class CharaDataHubUi
|
|||||||
using var indent = ImRaii.PushIndent(5f);
|
using var indent = ImRaii.PushIndent(5f);
|
||||||
if (_charaDataNearbyManager.NearbyData.Count == 0)
|
if (_charaDataNearbyManager.NearbyData.Count == 0)
|
||||||
{
|
{
|
||||||
UiSharedService.DrawGroupedCenteredColorText("No Shared World Poses found nearby.", ImGuiColors.DalamudYellow);
|
UiSharedService.DrawGroupedCenteredColorText("No Shared World Poses found nearby.", UiSharedService.AccentColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool wasAnythingHovered = false;
|
bool wasAnythingHovered = false;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using Dalamud.Bindings.ImGui;
|
using Dalamud.Bindings.ImGui;
|
||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
using Dalamud.Interface.Colors;
|
using Dalamud.Interface.Colors;
|
||||||
@@ -16,10 +17,13 @@ using MareSynchronos.Services.Mediator;
|
|||||||
using MareSynchronos.Services.ServerConfiguration;
|
using MareSynchronos.Services.ServerConfiguration;
|
||||||
using MareSynchronos.Utils;
|
using MareSynchronos.Utils;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
namespace MareSynchronos.UI;
|
namespace MareSynchronos.UI;
|
||||||
|
|
||||||
internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
|
public sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
|
||||||
{
|
{
|
||||||
private const int maxPoses = 10;
|
private const int maxPoses = 10;
|
||||||
private readonly CharaDataManager _charaDataManager;
|
private readonly CharaDataManager _charaDataManager;
|
||||||
@@ -31,6 +35,7 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
|
|||||||
private readonly CharaDataGposeTogetherManager _charaDataGposeTogetherManager;
|
private readonly CharaDataGposeTogetherManager _charaDataGposeTogetherManager;
|
||||||
private readonly ServerConfigurationManager _serverConfigurationManager;
|
private readonly ServerConfigurationManager _serverConfigurationManager;
|
||||||
private readonly UiSharedService _uiSharedService;
|
private readonly UiSharedService _uiSharedService;
|
||||||
|
private readonly McdfShareManager _mcdfShareManager;
|
||||||
private CancellationTokenSource? _closalCts = new();
|
private CancellationTokenSource? _closalCts = new();
|
||||||
private bool _disableUI = false;
|
private bool _disableUI = false;
|
||||||
private CancellationTokenSource? _disposalCts = new();
|
private CancellationTokenSource? _disposalCts = new();
|
||||||
@@ -63,6 +68,15 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string SanitizeFileName(string? candidate, string fallback)
|
||||||
|
{
|
||||||
|
var invalidChars = Path.GetInvalidFileNameChars();
|
||||||
|
if (string.IsNullOrWhiteSpace(candidate)) return fallback;
|
||||||
|
|
||||||
|
var sanitized = new string(candidate.Select(ch => invalidChars.Contains(ch) ? '_' : ch).ToArray()).Trim('_');
|
||||||
|
return string.IsNullOrWhiteSpace(sanitized) ? fallback : sanitized;
|
||||||
|
}
|
||||||
private string _selectedSpecificUserIndividual = string.Empty;
|
private string _selectedSpecificUserIndividual = string.Empty;
|
||||||
private string _selectedSpecificGroupIndividual = string.Empty;
|
private string _selectedSpecificGroupIndividual = string.Empty;
|
||||||
private string _sharedWithYouDescriptionFilter = string.Empty;
|
private string _sharedWithYouDescriptionFilter = string.Empty;
|
||||||
@@ -74,12 +88,21 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
|
|||||||
private string? _openComboHybridId = null;
|
private string? _openComboHybridId = null;
|
||||||
private (string Id, string? Alias, string AliasOrId, string? Note)[]? _openComboHybridEntries = null;
|
private (string Id, string? Alias, string AliasOrId, string? Note)[]? _openComboHybridEntries = null;
|
||||||
private bool _comboHybridUsedLastFrame = false;
|
private bool _comboHybridUsedLastFrame = false;
|
||||||
|
private bool _mcdfShareInitialized;
|
||||||
|
private string _mcdfShareDescription = string.Empty;
|
||||||
|
private readonly List<string> _mcdfShareAllowedIndividuals = new();
|
||||||
|
private readonly List<string> _mcdfShareAllowedSyncshells = new();
|
||||||
|
private string _mcdfShareIndividualDropdownSelection = string.Empty;
|
||||||
|
private string _mcdfShareIndividualInput = string.Empty;
|
||||||
|
private string _mcdfShareSyncshellDropdownSelection = string.Empty;
|
||||||
|
private string _mcdfShareSyncshellInput = string.Empty;
|
||||||
|
private int _mcdfShareExpireDays;
|
||||||
|
|
||||||
public CharaDataHubUi(ILogger<CharaDataHubUi> logger, MareMediator mediator, PerformanceCollectorService performanceCollectorService,
|
public CharaDataHubUi(ILogger<CharaDataHubUi> logger, MareMediator mediator, PerformanceCollectorService performanceCollectorService,
|
||||||
CharaDataManager charaDataManager, CharaDataNearbyManager charaDataNearbyManager, CharaDataConfigService configService,
|
CharaDataManager charaDataManager, CharaDataNearbyManager charaDataNearbyManager, CharaDataConfigService configService,
|
||||||
UiSharedService uiSharedService, ServerConfigurationManager serverConfigurationManager,
|
UiSharedService uiSharedService, ServerConfigurationManager serverConfigurationManager,
|
||||||
DalamudUtilService dalamudUtilService, FileDialogManager fileDialogManager, PairManager pairManager,
|
DalamudUtilService dalamudUtilService, FileDialogManager fileDialogManager, PairManager pairManager,
|
||||||
CharaDataGposeTogetherManager charaDataGposeTogetherManager)
|
CharaDataGposeTogetherManager charaDataGposeTogetherManager, McdfShareManager mcdfShareManager)
|
||||||
: base(logger, mediator, "Umbra Character Data Hub###UmbraCharaDataUI", performanceCollectorService)
|
: base(logger, mediator, "Umbra Character Data Hub###UmbraCharaDataUI", performanceCollectorService)
|
||||||
{
|
{
|
||||||
SetWindowSizeConstraints();
|
SetWindowSizeConstraints();
|
||||||
@@ -93,6 +116,7 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
|
|||||||
_fileDialogManager = fileDialogManager;
|
_fileDialogManager = fileDialogManager;
|
||||||
_pairManager = pairManager;
|
_pairManager = pairManager;
|
||||||
_charaDataGposeTogetherManager = charaDataGposeTogetherManager;
|
_charaDataGposeTogetherManager = charaDataGposeTogetherManager;
|
||||||
|
_mcdfShareManager = mcdfShareManager;
|
||||||
Mediator.Subscribe<GposeStartMessage>(this, (_) => IsOpen |= _configService.Current.OpenMareHubOnGposeStart);
|
Mediator.Subscribe<GposeStartMessage>(this, (_) => IsOpen |= _configService.Current.OpenMareHubOnGposeStart);
|
||||||
Mediator.Subscribe<OpenCharaDataHubWithFilterMessage>(this, (msg) =>
|
Mediator.Subscribe<OpenCharaDataHubWithFilterMessage>(this, (msg) =>
|
||||||
{
|
{
|
||||||
@@ -158,6 +182,19 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected override void DrawInternal()
|
protected override void DrawInternal()
|
||||||
|
{
|
||||||
|
DrawHubContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DrawInline()
|
||||||
|
{
|
||||||
|
using (ImRaii.PushId("CharaDataHubInline"))
|
||||||
|
{
|
||||||
|
DrawHubContent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawHubContent()
|
||||||
{
|
{
|
||||||
if (!_comboHybridUsedLastFrame)
|
if (!_comboHybridUsedLastFrame)
|
||||||
{
|
{
|
||||||
@@ -198,7 +235,7 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
if (!string.IsNullOrEmpty(_charaDataManager.DataApplicationProgress))
|
if (!string.IsNullOrEmpty(_charaDataManager.DataApplicationProgress))
|
||||||
{
|
{
|
||||||
UiSharedService.ColorTextWrapped(_charaDataManager.DataApplicationProgress, ImGuiColors.DalamudYellow);
|
UiSharedService.ColorTextWrapped(_charaDataManager.DataApplicationProgress, UiSharedService.AccentColor);
|
||||||
}
|
}
|
||||||
if (_charaDataManager.DataApplicationTask != null)
|
if (_charaDataManager.DataApplicationTask != null)
|
||||||
{
|
{
|
||||||
@@ -208,115 +245,140 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
using var tabs = ImRaii.TabBar("TabsTopLevel");
|
|
||||||
bool smallUi = false;
|
bool smallUi = false;
|
||||||
|
using (var topTabColor = ImRaii.PushColor(ImGuiCol.Tab, UiSharedService.AccentColor))
|
||||||
_isHandlingSelf = _charaDataManager.HandledCharaData.Any(c => c.Value.IsSelf);
|
using (var topTabHoverColor = ImRaii.PushColor(ImGuiCol.TabHovered, UiSharedService.AccentHoverColor))
|
||||||
if (_isHandlingSelf) _openMcdOnlineOnNextRun = false;
|
using (var topTabActiveColor = ImRaii.PushColor(ImGuiCol.TabActive, UiSharedService.AccentActiveColor))
|
||||||
|
|
||||||
using (var gposeTogetherTabItem = ImRaii.TabItem("GPose Together"))
|
|
||||||
{
|
{
|
||||||
if (gposeTogetherTabItem)
|
using var tabs = ImRaii.TabBar("TabsTopLevel");
|
||||||
|
|
||||||
|
_isHandlingSelf = _charaDataManager.HandledCharaData.Any(c => c.Value.IsSelf);
|
||||||
|
if (_isHandlingSelf) _openMcdOnlineOnNextRun = false;
|
||||||
|
|
||||||
|
using (var gposeTogetherTabItem = ImRaii.TabItem("GPose Together"))
|
||||||
{
|
{
|
||||||
smallUi = true;
|
if (gposeTogetherTabItem)
|
||||||
|
|
||||||
DrawGposeTogether();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
using (var applicationTabItem = ImRaii.TabItem("Data Application", _openDataApplicationShared ? ImGuiTabItemFlags.SetSelected : ImGuiTabItemFlags.None))
|
|
||||||
{
|
|
||||||
if (applicationTabItem)
|
|
||||||
{
|
|
||||||
smallUi = true;
|
|
||||||
using var appTabs = ImRaii.TabBar("TabsApplicationLevel");
|
|
||||||
|
|
||||||
using (ImRaii.Disabled(!_uiSharedService.IsInGpose))
|
|
||||||
{
|
{
|
||||||
using (var gposeTabItem = ImRaii.TabItem("GPose Actors"))
|
smallUi = true;
|
||||||
|
|
||||||
|
DrawGposeTogether();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var applicationTabItem = ImRaii.TabItem("Data Application", _openDataApplicationShared ? ImGuiTabItemFlags.SetSelected : ImGuiTabItemFlags.None))
|
||||||
|
{
|
||||||
|
if (applicationTabItem)
|
||||||
|
{
|
||||||
|
smallUi = true;
|
||||||
|
using (var appTabColor = ImRaii.PushColor(ImGuiCol.Tab, UiSharedService.AccentColor))
|
||||||
|
using (var appTabHoverColor = ImRaii.PushColor(ImGuiCol.TabHovered, UiSharedService.AccentHoverColor))
|
||||||
|
using (var appTabActiveColor = ImRaii.PushColor(ImGuiCol.TabActive, UiSharedService.AccentActiveColor))
|
||||||
{
|
{
|
||||||
if (gposeTabItem)
|
using var appTabs = ImRaii.TabBar("TabsApplicationLevel");
|
||||||
|
|
||||||
|
using (ImRaii.Disabled(!_uiSharedService.IsInGpose))
|
||||||
{
|
{
|
||||||
using var id = ImRaii.PushId("gposeControls");
|
using (var gposeTabItem = ImRaii.TabItem("GPose Actors"))
|
||||||
DrawGposeControls();
|
{
|
||||||
|
if (gposeTabItem)
|
||||||
|
{
|
||||||
|
using var id = ImRaii.PushId("gposeControls");
|
||||||
|
DrawGposeControls();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!_uiSharedService.IsInGpose)
|
||||||
|
UiSharedService.AttachToolTip("Only available in GPose");
|
||||||
|
|
||||||
|
using (var nearbyPosesTabItem = ImRaii.TabItem("Poses Nearby"))
|
||||||
|
{
|
||||||
|
if (nearbyPosesTabItem)
|
||||||
|
{
|
||||||
|
using var id = ImRaii.PushId("nearbyPoseControls");
|
||||||
|
_charaDataNearbyManager.ComputeNearbyData = true;
|
||||||
|
|
||||||
|
DrawNearbyPoses();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_charaDataNearbyManager.ComputeNearbyData = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var gposeTabItem = ImRaii.TabItem("Apply Data", _openDataApplicationShared ? ImGuiTabItemFlags.SetSelected : ImGuiTabItemFlags.None))
|
||||||
|
{
|
||||||
|
if (gposeTabItem)
|
||||||
|
{
|
||||||
|
smallUi |= true;
|
||||||
|
using var id = ImRaii.PushId("applyData");
|
||||||
|
DrawDataApplication();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!_uiSharedService.IsInGpose)
|
else
|
||||||
UiSharedService.AttachToolTip("Only available in GPose");
|
|
||||||
|
|
||||||
using (var nearbyPosesTabItem = ImRaii.TabItem("Poses Nearby"))
|
|
||||||
{
|
{
|
||||||
if (nearbyPosesTabItem)
|
_charaDataNearbyManager.ComputeNearbyData = false;
|
||||||
{
|
|
||||||
using var id = ImRaii.PushId("nearbyPoseControls");
|
|
||||||
_charaDataNearbyManager.ComputeNearbyData = true;
|
|
||||||
|
|
||||||
DrawNearbyPoses();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_charaDataNearbyManager.ComputeNearbyData = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
using (var gposeTabItem = ImRaii.TabItem("Apply Data", _openDataApplicationShared ? ImGuiTabItemFlags.SetSelected : ImGuiTabItemFlags.None))
|
|
||||||
{
|
|
||||||
if (gposeTabItem)
|
|
||||||
{
|
|
||||||
smallUi |= true;
|
|
||||||
using var id = ImRaii.PushId("applyData");
|
|
||||||
DrawDataApplication();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
_charaDataNearbyManager.ComputeNearbyData = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
using (ImRaii.Disabled(_isHandlingSelf))
|
using (ImRaii.Disabled(_isHandlingSelf))
|
||||||
{
|
|
||||||
ImGuiTabItemFlags flagsTopLevel = ImGuiTabItemFlags.None;
|
|
||||||
if (_openMcdOnlineOnNextRun)
|
|
||||||
{
|
{
|
||||||
flagsTopLevel = ImGuiTabItemFlags.SetSelected;
|
ImGuiTabItemFlags flagsTopLevel = ImGuiTabItemFlags.None;
|
||||||
_openMcdOnlineOnNextRun = false;
|
if (_openMcdOnlineOnNextRun)
|
||||||
}
|
|
||||||
|
|
||||||
using (var creationTabItem = ImRaii.TabItem("Data Creation", flagsTopLevel))
|
|
||||||
{
|
|
||||||
if (creationTabItem)
|
|
||||||
{
|
{
|
||||||
using var creationTabs = ImRaii.TabBar("TabsCreationLevel");
|
flagsTopLevel = ImGuiTabItemFlags.SetSelected;
|
||||||
|
_openMcdOnlineOnNextRun = false;
|
||||||
|
}
|
||||||
|
|
||||||
ImGuiTabItemFlags flags = ImGuiTabItemFlags.None;
|
using (var creationTabItem = ImRaii.TabItem("Data Creation", flagsTopLevel))
|
||||||
if (_openMcdOnlineOnNextRun)
|
{
|
||||||
|
if (creationTabItem)
|
||||||
{
|
{
|
||||||
flags = ImGuiTabItemFlags.SetSelected;
|
using (var creationTabColor = ImRaii.PushColor(ImGuiCol.Tab, UiSharedService.AccentColor))
|
||||||
_openMcdOnlineOnNextRun = false;
|
using (var creationTabHoverColor = ImRaii.PushColor(ImGuiCol.TabHovered, UiSharedService.AccentHoverColor))
|
||||||
}
|
using (var creationTabActiveColor = ImRaii.PushColor(ImGuiCol.TabActive, UiSharedService.AccentActiveColor))
|
||||||
using (var mcdOnlineTabItem = ImRaii.TabItem("Online Data", flags))
|
|
||||||
{
|
|
||||||
if (mcdOnlineTabItem)
|
|
||||||
{
|
{
|
||||||
using var id = ImRaii.PushId("mcdOnline");
|
using var creationTabs = ImRaii.TabBar("TabsCreationLevel");
|
||||||
DrawMcdOnline();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
using (var mcdfTabItem = ImRaii.TabItem("MCDF Export"))
|
ImGuiTabItemFlags flags = ImGuiTabItemFlags.None;
|
||||||
{
|
if (_openMcdOnlineOnNextRun)
|
||||||
if (mcdfTabItem)
|
{
|
||||||
{
|
flags = ImGuiTabItemFlags.SetSelected;
|
||||||
using var id = ImRaii.PushId("mcdfExport");
|
_openMcdOnlineOnNextRun = false;
|
||||||
DrawMcdfExport();
|
}
|
||||||
|
using (var mcdOnlineTabItem = ImRaii.TabItem("Online Data", flags))
|
||||||
|
{
|
||||||
|
if (mcdOnlineTabItem)
|
||||||
|
{
|
||||||
|
using var id = ImRaii.PushId("mcdOnline");
|
||||||
|
DrawMcdOnline();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var mcdfTabItem = ImRaii.TabItem("MCDF Export"))
|
||||||
|
{
|
||||||
|
if (mcdfTabItem)
|
||||||
|
{
|
||||||
|
using var id = ImRaii.PushId("mcdfExport");
|
||||||
|
DrawMcdfExport();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var mcdfShareTabItem = ImRaii.TabItem("Partage MCDF"))
|
||||||
|
{
|
||||||
|
if (mcdfShareTabItem)
|
||||||
|
{
|
||||||
|
using var id = ImRaii.PushId("mcdfShare");
|
||||||
|
DrawMcdfShare();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_isHandlingSelf)
|
if (_isHandlingSelf)
|
||||||
{
|
{
|
||||||
UiSharedService.AttachToolTip("Cannot use creation tools while having Character Data applied to self.");
|
UiSharedService.AttachToolTip("Cannot use creation tools while having Character Data applied to self.");
|
||||||
@@ -444,14 +506,18 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
|
|||||||
if (!_hasValidGposeTarget)
|
if (!_hasValidGposeTarget)
|
||||||
{
|
{
|
||||||
ImGuiHelpers.ScaledDummy(3);
|
ImGuiHelpers.ScaledDummy(3);
|
||||||
UiSharedService.DrawGroupedCenteredColorText("Applying data is only available in GPose with a valid selected GPose target.", ImGuiColors.DalamudYellow, 350);
|
UiSharedService.DrawGroupedCenteredColorText("Applying data is only available in GPose with a valid selected GPose target.", UiSharedService.AccentColor, 350);
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGuiHelpers.ScaledDummy(10);
|
ImGuiHelpers.ScaledDummy(10);
|
||||||
|
|
||||||
using var tabs = ImRaii.TabBar("Tabs");
|
using (var applyTabColor = ImRaii.PushColor(ImGuiCol.Tab, UiSharedService.AccentColor))
|
||||||
|
using (var applyTabHoverColor = ImRaii.PushColor(ImGuiCol.TabHovered, UiSharedService.AccentHoverColor))
|
||||||
|
using (var applyTabActiveColor = ImRaii.PushColor(ImGuiCol.TabActive, UiSharedService.AccentActiveColor))
|
||||||
|
{
|
||||||
|
using var tabs = ImRaii.TabBar("Tabs");
|
||||||
|
|
||||||
using (var byFavoriteTabItem = ImRaii.TabItem("Favorites"))
|
using (var byFavoriteTabItem = ImRaii.TabItem("Favorites"))
|
||||||
{
|
{
|
||||||
if (byFavoriteTabItem)
|
if (byFavoriteTabItem)
|
||||||
{
|
{
|
||||||
@@ -603,7 +669,7 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
if (_configService.Current.FavoriteCodes.Count == 0)
|
if (_configService.Current.FavoriteCodes.Count == 0)
|
||||||
{
|
{
|
||||||
UiSharedService.ColorTextWrapped("You have no favorites added. Add Favorites through the other tabs before you can use this tab.", ImGuiColors.DalamudYellow);
|
UiSharedService.ColorTextWrapped("You have no favorites added. Add Favorites through the other tabs before you can use this tab.", UiSharedService.AccentColor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -652,7 +718,7 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
|
|||||||
ImGui.NewLine();
|
ImGui.NewLine();
|
||||||
if (!_charaDataManager.DownloadMetaInfoTask?.IsCompleted ?? false)
|
if (!_charaDataManager.DownloadMetaInfoTask?.IsCompleted ?? false)
|
||||||
{
|
{
|
||||||
UiSharedService.ColorTextWrapped("Downloading meta info. Please wait.", ImGuiColors.DalamudYellow);
|
UiSharedService.ColorTextWrapped("Downloading meta info. Please wait.", UiSharedService.AccentColor);
|
||||||
}
|
}
|
||||||
if ((_charaDataManager.DownloadMetaInfoTask?.IsCompleted ?? false) && !_charaDataManager.DownloadMetaInfoTask.Result.Success)
|
if ((_charaDataManager.DownloadMetaInfoTask?.IsCompleted ?? false) && !_charaDataManager.DownloadMetaInfoTask.Result.Success)
|
||||||
{
|
{
|
||||||
@@ -859,15 +925,16 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
|
|||||||
UiSharedService.ColorTextWrapped("Failure to read MCDF file. MCDF file is possibly corrupt. Re-export the MCDF file and try again.",
|
UiSharedService.ColorTextWrapped("Failure to read MCDF file. MCDF file is possibly corrupt. Re-export the MCDF file and try again.",
|
||||||
UiSharedService.AccentColor);
|
UiSharedService.AccentColor);
|
||||||
UiSharedService.ColorTextWrapped("Note: if this is your MCDF, try redrawing yourself, wait and re-export the file. " +
|
UiSharedService.ColorTextWrapped("Note: if this is your MCDF, try redrawing yourself, wait and re-export the file. " +
|
||||||
"If you received it from someone else have them do the same.", ImGuiColors.DalamudYellow);
|
"If you received it from someone else have them do the same.", UiSharedService.AccentColor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
UiSharedService.ColorTextWrapped("Loading Character...", ImGuiColors.DalamudYellow);
|
UiSharedService.ColorTextWrapped("Loading Character...", UiSharedService.AccentColor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawMcdfExport()
|
private void DrawMcdfExport()
|
||||||
@@ -892,7 +959,7 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
string defaultFileName = string.IsNullOrEmpty(_exportDescription)
|
string defaultFileName = string.IsNullOrEmpty(_exportDescription)
|
||||||
? "export.mcdf"
|
? "export.mcdf"
|
||||||
: string.Join('_', $"{_exportDescription}.mcdf".Split(Path.GetInvalidFileNameChars()));
|
: SanitizeFileName(_exportDescription, "export") + ".mcdf";
|
||||||
_uiSharedService.FileDialogManager.SaveFileDialog("Export Character to file", ".mcdf", defaultFileName, ".mcdf", (success, path) =>
|
_uiSharedService.FileDialogManager.SaveFileDialog("Export Character to file", ".mcdf", defaultFileName, ".mcdf", (success, path) =>
|
||||||
{
|
{
|
||||||
if (!success) return;
|
if (!success) return;
|
||||||
@@ -905,12 +972,469 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
|
|||||||
}, Directory.Exists(_configService.Current.LastSavedCharaDataLocation) ? _configService.Current.LastSavedCharaDataLocation : null);
|
}, Directory.Exists(_configService.Current.LastSavedCharaDataLocation) ? _configService.Current.LastSavedCharaDataLocation : null);
|
||||||
}
|
}
|
||||||
UiSharedService.ColorTextWrapped("Note: For best results make sure you have everything you want to be shared as well as the correct character appearance" +
|
UiSharedService.ColorTextWrapped("Note: For best results make sure you have everything you want to be shared as well as the correct character appearance" +
|
||||||
" equipped and redraw your character before exporting.", ImGuiColors.DalamudYellow);
|
" equipped and redraw your character before exporting.", UiSharedService.AccentColor);
|
||||||
|
|
||||||
ImGui.Unindent();
|
ImGui.Unindent();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void DrawMcdfShare()
|
||||||
|
{
|
||||||
|
if (!_mcdfShareInitialized && !_mcdfShareManager.IsBusy)
|
||||||
|
{
|
||||||
|
_mcdfShareInitialized = true;
|
||||||
|
_ = _mcdfShareManager.RefreshAsync(CancellationToken.None);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_mcdfShareManager.IsBusy)
|
||||||
|
{
|
||||||
|
UiSharedService.ColorTextWrapped("Traitement en cours...", ImGuiColors.DalamudYellow);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(_mcdfShareManager.LastError))
|
||||||
|
{
|
||||||
|
UiSharedService.ColorTextWrapped(_mcdfShareManager.LastError!, ImGuiColors.DalamudRed);
|
||||||
|
}
|
||||||
|
else if (!string.IsNullOrEmpty(_mcdfShareManager.LastSuccess))
|
||||||
|
{
|
||||||
|
UiSharedService.ColorTextWrapped(_mcdfShareManager.LastSuccess!, ImGuiColors.HealerGreen);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui.Button("Actualiser les partages"))
|
||||||
|
{
|
||||||
|
_ = _mcdfShareManager.RefreshAsync(CancellationToken.None);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.Separator();
|
||||||
|
_uiSharedService.BigText("Créer un partage MCDF");
|
||||||
|
|
||||||
|
ImGui.InputTextWithHint("##mcdfShareDescription", "Description", ref _mcdfShareDescription, 128);
|
||||||
|
ImGui.InputInt("Expiration (jours, 0 = jamais)", ref _mcdfShareExpireDays);
|
||||||
|
|
||||||
|
DrawMcdfShareIndividualDropdown();
|
||||||
|
ImGui.SameLine();
|
||||||
|
ImGui.SetNextItemWidth(220f);
|
||||||
|
if (ImGui.InputTextWithHint("##mcdfShareUidInput", "UID ou vanity", ref _mcdfShareIndividualInput, 32))
|
||||||
|
{
|
||||||
|
_mcdfShareIndividualDropdownSelection = string.Empty;
|
||||||
|
}
|
||||||
|
ImGui.SameLine();
|
||||||
|
var normalizedUid = NormalizeUidCandidate(_mcdfShareIndividualInput);
|
||||||
|
using (ImRaii.Disabled(string.IsNullOrEmpty(normalizedUid)
|
||||||
|
|| _mcdfShareAllowedIndividuals.Any(p => string.Equals(p, normalizedUid, StringComparison.OrdinalIgnoreCase))))
|
||||||
|
{
|
||||||
|
if (ImGui.SmallButton("Ajouter"))
|
||||||
|
{
|
||||||
|
_mcdfShareAllowedIndividuals.Add(normalizedUid);
|
||||||
|
_mcdfShareIndividualInput = string.Empty;
|
||||||
|
_mcdfShareIndividualDropdownSelection = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui.SameLine();
|
||||||
|
ImGui.TextUnformatted("UID synchronisé à ajouter");
|
||||||
|
_uiSharedService.DrawHelpText("Choisissez un pair synchronisé dans la liste ou saisissez un UID. Les utilisateurs listés pourront récupérer ce partage MCDF.");
|
||||||
|
|
||||||
|
foreach (var uid in _mcdfShareAllowedIndividuals.ToArray())
|
||||||
|
{
|
||||||
|
using (ImRaii.PushId("mcdfShareUid" + uid))
|
||||||
|
{
|
||||||
|
ImGui.BulletText(FormatPairLabel(uid));
|
||||||
|
ImGui.SameLine();
|
||||||
|
if (ImGui.SmallButton("Retirer"))
|
||||||
|
{
|
||||||
|
_mcdfShareAllowedIndividuals.Remove(uid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DrawMcdfShareSyncshellDropdown();
|
||||||
|
ImGui.SameLine();
|
||||||
|
ImGui.SetNextItemWidth(220f);
|
||||||
|
if (ImGui.InputTextWithHint("##mcdfShareSyncshellInput", "GID ou alias", ref _mcdfShareSyncshellInput, 32))
|
||||||
|
{
|
||||||
|
_mcdfShareSyncshellDropdownSelection = string.Empty;
|
||||||
|
}
|
||||||
|
ImGui.SameLine();
|
||||||
|
var normalizedSyncshell = NormalizeSyncshellCandidate(_mcdfShareSyncshellInput);
|
||||||
|
using (ImRaii.Disabled(string.IsNullOrEmpty(normalizedSyncshell)
|
||||||
|
|| _mcdfShareAllowedSyncshells.Any(p => string.Equals(p, normalizedSyncshell, StringComparison.OrdinalIgnoreCase))))
|
||||||
|
{
|
||||||
|
if (ImGui.SmallButton("Ajouter"))
|
||||||
|
{
|
||||||
|
_mcdfShareAllowedSyncshells.Add(normalizedSyncshell);
|
||||||
|
_mcdfShareSyncshellInput = string.Empty;
|
||||||
|
_mcdfShareSyncshellDropdownSelection = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui.SameLine();
|
||||||
|
ImGui.TextUnformatted("Syncshell à ajouter");
|
||||||
|
_uiSharedService.DrawHelpText("Sélectionnez une syncshell synchronisée ou saisissez un identifiant. Les syncshells listées auront accès au partage.");
|
||||||
|
|
||||||
|
foreach (var shell in _mcdfShareAllowedSyncshells.ToArray())
|
||||||
|
{
|
||||||
|
using (ImRaii.PushId("mcdfShareShell" + shell))
|
||||||
|
{
|
||||||
|
ImGui.BulletText(FormatSyncshellLabel(shell));
|
||||||
|
ImGui.SameLine();
|
||||||
|
if (ImGui.SmallButton("Retirer"))
|
||||||
|
{
|
||||||
|
_mcdfShareAllowedSyncshells.Remove(shell);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
using (ImRaii.Disabled(_mcdfShareManager.IsBusy))
|
||||||
|
{
|
||||||
|
if (ImGui.Button("Créer"))
|
||||||
|
{
|
||||||
|
DateTime? expiresAt = _mcdfShareExpireDays <= 0 ? null : DateTime.UtcNow.AddDays(_mcdfShareExpireDays);
|
||||||
|
_ = _mcdfShareManager.CreateShareAsync(_mcdfShareDescription, _mcdfShareAllowedIndividuals.ToList(), _mcdfShareAllowedSyncshells.ToList(), expiresAt, CancellationToken.None);
|
||||||
|
_mcdfShareDescription = string.Empty;
|
||||||
|
_mcdfShareAllowedIndividuals.Clear();
|
||||||
|
_mcdfShareAllowedSyncshells.Clear();
|
||||||
|
_mcdfShareIndividualInput = string.Empty;
|
||||||
|
_mcdfShareIndividualDropdownSelection = string.Empty;
|
||||||
|
_mcdfShareSyncshellInput = string.Empty;
|
||||||
|
_mcdfShareSyncshellDropdownSelection = string.Empty;
|
||||||
|
_mcdfShareExpireDays = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.Separator();
|
||||||
|
_uiSharedService.BigText("Mes partages : ");
|
||||||
|
|
||||||
|
if (_mcdfShareManager.OwnShares.Count == 0)
|
||||||
|
{
|
||||||
|
ImGui.TextDisabled("Aucun partage MCDF créé.");
|
||||||
|
}
|
||||||
|
else if (ImGui.BeginTable("mcdf-own-shares", 6, ImGuiTableFlags.SizingStretchProp | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersOuter))
|
||||||
|
{
|
||||||
|
ImGui.TableSetupColumn("Description");
|
||||||
|
ImGui.TableSetupColumn("Créé le");
|
||||||
|
ImGui.TableSetupColumn("Expire");
|
||||||
|
ImGui.TableSetupColumn("Téléchargements");
|
||||||
|
ImGui.TableSetupColumn("Accès");
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
ImGui.TableNextRow();
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.TextUnformatted(string.IsNullOrEmpty(entry.Description) ? entry.Id.ToString() : entry.Description);
|
||||||
|
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.TextUnformatted(entry.CreatedUtc.ToLocalTime().ToString("g"));
|
||||||
|
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.TextUnformatted(entry.ExpiresAtUtc.HasValue ? entry.ExpiresAtUtc.Value.ToLocalTime().ToString("g") : "Jamais");
|
||||||
|
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.TextUnformatted(entry.DownloadCount.ToString());
|
||||||
|
|
||||||
|
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))
|
||||||
|
{
|
||||||
|
if (ImGui.SmallButton("Appliquer en GPose"))
|
||||||
|
{
|
||||||
|
_ = _mcdfShareManager.ApplyShareAsync(entry.Id, CancellationToken.None);
|
||||||
|
}
|
||||||
|
ImGui.SameLine();
|
||||||
|
if (ImGui.SmallButton("Enregistrer"))
|
||||||
|
{
|
||||||
|
var baseName = SanitizeFileName(entry.Description, entry.Id.ToString());
|
||||||
|
var defaultName = baseName + ".mcdf";
|
||||||
|
_fileDialogManager.SaveFileDialog("Enregistrer le partage MCDF", ".mcdf", defaultName, ".mcdf", async (success, path) =>
|
||||||
|
{
|
||||||
|
if (!success || string.IsNullOrEmpty(path)) return;
|
||||||
|
await _mcdfShareManager.ExportShareAsync(entry.Id, path, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
ImGui.SameLine();
|
||||||
|
if (ImGui.SmallButton("Supprimer"))
|
||||||
|
{
|
||||||
|
_ = _mcdfShareManager.DeleteShareAsync(entry.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.EndTable();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.Separator();
|
||||||
|
_uiSharedService.BigText("Partagés avec moi : ");
|
||||||
|
|
||||||
|
if (_mcdfShareManager.SharedShares.Count == 0)
|
||||||
|
{
|
||||||
|
ImGui.TextDisabled("Aucun partage MCDF reçu.");
|
||||||
|
}
|
||||||
|
else if (ImGui.BeginTable("mcdf-shared-shares", 5, ImGuiTableFlags.SizingStretchProp | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersOuter))
|
||||||
|
{
|
||||||
|
ImGui.TableSetupColumn("Description");
|
||||||
|
ImGui.TableSetupColumn("Propriétaire");
|
||||||
|
ImGui.TableSetupColumn("Expire");
|
||||||
|
ImGui.TableSetupColumn("Téléchargements");
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
ImGui.TableNextRow();
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.TextUnformatted(string.IsNullOrEmpty(entry.Description) ? entry.Id.ToString() : entry.Description);
|
||||||
|
|
||||||
|
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");
|
||||||
|
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.TextUnformatted(entry.DownloadCount.ToString());
|
||||||
|
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
using (ImRaii.PushId("sharedShare" + entry.Id))
|
||||||
|
{
|
||||||
|
if (ImGui.SmallButton("Appliquer"))
|
||||||
|
{
|
||||||
|
_ = _mcdfShareManager.ApplyShareAsync(entry.Id, CancellationToken.None);
|
||||||
|
}
|
||||||
|
ImGui.SameLine();
|
||||||
|
if (ImGui.SmallButton("Enregistrer"))
|
||||||
|
{
|
||||||
|
var baseName = SanitizeFileName(entry.Description, entry.Id.ToString());
|
||||||
|
var defaultName = baseName + ".mcdf";
|
||||||
|
_fileDialogManager.SaveFileDialog("Enregistrer le partage MCDF", ".mcdf", defaultName, ".mcdf", async (success, path) =>
|
||||||
|
{
|
||||||
|
if (!success || string.IsNullOrEmpty(path)) return;
|
||||||
|
await _mcdfShareManager.ExportShareAsync(entry.Id, path, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.EndTable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawMcdfShareIndividualDropdown()
|
||||||
|
{
|
||||||
|
ImGui.SetNextItemWidth(220f);
|
||||||
|
var previewSource = string.IsNullOrEmpty(_mcdfShareIndividualDropdownSelection)
|
||||||
|
? _mcdfShareIndividualInput
|
||||||
|
: _mcdfShareIndividualDropdownSelection;
|
||||||
|
var previewLabel = string.IsNullOrEmpty(previewSource)
|
||||||
|
? "Sélectionner un pair synchronisé..."
|
||||||
|
: FormatPairLabel(previewSource);
|
||||||
|
|
||||||
|
using var combo = ImRaii.Combo("##mcdfShareUidDropdown", previewLabel, ImGuiComboFlags.None);
|
||||||
|
if (!combo)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var pair in _pairManager.DirectPairs
|
||||||
|
.OrderBy(p => p.GetNoteOrName() ?? p.UserData.AliasOrUID, StringComparer.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var normalized = pair.UserData.UID;
|
||||||
|
var display = FormatPairLabel(normalized);
|
||||||
|
bool selected = string.Equals(normalized, _mcdfShareIndividualDropdownSelection, StringComparison.OrdinalIgnoreCase);
|
||||||
|
if (ImGui.Selectable(display, selected))
|
||||||
|
{
|
||||||
|
_mcdfShareIndividualDropdownSelection = normalized;
|
||||||
|
_mcdfShareIndividualInput = normalized;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawMcdfShareSyncshellDropdown()
|
||||||
|
{
|
||||||
|
ImGui.SetNextItemWidth(220f);
|
||||||
|
var previewSource = string.IsNullOrEmpty(_mcdfShareSyncshellDropdownSelection)
|
||||||
|
? _mcdfShareSyncshellInput
|
||||||
|
: _mcdfShareSyncshellDropdownSelection;
|
||||||
|
var previewLabel = string.IsNullOrEmpty(previewSource)
|
||||||
|
? "Sélectionner une syncshell..."
|
||||||
|
: FormatSyncshellLabel(previewSource);
|
||||||
|
|
||||||
|
using var combo = ImRaii.Combo("##mcdfShareSyncshellDropdown", previewLabel, ImGuiComboFlags.None);
|
||||||
|
if (!combo)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var group in _pairManager.Groups.Values
|
||||||
|
.OrderBy(g => _serverConfigurationManager.GetNoteForGid(g.GID) ?? g.GroupAliasOrGID, StringComparer.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var gid = group.GID;
|
||||||
|
var display = FormatSyncshellLabel(gid);
|
||||||
|
bool selected = string.Equals(gid, _mcdfShareSyncshellDropdownSelection, StringComparison.OrdinalIgnoreCase);
|
||||||
|
if (ImGui.Selectable(display, selected))
|
||||||
|
{
|
||||||
|
_mcdfShareSyncshellDropdownSelection = gid;
|
||||||
|
_mcdfShareSyncshellInput = gid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string NormalizeUidCandidate(string candidate)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(candidate))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
var trimmed = candidate.Trim();
|
||||||
|
|
||||||
|
foreach (var pair in _pairManager.DirectPairs)
|
||||||
|
{
|
||||||
|
var alias = pair.UserData.Alias;
|
||||||
|
var aliasOrUid = pair.UserData.AliasOrUID;
|
||||||
|
var note = pair.GetNoteOrName();
|
||||||
|
|
||||||
|
if (string.Equals(pair.UserData.UID, trimmed, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| (!string.IsNullOrEmpty(alias) && string.Equals(alias, trimmed, StringComparison.OrdinalIgnoreCase))
|
||||||
|
|| string.Equals(aliasOrUid, trimmed, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| (!string.IsNullOrEmpty(note) && string.Equals(note, trimmed, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
{
|
||||||
|
return pair.UserData.UID;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return trimmed.ToUpperInvariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
private string NormalizeSyncshellCandidate(string candidate)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(candidate))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
var trimmed = candidate.Trim();
|
||||||
|
|
||||||
|
foreach (var group in _pairManager.Groups.Values)
|
||||||
|
{
|
||||||
|
var alias = group.GroupAlias;
|
||||||
|
var aliasOrGid = group.GroupAliasOrGID;
|
||||||
|
var note = _serverConfigurationManager.GetNoteForGid(group.GID);
|
||||||
|
|
||||||
|
if (string.Equals(group.GID, trimmed, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| (!string.IsNullOrEmpty(alias) && string.Equals(alias, trimmed, StringComparison.OrdinalIgnoreCase))
|
||||||
|
|| string.Equals(aliasOrGid, trimmed, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| (!string.IsNullOrEmpty(note) && string.Equals(note, trimmed, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
{
|
||||||
|
return group.GID;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var pair in _pairManager.DirectPairs)
|
||||||
|
{
|
||||||
|
var alias = pair.UserData.Alias;
|
||||||
|
var aliasOrUid = pair.UserData.AliasOrUID;
|
||||||
|
var note = pair.GetNoteOrName();
|
||||||
|
|
||||||
|
if (string.Equals(pair.UserData.UID, candidate, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| (!string.IsNullOrEmpty(alias) && string.Equals(alias, candidate, StringComparison.OrdinalIgnoreCase))
|
||||||
|
|| string.Equals(aliasOrUid, candidate, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| (!string.IsNullOrEmpty(note) && string.Equals(note, candidate, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
{
|
||||||
|
return string.IsNullOrEmpty(note) ? aliasOrUid : $"{note} ({aliasOrUid})";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string FormatSyncshellLabel(string candidate)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(candidate))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var group in _pairManager.Groups.Values)
|
||||||
|
{
|
||||||
|
var alias = group.GroupAlias;
|
||||||
|
var aliasOrGid = group.GroupAliasOrGID;
|
||||||
|
var note = _serverConfigurationManager.GetNoteForGid(group.GID);
|
||||||
|
|
||||||
|
if (string.Equals(group.GID, candidate, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| (!string.IsNullOrEmpty(alias) && string.Equals(alias, candidate, StringComparison.OrdinalIgnoreCase))
|
||||||
|
|| string.Equals(aliasOrGid, candidate, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| (!string.IsNullOrEmpty(note) && string.Equals(note, candidate, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
{
|
||||||
|
return string.IsNullOrEmpty(note) ? aliasOrGid : $"{note} ({aliasOrGid})";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
|
||||||
private void DrawMetaInfoData(string selectedGposeActor, bool hasValidGposeTarget, CharaDataMetaInfoExtendedDto data, bool canOpen = false)
|
private void DrawMetaInfoData(string selectedGposeActor, bool hasValidGposeTarget, CharaDataMetaInfoExtendedDto data, bool canOpen = false)
|
||||||
{
|
{
|
||||||
ImGuiHelpers.ScaledDummy(5);
|
ImGuiHelpers.ScaledDummy(5);
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ using MareSynchronos.Services;
|
|||||||
using MareSynchronos.Services.Mediator;
|
using MareSynchronos.Services.Mediator;
|
||||||
using MareSynchronos.Services.ServerConfiguration;
|
using MareSynchronos.Services.ServerConfiguration;
|
||||||
using MareSynchronos.Services.AutoDetect;
|
using MareSynchronos.Services.AutoDetect;
|
||||||
|
using MareSynchronos.Services.Notifications;
|
||||||
using MareSynchronos.UI.Components;
|
using MareSynchronos.UI.Components;
|
||||||
using MareSynchronos.UI.Handlers;
|
using MareSynchronos.UI.Handlers;
|
||||||
using MareSynchronos.WebAPI;
|
using MareSynchronos.WebAPI;
|
||||||
@@ -28,6 +29,7 @@ using System.Diagnostics;
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
namespace MareSynchronos.UI;
|
namespace MareSynchronos.UI;
|
||||||
@@ -57,6 +59,8 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
private readonly SettingsUi _settingsUi;
|
private readonly SettingsUi _settingsUi;
|
||||||
private readonly AutoDetectUi _autoDetectUi;
|
private readonly AutoDetectUi _autoDetectUi;
|
||||||
private readonly DataAnalysisUi _dataAnalysisUi;
|
private readonly DataAnalysisUi _dataAnalysisUi;
|
||||||
|
private readonly CharaDataHubUi _charaDataHubUi;
|
||||||
|
private readonly NotificationTracker _notificationTracker;
|
||||||
private bool _buttonState;
|
private bool _buttonState;
|
||||||
private string _characterOrCommentFilter = string.Empty;
|
private string _characterOrCommentFilter = string.Empty;
|
||||||
private Pair? _lastAddedUser;
|
private Pair? _lastAddedUser;
|
||||||
@@ -71,6 +75,7 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
private bool _visibleOpen = true;
|
private bool _visibleOpen = true;
|
||||||
private bool _selfAnalysisOpen = false;
|
private bool _selfAnalysisOpen = false;
|
||||||
private List<Services.Mediator.NearbyEntry> _nearbyEntries = new();
|
private List<Services.Mediator.NearbyEntry> _nearbyEntries = new();
|
||||||
|
private int _notificationCount;
|
||||||
private const long SelfAnalysisSizeWarningThreshold = 300L * 1024 * 1024;
|
private const long SelfAnalysisSizeWarningThreshold = 300L * 1024 * 1024;
|
||||||
private const long SelfAnalysisTriangleWarningThreshold = 150_000;
|
private const long SelfAnalysisTriangleWarningThreshold = 150_000;
|
||||||
private CompactUiSection _activeSection = CompactUiSection.VisiblePairs;
|
private CompactUiSection _activeSection = CompactUiSection.VisiblePairs;
|
||||||
@@ -84,6 +89,7 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
private enum CompactUiSection
|
private enum CompactUiSection
|
||||||
{
|
{
|
||||||
VisiblePairs,
|
VisiblePairs,
|
||||||
|
Notifications,
|
||||||
IndividualPairs,
|
IndividualPairs,
|
||||||
Syncshells,
|
Syncshells,
|
||||||
AutoDetect,
|
AutoDetect,
|
||||||
@@ -108,7 +114,9 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
EditProfileUi editProfileUi,
|
EditProfileUi editProfileUi,
|
||||||
SettingsUi settingsUi,
|
SettingsUi settingsUi,
|
||||||
AutoDetectUi autoDetectUi,
|
AutoDetectUi autoDetectUi,
|
||||||
DataAnalysisUi dataAnalysisUi)
|
DataAnalysisUi dataAnalysisUi,
|
||||||
|
CharaDataHubUi charaDataHubUi,
|
||||||
|
NotificationTracker notificationTracker)
|
||||||
: base(logger, mediator, "###UmbraSyncMainUI", performanceCollectorService)
|
: base(logger, mediator, "###UmbraSyncMainUI", performanceCollectorService)
|
||||||
{
|
{
|
||||||
_uiSharedService = uiShared;
|
_uiSharedService = uiShared;
|
||||||
@@ -126,6 +134,8 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
_settingsUi = settingsUi;
|
_settingsUi = settingsUi;
|
||||||
_autoDetectUi = autoDetectUi;
|
_autoDetectUi = autoDetectUi;
|
||||||
_dataAnalysisUi = dataAnalysisUi;
|
_dataAnalysisUi = dataAnalysisUi;
|
||||||
|
_charaDataHubUi = charaDataHubUi;
|
||||||
|
_notificationTracker = notificationTracker;
|
||||||
var tagHandler = new TagHandler(_serverManager);
|
var tagHandler = new TagHandler(_serverManager);
|
||||||
|
|
||||||
_groupPanel = new(this, uiShared, _pairManager, chatService, uidDisplayHandler, _serverManager, _charaDataManager, _autoDetectRequestService);
|
_groupPanel = new(this, uiShared, _pairManager, chatService, uidDisplayHandler, _serverManager, _charaDataManager, _autoDetectRequestService);
|
||||||
@@ -162,6 +172,8 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
Mediator.Subscribe<NotificationStateChanged>(this, msg => _notificationCount = msg.TotalCount);
|
||||||
|
_notificationCount = _notificationTracker.Count;
|
||||||
|
|
||||||
Flags |= ImGuiWindowFlags.NoDocking;
|
Flags |= ImGuiWindowFlags.NoDocking;
|
||||||
|
|
||||||
@@ -706,7 +718,7 @@ if (showNearby && pendingInvites > 0)
|
|||||||
|
|
||||||
if (!showVisibleCard && !showNearbyCard)
|
if (!showVisibleCard && !showNearbyCard)
|
||||||
{
|
{
|
||||||
const string calmMessage = "C'est bien trop calme ici... Il n'y a rien pour le moment.";
|
const string calmMessage = "C'est bien trop calme ici... Il n'y a personne pour le moment.";
|
||||||
using (_uiSharedService.UidFont.Push())
|
using (_uiSharedService.UidFont.Push())
|
||||||
{
|
{
|
||||||
var regionMin = ImGui.GetWindowContentRegionMin();
|
var regionMin = ImGui.GetWindowContentRegionMin();
|
||||||
@@ -848,28 +860,34 @@ if (showNearby && pendingInvites > 0)
|
|||||||
ImGuiHelpers.ScaledDummy(4f);
|
ImGuiHelpers.ScaledDummy(4f);
|
||||||
var indent = 18f * ImGuiHelpers.GlobalScale;
|
var indent = 18f * ImGuiHelpers.GlobalScale;
|
||||||
ImGui.Indent(indent);
|
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))
|
ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.WidthStretch, 1f);
|
||||||
{
|
ImGui.TableSetupColumn("Action", ImGuiTableColumnFlags.WidthFixed, actionButtonSize.X);
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var name = e.DisplayName ?? e.Name;
|
foreach (var e in nearbyEntries)
|
||||||
ImGui.AlignTextToFramePadding();
|
{
|
||||||
ImGui.TextUnformatted(name);
|
if (!e.AcceptPairRequests || string.IsNullOrEmpty(e.Token))
|
||||||
var right = ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth();
|
{
|
||||||
ImGui.SameLine();
|
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))
|
using (ImRaii.PushId(e.Token ?? e.Uid ?? e.Name ?? string.Empty))
|
||||||
{
|
{
|
||||||
if (_uiSharedService.IconButton(FontAwesomeIcon.UserPlus))
|
if (_uiSharedService.IconButton(FontAwesomeIcon.UserPlus))
|
||||||
@@ -879,12 +897,9 @@ if (showNearby && pendingInvites > 0)
|
|||||||
}
|
}
|
||||||
UiSharedService.AttachToolTip("Envoyer une invitation d'apparaige");
|
UiSharedService.AttachToolTip("Envoyer une invitation d'apparaige");
|
||||||
}
|
}
|
||||||
else
|
ImGui.EndTable();
|
||||||
{
|
|
||||||
_uiSharedService.IconText(FontAwesomeIcon.QuestionCircle, ImGuiColors.DalamudGrey3);
|
|
||||||
UiSharedService.AttachToolTip("Impossible d'inviter ce joueur");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui.Unindent(indent);
|
ImGui.Unindent(indent);
|
||||||
}, stretchWidth: true);
|
}, stretchWidth: true);
|
||||||
}
|
}
|
||||||
@@ -898,6 +913,8 @@ if (showNearby && pendingInvites > 0)
|
|||||||
ImGuiHelpers.ScaledDummy(6f);
|
ImGuiHelpers.ScaledDummy(6f);
|
||||||
DrawConnectionIcon();
|
DrawConnectionIcon();
|
||||||
ImGuiHelpers.ScaledDummy(12f);
|
ImGuiHelpers.ScaledDummy(12f);
|
||||||
|
DrawSidebarButton(FontAwesomeIcon.Bell, "Notifications", CompactUiSection.Notifications, true, _notificationCount > 0, _notificationCount, null, ImGuiColors.DalamudOrange);
|
||||||
|
ImGuiHelpers.ScaledDummy(3f);
|
||||||
|
|
||||||
DrawSidebarButton(FontAwesomeIcon.Eye, "Visible pairs", CompactUiSection.VisiblePairs, isConnected);
|
DrawSidebarButton(FontAwesomeIcon.Eye, "Visible pairs", CompactUiSection.VisiblePairs, isConnected);
|
||||||
ImGuiHelpers.ScaledDummy(3f);
|
ImGuiHelpers.ScaledDummy(3f);
|
||||||
@@ -912,15 +929,9 @@ if (showNearby && pendingInvites > 0)
|
|||||||
: "AutoDetect";
|
: "AutoDetect";
|
||||||
DrawSidebarButton(FontAwesomeIcon.BroadcastTower, autoDetectTooltip, CompactUiSection.AutoDetect, isConnected, highlightAutoDetect, pendingInvites);
|
DrawSidebarButton(FontAwesomeIcon.BroadcastTower, autoDetectTooltip, CompactUiSection.AutoDetect, isConnected, highlightAutoDetect, pendingInvites);
|
||||||
ImGuiHelpers.ScaledDummy(3f);
|
ImGuiHelpers.ScaledDummy(3f);
|
||||||
DrawSidebarButton(FontAwesomeIcon.PersonCircleQuestion, "Character Analysis", CompactUiSection.CharacterAnalysis, isConnected, _dataAnalysisUi.IsOpen, 0, () =>
|
DrawSidebarButton(FontAwesomeIcon.PersonCircleQuestion, "Character Analysis", CompactUiSection.CharacterAnalysis, isConnected);
|
||||||
{
|
|
||||||
Mediator.Publish(new UiToggleMessage(typeof(DataAnalysisUi)));
|
|
||||||
});
|
|
||||||
ImGuiHelpers.ScaledDummy(3f);
|
ImGuiHelpers.ScaledDummy(3f);
|
||||||
DrawSidebarButton(FontAwesomeIcon.Running, "Character Data Hub", CompactUiSection.CharacterDataHub, isConnected, false, 0, () =>
|
DrawSidebarButton(FontAwesomeIcon.Running, "Character Data Hub", CompactUiSection.CharacterDataHub, isConnected);
|
||||||
{
|
|
||||||
Mediator.Publish(new UiToggleMessage(typeof(CharaDataHubUi)));
|
|
||||||
});
|
|
||||||
ImGuiHelpers.ScaledDummy(12f);
|
ImGuiHelpers.ScaledDummy(12f);
|
||||||
DrawSidebarButton(FontAwesomeIcon.UserCircle, "Edit Profile", CompactUiSection.EditProfile, isConnected);
|
DrawSidebarButton(FontAwesomeIcon.UserCircle, "Edit Profile", CompactUiSection.EditProfile, isConnected);
|
||||||
ImGuiHelpers.ScaledDummy(3f);
|
ImGuiHelpers.ScaledDummy(3f);
|
||||||
@@ -930,7 +941,7 @@ if (showNearby && pendingInvites > 0)
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawSidebarButton(FontAwesomeIcon icon, string tooltip, CompactUiSection section, bool enabled = true, bool highlight = false, int badgeCount = 0, Action? onClick = null)
|
private void DrawSidebarButton(FontAwesomeIcon icon, string tooltip, CompactUiSection section, bool enabled = true, bool highlight = false, int badgeCount = 0, Action? onClick = null, Vector4? highlightColor = null)
|
||||||
{
|
{
|
||||||
using var id = ImRaii.PushId((int)section);
|
using var id = ImRaii.PushId((int)section);
|
||||||
float regionWidth = ImGui.GetContentRegionAvail().X;
|
float regionWidth = ImGui.GetContentRegionAvail().X;
|
||||||
@@ -940,7 +951,7 @@ if (showNearby && pendingInvites > 0)
|
|||||||
|
|
||||||
bool isActive = _activeSection == section;
|
bool isActive = _activeSection == section;
|
||||||
|
|
||||||
if (DrawSidebarSquareButton(icon, isActive, highlight, enabled, badgeCount))
|
if (DrawSidebarSquareButton(icon, isActive, highlight, enabled, badgeCount, highlightColor))
|
||||||
{
|
{
|
||||||
if (onClick != null)
|
if (onClick != null)
|
||||||
{
|
{
|
||||||
@@ -970,7 +981,7 @@ if (showNearby && pendingInvites > 0)
|
|||||||
|
|
||||||
bool isTogglingDisabled = !hasServer || state is ServerState.Reconnecting or ServerState.Disconnecting;
|
bool isTogglingDisabled = !hasServer || state is ServerState.Reconnecting or ServerState.Disconnecting;
|
||||||
|
|
||||||
if (DrawSidebarSquareButton(icon, isLinked, false, !isTogglingDisabled, 0) && !isTogglingDisabled)
|
if (DrawSidebarSquareButton(icon, isLinked, false, !isTogglingDisabled, 0, null) && !isTogglingDisabled)
|
||||||
{
|
{
|
||||||
ToggleConnection();
|
ToggleConnection();
|
||||||
}
|
}
|
||||||
@@ -988,7 +999,7 @@ if (showNearby && pendingInvites > 0)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool DrawSidebarSquareButton(FontAwesomeIcon icon, bool isActive, bool highlight, bool enabled, int badgeCount)
|
private bool DrawSidebarSquareButton(FontAwesomeIcon icon, bool isActive, bool highlight, bool enabled, int badgeCount, Vector4? highlightColor)
|
||||||
{
|
{
|
||||||
float size = SidebarIconSize * ImGuiHelpers.GlobalScale;
|
float size = SidebarIconSize * ImGuiHelpers.GlobalScale;
|
||||||
|
|
||||||
@@ -1021,9 +1032,14 @@ if (showNearby && pendingInvites > 0)
|
|||||||
start.Y + (size - iconSize.Y) / 2f);
|
start.Y + (size - iconSize.Y) / 2f);
|
||||||
uint iconColor = ImGui.ColorConvertFloat4ToU32(new Vector4(0.85f, 0.85f, 0.9f, 1f));
|
uint iconColor = ImGui.ColorConvertFloat4ToU32(new Vector4(0.85f, 0.85f, 0.9f, 1f));
|
||||||
if (highlight)
|
if (highlight)
|
||||||
iconColor = ImGui.ColorConvertFloat4ToU32(new Vector4(0.45f, 0.85f, 0.45f, 1f));
|
{
|
||||||
|
var color = highlightColor ?? new Vector4(0.45f, 0.85f, 0.45f, 1f);
|
||||||
|
iconColor = ImGui.ColorConvertFloat4ToU32(color);
|
||||||
|
}
|
||||||
else if (isActive)
|
else if (isActive)
|
||||||
|
{
|
||||||
iconColor = ImGui.GetColorU32(ImGuiCol.Text);
|
iconColor = ImGui.GetColorU32(ImGuiCol.Text);
|
||||||
|
}
|
||||||
ImGui.GetWindowDrawList().AddText(textPos, iconColor, iconText);
|
ImGui.GetWindowDrawList().AddText(textPos, iconColor, iconText);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1093,6 +1109,9 @@ if (showNearby && pendingInvites > 0)
|
|||||||
case CompactUiSection.VisiblePairs:
|
case CompactUiSection.VisiblePairs:
|
||||||
DrawPairSection(PairContentMode.VisibleOnly);
|
DrawPairSection(PairContentMode.VisibleOnly);
|
||||||
break;
|
break;
|
||||||
|
case CompactUiSection.Notifications:
|
||||||
|
DrawNotificationsSection();
|
||||||
|
break;
|
||||||
case CompactUiSection.IndividualPairs:
|
case CompactUiSection.IndividualPairs:
|
||||||
DrawPairSection(PairContentMode.All);
|
DrawPairSection(PairContentMode.All);
|
||||||
break;
|
break;
|
||||||
@@ -1102,6 +1121,14 @@ if (showNearby && pendingInvites > 0)
|
|||||||
case CompactUiSection.AutoDetect:
|
case CompactUiSection.AutoDetect:
|
||||||
DrawAutoDetectSection();
|
DrawAutoDetectSection();
|
||||||
break;
|
break;
|
||||||
|
case CompactUiSection.CharacterAnalysis:
|
||||||
|
if (_dataAnalysisUi.IsOpen) _dataAnalysisUi.IsOpen = false;
|
||||||
|
_dataAnalysisUi.DrawInline();
|
||||||
|
break;
|
||||||
|
case CompactUiSection.CharacterDataHub:
|
||||||
|
if (_charaDataHubUi.IsOpen) _charaDataHubUi.IsOpen = false;
|
||||||
|
_charaDataHubUi.DrawInline();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
DrawNewUserNoteModal();
|
DrawNewUserNoteModal();
|
||||||
@@ -1133,6 +1160,125 @@ if (showNearby && pendingInvites > 0)
|
|||||||
using (ImRaii.PushId("autodetect-inline")) _autoDetectUi.DrawInline();
|
using (ImRaii.PushId("autodetect-inline")) _autoDetectUi.DrawInline();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void DrawNotificationsSection()
|
||||||
|
{
|
||||||
|
var notifications = _notificationTracker.GetEntries();
|
||||||
|
if (notifications.Count == 0)
|
||||||
|
{
|
||||||
|
UiSharedService.ColorTextWrapped("Aucune notification en attente.", ImGuiColors.DalamudGrey3);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var notification in notifications.OrderByDescending(n => n.CreatedAt))
|
||||||
|
{
|
||||||
|
switch (notification.Category)
|
||||||
|
{
|
||||||
|
case NotificationCategory.AutoDetect:
|
||||||
|
DrawAutoDetectNotification(notification);
|
||||||
|
break;
|
||||||
|
case NotificationCategory.Syncshell:
|
||||||
|
DrawSyncshellNotification(notification);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
UiSharedService.DrawCard($"notification-{notification.Category}-{notification.Id}", () =>
|
||||||
|
{
|
||||||
|
ImGui.TextUnformatted(notification.Title);
|
||||||
|
if (!string.IsNullOrEmpty(notification.Description))
|
||||||
|
{
|
||||||
|
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey3);
|
||||||
|
ImGui.TextUnformatted(notification.Description);
|
||||||
|
ImGui.PopStyleColor();
|
||||||
|
}
|
||||||
|
}, stretchWidth: true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGuiHelpers.ScaledDummy(4f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawAutoDetectNotification(NotificationEntry notification)
|
||||||
|
{
|
||||||
|
UiSharedService.DrawCard($"notification-autodetect-{notification.Id}", () =>
|
||||||
|
{
|
||||||
|
var label = _nearbyPending.Pending.TryGetValue(notification.Id, out var displayName)
|
||||||
|
? displayName
|
||||||
|
: notification.Title;
|
||||||
|
|
||||||
|
ImGui.TextUnformatted(label);
|
||||||
|
if (!string.IsNullOrEmpty(notification.Description))
|
||||||
|
{
|
||||||
|
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey3);
|
||||||
|
ImGui.TextWrapped(notification.Description);
|
||||||
|
ImGui.PopStyleColor();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGuiHelpers.ScaledDummy(3f);
|
||||||
|
|
||||||
|
bool hasPending = _nearbyPending.Pending.ContainsKey(notification.Id);
|
||||||
|
using (ImRaii.PushId(notification.Id))
|
||||||
|
{
|
||||||
|
using (ImRaii.Disabled(!hasPending))
|
||||||
|
{
|
||||||
|
if (ImGui.Button("Accepter"))
|
||||||
|
{
|
||||||
|
TriggerAcceptAutoDetectNotification(notification.Id);
|
||||||
|
}
|
||||||
|
ImGui.SameLine();
|
||||||
|
if (ImGui.Button("Refuser"))
|
||||||
|
{
|
||||||
|
_nearbyPending.Remove(notification.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasPending)
|
||||||
|
{
|
||||||
|
ImGui.SameLine();
|
||||||
|
if (ImGui.Button("Effacer"))
|
||||||
|
{
|
||||||
|
_notificationTracker.Remove(NotificationCategory.AutoDetect, notification.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 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 () =>
|
||||||
|
{
|
||||||
|
bool accepted = await _nearbyPending.AcceptAsync(uid).ConfigureAwait(false);
|
||||||
|
if (!accepted)
|
||||||
|
{
|
||||||
|
Mediator.Publish(new NotificationMessage("AutoDetect", $"Impossible d'accepter l'invitation {uid}.", NotificationType.Warning, TimeSpan.FromSeconds(5)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private void DrawNewUserNoteModal()
|
private void DrawNewUserNoteModal()
|
||||||
{
|
{
|
||||||
if (_configService.Current.OpenPopupOnAdd && _pairManager.LastAddedUser != null)
|
if (_configService.Current.OpenPopupOnAdd && _pairManager.LastAddedUser != null)
|
||||||
@@ -1171,9 +1317,12 @@ if (showNearby && pendingInvites > 0)
|
|||||||
private static bool RequiresServerConnection(CompactUiSection section)
|
private static bool RequiresServerConnection(CompactUiSection section)
|
||||||
{
|
{
|
||||||
return section is CompactUiSection.VisiblePairs
|
return section is CompactUiSection.VisiblePairs
|
||||||
|
or CompactUiSection.Notifications
|
||||||
or CompactUiSection.IndividualPairs
|
or CompactUiSection.IndividualPairs
|
||||||
or CompactUiSection.Syncshells
|
or CompactUiSection.Syncshells
|
||||||
or CompactUiSection.AutoDetect;
|
or CompactUiSection.AutoDetect
|
||||||
|
or CompactUiSection.CharacterAnalysis
|
||||||
|
or CompactUiSection.CharacterDataHub;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsAlreadyPairedQuickMenu(Services.Mediator.NearbyEntry entry)
|
private bool IsAlreadyPairedQuickMenu(Services.Mediator.NearbyEntry entry)
|
||||||
@@ -1288,37 +1437,61 @@ if (showNearby && pendingInvites > 0)
|
|||||||
|
|
||||||
var originalPos = ImGui.GetCursorPos();
|
var originalPos = ImGui.GetCursorPos();
|
||||||
UiSharedService.SetFontScale(1.5f);
|
UiSharedService.SetFontScale(1.5f);
|
||||||
Vector2 buttonSize = Vector2.Zero;
|
|
||||||
float spacingX = ImGui.GetStyle().ItemSpacing.X;
|
float spacingX = ImGui.GetStyle().ItemSpacing.X;
|
||||||
|
|
||||||
if (_apiController.ServerState is ServerState.Connected)
|
|
||||||
{
|
|
||||||
buttonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Copy);
|
|
||||||
ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth() - buttonSize.X);
|
|
||||||
ImGui.SetCursorPosY(originalPos.Y + uidTextSize.Y / 2f - buttonSize.Y / 2f);
|
|
||||||
if (_uiSharedService.IconButton(FontAwesomeIcon.Copy))
|
|
||||||
{
|
|
||||||
ImGui.SetClipboardText(_apiController.DisplayName);
|
|
||||||
}
|
|
||||||
UiSharedService.AttachToolTip("Copy your UID to clipboard");
|
|
||||||
ImGui.SameLine();
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.SetCursorPos(originalPos);
|
|
||||||
UiSharedService.SetFontScale(1f);
|
|
||||||
|
|
||||||
float referenceHeight = buttonSize.Y > 0f ? buttonSize.Y : ImGui.GetFrameHeight();
|
|
||||||
ImGui.SetCursorPosY(originalPos.Y + referenceHeight / 2f - uidTextSize.Y / 2f - spacingX / 2f);
|
|
||||||
float contentMin = ImGui.GetWindowContentRegionMin().X;
|
float contentMin = ImGui.GetWindowContentRegionMin().X;
|
||||||
float contentMax = ImGui.GetWindowContentRegionMax().X;
|
float contentMax = ImGui.GetWindowContentRegionMax().X;
|
||||||
float availableWidth = contentMax - contentMin;
|
float availableWidth = contentMax - contentMin;
|
||||||
float center = contentMin + availableWidth / 2f;
|
float center = contentMin + availableWidth / 2f;
|
||||||
ImGui.SetCursorPosX(center - uidTextSize.X / 2f);
|
|
||||||
|
bool isConnected = _apiController.ServerState is ServerState.Connected;
|
||||||
|
float buttonSize = 18f * ImGuiHelpers.GlobalScale;
|
||||||
|
float textPosY = originalPos.Y + MathF.Max(buttonSize, uidTextSize.Y) / 2f - uidTextSize.Y / 2f;
|
||||||
|
float textPosX = center - uidTextSize.X / 2f;
|
||||||
|
|
||||||
|
if (isConnected)
|
||||||
|
{
|
||||||
|
float buttonX = textPosX - spacingX - buttonSize;
|
||||||
|
float buttonVerticalOffset = 7f * ImGuiHelpers.GlobalScale;
|
||||||
|
float buttonY = textPosY + uidTextSize.Y - buttonSize + buttonVerticalOffset;
|
||||||
|
ImGui.SetCursorPos(new Vector2(buttonX, buttonY));
|
||||||
|
if (ImGui.Button("##copy", new Vector2(buttonSize, buttonSize)))
|
||||||
|
{
|
||||||
|
ImGui.SetClipboardText(_apiController.DisplayName);
|
||||||
|
}
|
||||||
|
var buttonMin = ImGui.GetItemRectMin();
|
||||||
|
var drawList = ImGui.GetWindowDrawList();
|
||||||
|
using (_uiSharedService.IconFont.Push())
|
||||||
|
{
|
||||||
|
string iconText = FontAwesomeIcon.Copy.ToIconString();
|
||||||
|
var baseSize = ImGui.CalcTextSize(iconText);
|
||||||
|
float maxDimension = MathF.Max(MathF.Max(baseSize.X, baseSize.Y), 1f);
|
||||||
|
float available = buttonSize - 4f;
|
||||||
|
float scale = MathF.Min(1f, available / maxDimension);
|
||||||
|
float iconWidth = baseSize.X * scale;
|
||||||
|
float iconHeight = baseSize.Y * scale;
|
||||||
|
var iconPos = new Vector2(
|
||||||
|
buttonMin.X + (buttonSize - iconWidth) / 2f,
|
||||||
|
buttonMin.Y + (buttonSize - iconHeight) / 2f);
|
||||||
|
var font = ImGui.GetFont();
|
||||||
|
float fontSize = ImGui.GetFontSize() * scale;
|
||||||
|
drawList.AddText(font, fontSize, iconPos, ImGui.GetColorU32(ImGuiCol.Text), iconText);
|
||||||
|
}
|
||||||
|
UiSharedService.AttachToolTip("Copy your UID to clipboard");
|
||||||
|
ImGui.SameLine(0f, spacingX);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ImGui.SetCursorPos(originalPos);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.SetCursorPos(new Vector2(textPosX, textPosY));
|
||||||
|
|
||||||
using (_uiSharedService.UidFont.Push())
|
using (_uiSharedService.UidFont.Push())
|
||||||
ImGui.TextColored(GetUidColor(), uidText);
|
ImGui.TextColored(GetUidColor(), uidText);
|
||||||
|
|
||||||
if (_apiController.ServerState is not ServerState.Connected)
|
UiSharedService.SetFontScale(1f);
|
||||||
|
|
||||||
|
if (!isConnected)
|
||||||
UiSharedService.ColorTextWrapped(GetServerError(), GetUidColor());
|
UiSharedService.ColorTextWrapped(GetServerError(), GetUidColor());
|
||||||
{
|
{
|
||||||
if (_apiController.ServerState is ServerState.NoSecretKey)
|
if (_apiController.ServerState is ServerState.NoSecretKey)
|
||||||
|
|||||||
@@ -79,6 +79,8 @@ public class DrawGroupPair : DrawPairBase
|
|||||||
width += _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Plus).X + spacing;
|
width += _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Plus).X + spacing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
width += spacing * 1.2f;
|
||||||
|
|
||||||
return width;
|
return width;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,6 +217,7 @@ public class DrawGroupPair : DrawPairBase
|
|||||||
var pauseIcon = _fullInfoDto.GroupUserPermissions.IsPaused() ? FontAwesomeIcon.Play : FontAwesomeIcon.Pause;
|
var pauseIcon = _fullInfoDto.GroupUserPermissions.IsPaused() ? FontAwesomeIcon.Play : FontAwesomeIcon.Pause;
|
||||||
var pauseButtonWidth = _uiSharedService.GetIconButtonSize(pauseIcon).X;
|
var pauseButtonWidth = _uiSharedService.GetIconButtonSize(pauseIcon).X;
|
||||||
var barButtonWidth = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Bars).X;
|
var barButtonWidth = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Bars).X;
|
||||||
|
var rightEdgeGap = spacing * 1.2f;
|
||||||
|
|
||||||
float totalWidth = 0f;
|
float totalWidth = 0f;
|
||||||
void Accumulate(bool condition, float width)
|
void Accumulate(bool condition, float width)
|
||||||
@@ -242,7 +245,7 @@ public class DrawGroupPair : DrawPairBase
|
|||||||
float cardPaddingX = UiSharedService.GetCardContentPaddingX();
|
float cardPaddingX = UiSharedService.GetCardContentPaddingX();
|
||||||
float rightMargin = cardPaddingX + 6f * ImGuiHelpers.GlobalScale;
|
float rightMargin = cardPaddingX + 6f * ImGuiHelpers.GlobalScale;
|
||||||
float baseX = MathF.Max(ImGui.GetCursorPosX(),
|
float baseX = MathF.Max(ImGui.GetCursorPosX(),
|
||||||
ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth() - rightMargin - totalWidth);
|
ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth() - rightMargin - rightEdgeGap - totalWidth);
|
||||||
float currentX = baseX;
|
float currentX = baseX;
|
||||||
|
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
@@ -266,6 +269,16 @@ public class DrawGroupPair : DrawPairBase
|
|||||||
|
|
||||||
if (showInfo && infoIconWidth > 0f)
|
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);
|
ImGui.SetCursorPosY(textPosY);
|
||||||
if (individualAnimDisabled || individualSoundsDisabled || individualVFXDisabled)
|
if (individualAnimDisabled || individualSoundsDisabled || individualVFXDisabled)
|
||||||
{
|
{
|
||||||
@@ -359,7 +372,7 @@ public class DrawGroupPair : DrawPairBase
|
|||||||
{
|
{
|
||||||
ImGui.SetCursorPosY(originalY);
|
ImGui.SetCursorPosY(originalY);
|
||||||
|
|
||||||
if (_uiSharedService.IconButton(FontAwesomeIcon.Plus))
|
if (_uiSharedService.IconPlusButtonCentered())
|
||||||
{
|
{
|
||||||
var targetUid = _pair.UserData.UID;
|
var targetUid = _pair.UserData.UID;
|
||||||
if (!string.IsNullOrEmpty(targetUid))
|
if (!string.IsNullOrEmpty(targetUid))
|
||||||
@@ -376,7 +389,7 @@ public class DrawGroupPair : DrawPairBase
|
|||||||
{
|
{
|
||||||
float gapToBars = showBars ? spacing * 0.5f : spacing;
|
float gapToBars = showBars ? spacing * 0.5f : spacing;
|
||||||
ImGui.SetCursorPosY(originalY);
|
ImGui.SetCursorPosY(originalY);
|
||||||
if (_uiSharedService.IconButton(pauseIcon))
|
if (pauseIcon == FontAwesomeIcon.Pause ? _uiSharedService.IconPauseButtonCentered() : _uiSharedService.IconButtonCentered(pauseIcon))
|
||||||
{
|
{
|
||||||
var newPermissions = _fullInfoDto.GroupUserPermissions ^ GroupUserPermissions.Paused;
|
var newPermissions = _fullInfoDto.GroupUserPermissions ^ GroupUserPermissions.Paused;
|
||||||
_fullInfoDto.GroupUserPermissions = newPermissions;
|
_fullInfoDto.GroupUserPermissions = newPermissions;
|
||||||
@@ -391,7 +404,7 @@ public class DrawGroupPair : DrawPairBase
|
|||||||
if (showBars)
|
if (showBars)
|
||||||
{
|
{
|
||||||
ImGui.SetCursorPosY(originalY);
|
ImGui.SetCursorPosY(originalY);
|
||||||
if (_uiSharedService.IconButton(FontAwesomeIcon.Bars))
|
if (_uiSharedService.IconButtonCentered(FontAwesomeIcon.Bars))
|
||||||
{
|
{
|
||||||
ImGui.OpenPopup("Syncshell Flyout Menu");
|
ImGui.OpenPopup("Syncshell Flyout Menu");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,8 +42,8 @@ public abstract class DrawPairBase
|
|||||||
var menuButtonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Bars);
|
var menuButtonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Bars);
|
||||||
|
|
||||||
float pauseClusterWidth = Math.Max(pauseButtonSize.X, playButtonSize.X);
|
float pauseClusterWidth = Math.Max(pauseButtonSize.X, playButtonSize.X);
|
||||||
float pauseClusterHeight = Math.Max(pauseButtonSize.Y, playButtonSize.Y);
|
float pauseClusterHeight = Math.Max(Math.Max(pauseButtonSize.Y, playButtonSize.Y), ImGui.GetFrameHeight());
|
||||||
float reservedSpacing = style.ItemSpacing.X * 2.4f;
|
float reservedSpacing = style.ItemSpacing.X * 1.6f;
|
||||||
float rightButtonWidth =
|
float rightButtonWidth =
|
||||||
menuButtonSize.X +
|
menuButtonSize.X +
|
||||||
pauseClusterWidth +
|
pauseClusterWidth +
|
||||||
@@ -84,11 +84,15 @@ public abstract class DrawPairBase
|
|||||||
|
|
||||||
ImGui.SetCursorPos(new Vector2(rowStartCursor.X + padding.X, iconTop));
|
ImGui.SetCursorPos(new Vector2(rowStartCursor.X + padding.X, iconTop));
|
||||||
DrawLeftSide(iconTop, iconTop);
|
DrawLeftSide(iconTop, iconTop);
|
||||||
ImGui.SameLine();
|
|
||||||
ImGui.SetCursorPosY(textTop);
|
float leftReserved = GetLeftSideReservedWidth();
|
||||||
var posX = ImGui.GetCursorPosX();
|
float nameStartX = rowStartCursor.X + padding.X + leftReserved;
|
||||||
|
|
||||||
var rightSide = DrawRightSide(buttonTop, buttonTop);
|
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.SetCursorPos(new Vector2(rowStartCursor.X, rowStartCursor.Y + totalHeight));
|
||||||
ImGui.SetCursorPosX(rowStartCursor.X);
|
ImGui.SetCursorPosX(rowStartCursor.X);
|
||||||
@@ -100,6 +104,8 @@ public abstract class DrawPairBase
|
|||||||
|
|
||||||
protected virtual float GetRightSideExtraWidth() => 0f;
|
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)
|
private void DrawName(float originalY, float leftSide, float rightSide)
|
||||||
{
|
{
|
||||||
_displayHandler.DrawPairText(_id, _pair, leftSide, originalY, () => rightSide - leftSide);
|
_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 += _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Running).X + spacingX * 0.5f;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
width += spacingX * 1.2f;
|
||||||
return width;
|
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)
|
protected override void DrawLeftSide(float textPosY, float originalY)
|
||||||
{
|
{
|
||||||
@@ -133,7 +153,8 @@ public class DrawUserPair : DrawPairBase
|
|||||||
var entryUID = _pair.UserData.AliasOrUID;
|
var entryUID = _pair.UserData.AliasOrUID;
|
||||||
var spacingX = ImGui.GetStyle().ItemSpacing.X;
|
var spacingX = ImGui.GetStyle().ItemSpacing.X;
|
||||||
var edgePadding = UiSharedService.GetCardContentPaddingX() + 6f * ImGuiHelpers.GlobalScale;
|
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;
|
var rightSidePos = windowEndX - barButtonSize.X;
|
||||||
|
|
||||||
// Flyout Menu
|
// Flyout Menu
|
||||||
@@ -150,13 +171,12 @@ public class DrawUserPair : DrawPairBase
|
|||||||
ImGui.EndPopup();
|
ImGui.EndPopup();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pause (mutual pairs only)
|
|
||||||
if (_pair.UserPair!.OwnPermissions.IsPaired() && _pair.UserPair!.OtherPermissions.IsPaired())
|
if (_pair.UserPair!.OwnPermissions.IsPaired() && _pair.UserPair!.OtherPermissions.IsPaired())
|
||||||
{
|
{
|
||||||
rightSidePos -= pauseIconSize.X + spacingX;
|
rightSidePos -= pauseIconSize.X + spacingX;
|
||||||
ImGui.SameLine(rightSidePos);
|
ImGui.SameLine(rightSidePos);
|
||||||
ImGui.SetCursorPosY(originalY);
|
ImGui.SetCursorPosY(originalY);
|
||||||
if (_uiSharedService.IconButton(pauseIcon))
|
if (pauseIcon == FontAwesomeIcon.Pause ? _uiSharedService.IconPauseButtonCentered() : _uiSharedService.IconButtonCentered(pauseIcon))
|
||||||
{
|
{
|
||||||
var perm = _pair.UserPair!.OwnPermissions;
|
var perm = _pair.UserPair!.OwnPermissions;
|
||||||
perm.SetPaused(!perm.IsPaused());
|
perm.SetPaused(!perm.IsPaused());
|
||||||
|
|||||||
@@ -65,6 +65,19 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected override void DrawInternal()
|
protected override void DrawInternal()
|
||||||
|
{
|
||||||
|
DrawAnalysisContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DrawInline()
|
||||||
|
{
|
||||||
|
using (ImRaii.PushId("CharacterAnalysisInline"))
|
||||||
|
{
|
||||||
|
DrawAnalysisContent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawAnalysisContent()
|
||||||
{
|
{
|
||||||
if (_conversionTask != null && !_conversionTask.IsCompleted)
|
if (_conversionTask != null && !_conversionTask.IsCompleted)
|
||||||
{
|
{
|
||||||
@@ -116,7 +129,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
|||||||
if (isAnalyzing)
|
if (isAnalyzing)
|
||||||
{
|
{
|
||||||
UiSharedService.ColorTextWrapped($"Analyzing {_characterAnalyzer.CurrentFile}/{_characterAnalyzer.TotalFiles}",
|
UiSharedService.ColorTextWrapped($"Analyzing {_characterAnalyzer.CurrentFile}/{_characterAnalyzer.TotalFiles}",
|
||||||
ImGuiColors.DalamudYellow);
|
UiSharedService.AccentColor);
|
||||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.StopCircle, "Cancel analysis"))
|
if (_uiSharedService.IconTextButton(FontAwesomeIcon.StopCircle, "Cancel analysis"))
|
||||||
{
|
{
|
||||||
_characterAnalyzer.CancelAnalyze();
|
_characterAnalyzer.CancelAnalyze();
|
||||||
@@ -127,7 +140,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
|||||||
if (needAnalysis)
|
if (needAnalysis)
|
||||||
{
|
{
|
||||||
UiSharedService.ColorTextWrapped("Some entries in the analysis have file size not determined yet, press the button below to analyze your current data",
|
UiSharedService.ColorTextWrapped("Some entries in the analysis have file size not determined yet, press the button below to analyze your current data",
|
||||||
ImGuiColors.DalamudYellow);
|
UiSharedService.AccentColor);
|
||||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.PlayCircle, "Start analysis (missing entries)"))
|
if (_uiSharedService.IconTextButton(FontAwesomeIcon.PlayCircle, "Start analysis (missing entries)"))
|
||||||
{
|
{
|
||||||
_ = _characterAnalyzer.ComputeAnalysis(print: false);
|
_ = _characterAnalyzer.ComputeAnalysis(print: false);
|
||||||
@@ -166,7 +179,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
|||||||
ImGui.TextUnformatted(UiSharedService.ByteToString(_cachedAnalysis!.Sum(c => c.Value.Sum(c => c.Value.OriginalSize))));
|
ImGui.TextUnformatted(UiSharedService.ByteToString(_cachedAnalysis!.Sum(c => c.Value.Sum(c => c.Value.OriginalSize))));
|
||||||
ImGui.TextUnformatted("Total size (download size):");
|
ImGui.TextUnformatted("Total size (download size):");
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudYellow, needAnalysis))
|
using (ImRaii.PushColor(ImGuiCol.Text, UiSharedService.AccentColor, needAnalysis))
|
||||||
{
|
{
|
||||||
ImGui.TextUnformatted(UiSharedService.ByteToString(_cachedAnalysis!.Sum(c => c.Value.Sum(c => c.Value.CompressedSize))));
|
ImGui.TextUnformatted(UiSharedService.ByteToString(_cachedAnalysis!.Sum(c => c.Value.Sum(c => c.Value.CompressedSize))));
|
||||||
if (needAnalysis && !isAnalyzing)
|
if (needAnalysis && !isAnalyzing)
|
||||||
@@ -180,140 +193,149 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
|||||||
ImGui.TextUnformatted($"Total modded model triangles: {UiSharedService.TrisToString(_cachedAnalysis.Sum(c => c.Value.Sum(f => f.Value.Triangles)))}");
|
ImGui.TextUnformatted($"Total modded model triangles: {UiSharedService.TrisToString(_cachedAnalysis.Sum(c => c.Value.Sum(f => f.Value.Triangles)))}");
|
||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
|
|
||||||
using var tabbar = ImRaii.TabBar("objectSelection");
|
|
||||||
foreach (var kvp in _cachedAnalysis)
|
|
||||||
{
|
{
|
||||||
using var id = ImRaii.PushId(kvp.Key.ToString());
|
using var objectTabColor = ImRaii.PushColor(ImGuiCol.Tab, UiSharedService.AccentColor);
|
||||||
string tabText = kvp.Key.ToString();
|
using var objectTabHoverColor = ImRaii.PushColor(ImGuiCol.TabHovered, UiSharedService.AccentHoverColor);
|
||||||
using var tab = ImRaii.TabItem(tabText + "###" + kvp.Key.ToString());
|
using var objectTabActiveColor = ImRaii.PushColor(ImGuiCol.TabActive, UiSharedService.AccentActiveColor);
|
||||||
if (tab.Success)
|
using var tabbar = ImRaii.TabBar("objectSelection");
|
||||||
|
foreach (var kvp in _cachedAnalysis)
|
||||||
{
|
{
|
||||||
var groupedfiles = kvp.Value.Select(v => v.Value).GroupBy(f => f.FileType, StringComparer.Ordinal)
|
using var id = ImRaii.PushId(kvp.Key.ToString());
|
||||||
.OrderBy(k => k.Key, StringComparer.Ordinal).ToList();
|
string tabText = kvp.Key.ToString();
|
||||||
|
using var tab = ImRaii.TabItem(tabText + "###" + kvp.Key.ToString());
|
||||||
|
if (tab.Success)
|
||||||
|
{
|
||||||
|
var groupedfiles = kvp.Value.Select(v => v.Value).GroupBy(f => f.FileType, StringComparer.Ordinal)
|
||||||
|
.OrderBy(k => k.Key, StringComparer.Ordinal).ToList();
|
||||||
|
|
||||||
ImGui.TextUnformatted("Files for " + kvp.Key);
|
ImGui.TextUnformatted("Files for " + kvp.Key);
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
ImGui.TextUnformatted(kvp.Value.Count.ToString());
|
ImGui.TextUnformatted(kvp.Value.Count.ToString());
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
|
|
||||||
using (var font = ImRaii.PushFont(UiBuilder.IconFont))
|
using (var font = ImRaii.PushFont(UiBuilder.IconFont))
|
||||||
{
|
|
||||||
ImGui.TextUnformatted(FontAwesomeIcon.InfoCircle.ToIconString());
|
|
||||||
}
|
|
||||||
if (ImGui.IsItemHovered())
|
|
||||||
{
|
|
||||||
string text = "";
|
|
||||||
text = string.Join(Environment.NewLine, groupedfiles
|
|
||||||
.Select(f => f.Key + ": " + f.Count() + " files, size: " + UiSharedService.ByteToString(f.Sum(v => v.OriginalSize))
|
|
||||||
+ ", compressed: " + UiSharedService.ByteToString(f.Sum(v => v.CompressedSize))));
|
|
||||||
ImGui.SetTooltip(text);
|
|
||||||
}
|
|
||||||
ImGui.TextUnformatted($"{kvp.Key} size (actual):");
|
|
||||||
ImGui.SameLine();
|
|
||||||
ImGui.TextUnformatted(UiSharedService.ByteToString(kvp.Value.Sum(c => c.Value.OriginalSize)));
|
|
||||||
ImGui.TextUnformatted($"{kvp.Key} size (download size):");
|
|
||||||
ImGui.SameLine();
|
|
||||||
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudYellow, needAnalysis))
|
|
||||||
{
|
|
||||||
ImGui.TextUnformatted(UiSharedService.ByteToString(kvp.Value.Sum(c => c.Value.CompressedSize)));
|
|
||||||
if (needAnalysis && !isAnalyzing)
|
|
||||||
{
|
{
|
||||||
ImGui.SameLine();
|
ImGui.TextUnformatted(FontAwesomeIcon.InfoCircle.ToIconString());
|
||||||
using (ImRaii.PushFont(UiBuilder.IconFont))
|
|
||||||
ImGui.TextUnformatted(FontAwesomeIcon.ExclamationCircle.ToIconString());
|
|
||||||
UiSharedService.AttachToolTip("Click \"Start analysis\" to calculate download size");
|
|
||||||
}
|
}
|
||||||
}
|
if (ImGui.IsItemHovered())
|
||||||
ImGui.TextUnformatted($"{kvp.Key} VRAM usage:");
|
|
||||||
ImGui.SameLine();
|
|
||||||
var vramUsage = groupedfiles.SingleOrDefault(v => string.Equals(v.Key, "tex", StringComparison.Ordinal));
|
|
||||||
if (vramUsage != null)
|
|
||||||
{
|
|
||||||
ImGui.TextUnformatted(UiSharedService.ByteToString(vramUsage.Sum(f => f.OriginalSize)));
|
|
||||||
}
|
|
||||||
ImGui.TextUnformatted($"{kvp.Key} modded model triangles: {UiSharedService.TrisToString(kvp.Value.Sum(f => f.Value.Triangles))}");
|
|
||||||
|
|
||||||
ImGui.Separator();
|
|
||||||
if (_selectedObjectTab != kvp.Key)
|
|
||||||
{
|
|
||||||
_selectedHash = string.Empty;
|
|
||||||
_selectedObjectTab = kvp.Key;
|
|
||||||
_selectedFileTypeTab = string.Empty;
|
|
||||||
_enableBc7ConversionMode = false;
|
|
||||||
_texturesToConvert.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
using var fileTabBar = ImRaii.TabBar("fileTabs");
|
|
||||||
|
|
||||||
foreach (IGrouping<string, CharacterAnalyzer.FileDataEntry>? fileGroup in groupedfiles)
|
|
||||||
{
|
|
||||||
string fileGroupText = fileGroup.Key + " [" + fileGroup.Count() + "]";
|
|
||||||
var requiresCompute = fileGroup.Any(k => !k.IsComputed);
|
|
||||||
using var tabcol = ImRaii.PushColor(ImGuiCol.Tab, UiSharedService.Color(ImGuiColors.DalamudYellow), requiresCompute);
|
|
||||||
ImRaii.IEndObject fileTab;
|
|
||||||
using (var textcol = ImRaii.PushColor(ImGuiCol.Text, UiSharedService.Color(new(0, 0, 0, 1)),
|
|
||||||
requiresCompute && !string.Equals(_selectedFileTypeTab, fileGroup.Key, StringComparison.Ordinal)))
|
|
||||||
{
|
{
|
||||||
fileTab = ImRaii.TabItem(fileGroupText + "###" + fileGroup.Key);
|
string text = "";
|
||||||
|
text = string.Join(Environment.NewLine, groupedfiles
|
||||||
|
.Select(f => f.Key + ": " + f.Count() + " files, size: " + UiSharedService.ByteToString(f.Sum(v => v.OriginalSize))
|
||||||
|
+ ", compressed: " + UiSharedService.ByteToString(f.Sum(v => v.CompressedSize))));
|
||||||
|
ImGui.SetTooltip(text);
|
||||||
}
|
}
|
||||||
|
ImGui.TextUnformatted($"{kvp.Key} size (actual):");
|
||||||
if (!fileTab) { fileTab.Dispose(); continue; }
|
ImGui.SameLine();
|
||||||
|
ImGui.TextUnformatted(UiSharedService.ByteToString(kvp.Value.Sum(c => c.Value.OriginalSize)));
|
||||||
if (!string.Equals(fileGroup.Key, _selectedFileTypeTab, StringComparison.Ordinal))
|
ImGui.TextUnformatted($"{kvp.Key} size (download size):");
|
||||||
|
ImGui.SameLine();
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.Text, UiSharedService.AccentColor, needAnalysis))
|
||||||
|
{
|
||||||
|
ImGui.TextUnformatted(UiSharedService.ByteToString(kvp.Value.Sum(c => c.Value.CompressedSize)));
|
||||||
|
if (needAnalysis && !isAnalyzing)
|
||||||
|
{
|
||||||
|
ImGui.SameLine();
|
||||||
|
using (ImRaii.PushFont(UiBuilder.IconFont))
|
||||||
|
ImGui.TextUnformatted(FontAwesomeIcon.ExclamationCircle.ToIconString());
|
||||||
|
UiSharedService.AttachToolTip("Click \"Start analysis\" to calculate download size");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui.TextUnformatted($"{kvp.Key} VRAM usage:");
|
||||||
|
ImGui.SameLine();
|
||||||
|
var vramUsage = groupedfiles.SingleOrDefault(v => string.Equals(v.Key, "tex", StringComparison.Ordinal));
|
||||||
|
if (vramUsage != null)
|
||||||
|
{
|
||||||
|
ImGui.TextUnformatted(UiSharedService.ByteToString(vramUsage.Sum(f => f.OriginalSize)));
|
||||||
|
}
|
||||||
|
ImGui.TextUnformatted($"{kvp.Key} modded model triangles: {UiSharedService.TrisToString(kvp.Value.Sum(f => f.Value.Triangles))}");
|
||||||
|
|
||||||
|
ImGui.Separator();
|
||||||
|
if (_selectedObjectTab != kvp.Key)
|
||||||
{
|
{
|
||||||
_selectedFileTypeTab = fileGroup.Key;
|
|
||||||
_selectedHash = string.Empty;
|
_selectedHash = string.Empty;
|
||||||
|
_selectedObjectTab = kvp.Key;
|
||||||
|
_selectedFileTypeTab = string.Empty;
|
||||||
_enableBc7ConversionMode = false;
|
_enableBc7ConversionMode = false;
|
||||||
_texturesToConvert.Clear();
|
_texturesToConvert.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui.TextUnformatted($"{fileGroup.Key} files");
|
using var fileTabColor = ImRaii.PushColor(ImGuiCol.Tab, UiSharedService.AccentColor);
|
||||||
ImGui.SameLine();
|
using var fileTabHoverColor = ImRaii.PushColor(ImGuiCol.TabHovered, UiSharedService.AccentHoverColor);
|
||||||
ImGui.TextUnformatted(fileGroup.Count().ToString());
|
using var fileTabActiveColor = ImRaii.PushColor(ImGuiCol.TabActive, UiSharedService.AccentActiveColor);
|
||||||
|
using var fileTabBar = ImRaii.TabBar("fileTabs");
|
||||||
|
|
||||||
ImGui.TextUnformatted($"{fileGroup.Key} files size (actual):");
|
foreach (IGrouping<string, CharacterAnalyzer.FileDataEntry>? fileGroup in groupedfiles)
|
||||||
ImGui.SameLine();
|
|
||||||
ImGui.TextUnformatted(UiSharedService.ByteToString(fileGroup.Sum(c => c.OriginalSize)));
|
|
||||||
|
|
||||||
ImGui.TextUnformatted($"{fileGroup.Key} files size (download size):");
|
|
||||||
ImGui.SameLine();
|
|
||||||
ImGui.TextUnformatted(UiSharedService.ByteToString(fileGroup.Sum(c => c.CompressedSize)));
|
|
||||||
|
|
||||||
if (string.Equals(_selectedFileTypeTab, "tex", StringComparison.Ordinal))
|
|
||||||
{
|
{
|
||||||
ImGui.Checkbox("Enable BC7 Conversion Mode", ref _enableBc7ConversionMode);
|
string fileGroupText = fileGroup.Key + " [" + fileGroup.Count() + "]";
|
||||||
if (_enableBc7ConversionMode)
|
var requiresCompute = fileGroup.Any(k => !k.IsComputed);
|
||||||
|
ImRaii.IEndObject fileTab;
|
||||||
|
using (var textcol = ImRaii.PushColor(ImGuiCol.Text, UiSharedService.Color(Vector4.One),
|
||||||
|
requiresCompute && !string.Equals(_selectedFileTypeTab, fileGroup.Key, StringComparison.Ordinal)))
|
||||||
{
|
{
|
||||||
UiSharedService.ColorText("WARNING BC7 CONVERSION:", ImGuiColors.DalamudYellow);
|
fileTab = ImRaii.TabItem(fileGroupText + "###" + fileGroup.Key);
|
||||||
ImGui.SameLine();
|
}
|
||||||
UiSharedService.ColorText("Converting textures to BC7 is irreversible!", UiSharedService.AccentColor);
|
|
||||||
UiSharedService.ColorTextWrapped("- Converting textures to BC7 will reduce their size (compressed and uncompressed) drastically. It is recommended to be used for large (4k+) textures." +
|
if (!fileTab) { fileTab.Dispose(); continue; }
|
||||||
Environment.NewLine + "- Some textures, especially ones utilizing colorsets, might not be suited for BC7 conversion and might produce visual artifacts." +
|
|
||||||
Environment.NewLine + "- Before converting textures, make sure to have the original files of the mod you are converting so you can reimport it in case of issues." +
|
if (!string.Equals(fileGroup.Key, _selectedFileTypeTab, StringComparison.Ordinal))
|
||||||
Environment.NewLine + "- Conversion will convert all found texture duplicates (entries with more than 1 file path) automatically." +
|
{
|
||||||
Environment.NewLine + "- Converting textures to BC7 is a very expensive operation and, depending on the amount of textures to convert, will take a while to complete."
|
_selectedFileTypeTab = fileGroup.Key;
|
||||||
, ImGuiColors.DalamudYellow);
|
_selectedHash = string.Empty;
|
||||||
if (_texturesToConvert.Count > 0 && _uiSharedService.IconTextButton(FontAwesomeIcon.PlayCircle, "Start conversion of " + _texturesToConvert.Count + " texture(s)"))
|
_enableBc7ConversionMode = false;
|
||||||
|
_texturesToConvert.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.TextUnformatted($"{fileGroup.Key} files");
|
||||||
|
ImGui.SameLine();
|
||||||
|
ImGui.TextUnformatted(fileGroup.Count().ToString());
|
||||||
|
|
||||||
|
ImGui.TextUnformatted($"{fileGroup.Key} files size (actual):");
|
||||||
|
ImGui.SameLine();
|
||||||
|
ImGui.TextUnformatted(UiSharedService.ByteToString(fileGroup.Sum(c => c.OriginalSize)));
|
||||||
|
|
||||||
|
ImGui.TextUnformatted($"{fileGroup.Key} files size (download size):");
|
||||||
|
ImGui.SameLine();
|
||||||
|
ImGui.TextUnformatted(UiSharedService.ByteToString(fileGroup.Sum(c => c.CompressedSize)));
|
||||||
|
|
||||||
|
if (string.Equals(_selectedFileTypeTab, "tex", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
ImGui.Checkbox("Enable BC7 Conversion Mode", ref _enableBc7ConversionMode);
|
||||||
|
if (_enableBc7ConversionMode)
|
||||||
{
|
{
|
||||||
var conversionCts = EnsureFreshCts(ref _conversionCancellationTokenSource);
|
UiSharedService.ColorText("WARNING BC7 CONVERSION:", UiSharedService.AccentColor);
|
||||||
_conversionTask = _ipcManager.Penumbra.ConvertTextureFiles(_logger, _texturesToConvert, _conversionProgress, conversionCts.Token);
|
ImGui.SameLine();
|
||||||
|
UiSharedService.ColorText("Converting textures to BC7 is irreversible!", UiSharedService.AccentColor);
|
||||||
|
UiSharedService.ColorTextWrapped("- Converting textures to BC7 will reduce their size (compressed and uncompressed) drastically. It is recommended to be used for large (4k+) textures." +
|
||||||
|
Environment.NewLine + "- Some textures, especially ones utilizing colorsets, might not be suited for BC7 conversion and might produce visual artifacts." +
|
||||||
|
Environment.NewLine + "- Before converting textures, make sure to have the original files of the mod you are converting so you can reimport it in case of issues." +
|
||||||
|
Environment.NewLine + "- Conversion will convert all found texture duplicates (entries with more than 1 file path) automatically." +
|
||||||
|
Environment.NewLine + "- Converting textures to BC7 is a very expensive operation and, depending on the amount of textures to convert, will take a while to complete."
|
||||||
|
, UiSharedService.AccentColor);
|
||||||
|
if (_texturesToConvert.Count > 0 && _uiSharedService.IconTextButton(FontAwesomeIcon.PlayCircle, "Start conversion of " + _texturesToConvert.Count + " texture(s)"))
|
||||||
|
{
|
||||||
|
var conversionCts = EnsureFreshCts(ref _conversionCancellationTokenSource);
|
||||||
|
_conversionTask = _ipcManager.Penumbra.ConvertTextureFiles(_logger, _texturesToConvert, _conversionProgress, conversionCts.Token);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ImGui.Separator();
|
||||||
|
DrawTable(fileGroup);
|
||||||
|
|
||||||
|
fileTab.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui.Separator();
|
|
||||||
DrawTable(fileGroup);
|
|
||||||
|
|
||||||
fileTab.Dispose();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
|
|
||||||
ImGui.TextUnformatted("Selected file:");
|
ImGui.TextUnformatted("Selected file:");
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
UiSharedService.ColorText(_selectedHash, ImGuiColors.DalamudYellow);
|
UiSharedService.ColorText(_selectedHash, UiSharedService.AccentColor);
|
||||||
|
|
||||||
if (_cachedAnalysis[_selectedObjectTab].TryGetValue(_selectedHash, out CharacterAnalyzer.FileDataEntry? item))
|
if (_cachedAnalysis[_selectedObjectTab].TryGetValue(_selectedHash, out CharacterAnalyzer.FileDataEntry? item))
|
||||||
{
|
{
|
||||||
@@ -440,8 +462,8 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
|||||||
ImGui.TableNextColumn();
|
ImGui.TableNextColumn();
|
||||||
if (string.Equals(_selectedHash, item.Hash, StringComparison.Ordinal))
|
if (string.Equals(_selectedHash, item.Hash, StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg1, UiSharedService.Color(ImGuiColors.DalamudYellow));
|
ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg1, UiSharedService.Color(UiSharedService.AccentColor));
|
||||||
ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg0, UiSharedService.Color(ImGuiColors.DalamudYellow));
|
ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg0, UiSharedService.Color(UiSharedService.AccentColor));
|
||||||
}
|
}
|
||||||
ImGui.TextUnformatted(item.Hash);
|
ImGui.TextUnformatted(item.Hash);
|
||||||
if (ImGui.IsItemClicked()) _selectedHash = item.Hash;
|
if (ImGui.IsItemClicked()) _selectedHash = item.Hash;
|
||||||
@@ -455,7 +477,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
|||||||
ImGui.TextUnformatted(UiSharedService.ByteToString(item.OriginalSize));
|
ImGui.TextUnformatted(UiSharedService.ByteToString(item.OriginalSize));
|
||||||
if (ImGui.IsItemClicked()) _selectedHash = item.Hash;
|
if (ImGui.IsItemClicked()) _selectedHash = item.Hash;
|
||||||
ImGui.TableNextColumn();
|
ImGui.TableNextColumn();
|
||||||
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudYellow, !item.IsComputed))
|
using (ImRaii.PushColor(ImGuiCol.Text, UiSharedService.AccentColor, !item.IsComputed))
|
||||||
ImGui.TextUnformatted(UiSharedService.ByteToString(item.CompressedSize));
|
ImGui.TextUnformatted(UiSharedService.ByteToString(item.CompressedSize));
|
||||||
if (ImGui.IsItemClicked()) _selectedHash = item.Hash;
|
if (ImGui.IsItemClicked()) _selectedHash = item.Hash;
|
||||||
if (string.Equals(fileGroup.Key, "tex", StringComparison.Ordinal))
|
if (string.Equals(fileGroup.Key, "tex", StringComparison.Ordinal))
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
private readonly AccountRegistrationService _registerService;
|
private readonly AccountRegistrationService _registerService;
|
||||||
private readonly ServerConfigurationManager _serverConfigurationManager;
|
private readonly ServerConfigurationManager _serverConfigurationManager;
|
||||||
private readonly UiSharedService _uiShared;
|
private readonly UiSharedService _uiShared;
|
||||||
|
private readonly TypingIndicatorStateService _typingStateService;
|
||||||
|
private readonly ChatTypingDetectionService _chatTypingDetectionService;
|
||||||
private static readonly string DtrDefaultPreviewText = DtrEntry.DefaultGlyph + " 123";
|
private static readonly string DtrDefaultPreviewText = DtrEntry.DefaultGlyph + " 123";
|
||||||
private bool _deleteAccountPopupModalShown = false;
|
private bool _deleteAccountPopupModalShown = false;
|
||||||
private string _lastTab = string.Empty;
|
private string _lastTab = string.Empty;
|
||||||
@@ -80,7 +82,9 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
FileCompactor fileCompactor, ApiController apiController,
|
FileCompactor fileCompactor, ApiController apiController,
|
||||||
IpcManager ipcManager, IpcProvider ipcProvider, CacheMonitor cacheMonitor,
|
IpcManager ipcManager, IpcProvider ipcProvider, CacheMonitor cacheMonitor,
|
||||||
DalamudUtilService dalamudUtilService, AccountRegistrationService registerService,
|
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;
|
_configService = configService;
|
||||||
_pairManager = pairManager;
|
_pairManager = pairManager;
|
||||||
@@ -102,6 +106,8 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
_autoDetectSuppressionService = autoDetectSuppressionService;
|
_autoDetectSuppressionService = autoDetectSuppressionService;
|
||||||
_fileCompactor = fileCompactor;
|
_fileCompactor = fileCompactor;
|
||||||
_uiShared = uiShared;
|
_uiShared = uiShared;
|
||||||
|
_typingStateService = typingIndicatorStateService;
|
||||||
|
_chatTypingDetectionService = chatTypingDetectionService;
|
||||||
AllowClickthrough = false;
|
AllowClickthrough = false;
|
||||||
AllowPinning = false;
|
AllowPinning = false;
|
||||||
_validationProgress = new Progress<(int, int, FileCacheEntity)>(v => _currentProgress = v);
|
_validationProgress = new Progress<(int, int, FileCacheEntity)>(v => _currentProgress = v);
|
||||||
@@ -1123,8 +1129,10 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
var useNameColors = _configService.Current.UseNameColors;
|
var useNameColors = _configService.Current.UseNameColors;
|
||||||
var nameColors = _configService.Current.NameColors;
|
var nameColors = _configService.Current.NameColors;
|
||||||
var autoPausedNameColors = _configService.Current.BlockedNameColors;
|
var autoPausedNameColors = _configService.Current.BlockedNameColors;
|
||||||
|
var typingEnabled = _configService.Current.TypingIndicatorEnabled;
|
||||||
var typingIndicatorNameplates = _configService.Current.TypingIndicatorShowOnNameplates;
|
var typingIndicatorNameplates = _configService.Current.TypingIndicatorShowOnNameplates;
|
||||||
var typingIndicatorPartyList = _configService.Current.TypingIndicatorShowOnPartyList;
|
var typingIndicatorPartyList = _configService.Current.TypingIndicatorShowOnPartyList;
|
||||||
|
var typingShowSelf = _configService.Current.TypingIndicatorShowSelf;
|
||||||
if (ImGui.Checkbox("Coloriser les plaques de nom des paires", ref useNameColors))
|
if (ImGui.Checkbox("Coloriser les plaques de nom des paires", ref useNameColors))
|
||||||
{
|
{
|
||||||
_configService.Current.UseNameColors = 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();
|
_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();
|
if (ImGui.Checkbox("Afficher la bulle de frappe sur les plaques", ref typingIndicatorNameplates))
|
||||||
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.Current.TypingIndicatorShowOnNameplates = typingIndicatorNameplates;
|
||||||
_configService.Save();
|
_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))
|
if (typingIndicatorNameplates)
|
||||||
{
|
{
|
||||||
_configService.Current.TypingIndicatorShowOnPartyList = typingIndicatorPartyList;
|
using var indentTyping = ImRaii.PushIndent();
|
||||||
_configService.Save();
|
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))
|
if (ImGui.Checkbox("Show separate Visible group", ref showVisibleSeparate))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,15 +3,21 @@ using Dalamud.Interface;
|
|||||||
using Dalamud.Interface.Colors;
|
using Dalamud.Interface.Colors;
|
||||||
using Dalamud.Interface.Utility;
|
using Dalamud.Interface.Utility;
|
||||||
using Dalamud.Interface.Utility.Raii;
|
using Dalamud.Interface.Utility.Raii;
|
||||||
|
using System;
|
||||||
using MareSynchronos.API.Data.Enum;
|
using MareSynchronos.API.Data.Enum;
|
||||||
using MareSynchronos.API.Data.Extensions;
|
using MareSynchronos.API.Data.Extensions;
|
||||||
using MareSynchronos.API.Dto.Group;
|
using MareSynchronos.API.Dto.Group;
|
||||||
using MareSynchronos.PlayerData.Pairs;
|
using MareSynchronos.PlayerData.Pairs;
|
||||||
using MareSynchronos.Services;
|
using MareSynchronos.Services;
|
||||||
|
using MareSynchronos.Services.AutoDetect;
|
||||||
using MareSynchronos.Services.Mediator;
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using MareSynchronos.Services.Notifications;
|
||||||
using MareSynchronos.WebAPI;
|
using MareSynchronos.WebAPI;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using MareSynchronos.MareConfiguration.Models;
|
||||||
|
|
||||||
namespace MareSynchronos.UI.Components.Popup;
|
namespace MareSynchronos.UI.Components.Popup;
|
||||||
|
|
||||||
@@ -23,6 +29,8 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
|||||||
private readonly List<string> _oneTimeInvites = [];
|
private readonly List<string> _oneTimeInvites = [];
|
||||||
private readonly PairManager _pairManager;
|
private readonly PairManager _pairManager;
|
||||||
private readonly UiSharedService _uiSharedService;
|
private readonly UiSharedService _uiSharedService;
|
||||||
|
private readonly SyncshellDiscoveryService _syncshellDiscoveryService;
|
||||||
|
private readonly NotificationTracker _notificationTracker;
|
||||||
private List<BannedGroupUserDto> _bannedUsers = [];
|
private List<BannedGroupUserDto> _bannedUsers = [];
|
||||||
private int _multiInvites;
|
private int _multiInvites;
|
||||||
private string _newPassword;
|
private string _newPassword;
|
||||||
@@ -30,20 +38,43 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
|||||||
private Task<int>? _pruneTestTask;
|
private Task<int>? _pruneTestTask;
|
||||||
private Task<int>? _pruneTask;
|
private Task<int>? _pruneTask;
|
||||||
private int _pruneDays = 14;
|
private int _pruneDays = 14;
|
||||||
|
private bool _autoDetectStateInitialized;
|
||||||
|
private bool _autoDetectStateLoading;
|
||||||
|
private bool _autoDetectToggleInFlight;
|
||||||
|
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,
|
public SyncshellAdminUI(ILogger<SyncshellAdminUI> logger, MareMediator mediator, ApiController apiController,
|
||||||
UiSharedService uiSharedService, PairManager pairManager, GroupFullInfoDto groupFullInfo, PerformanceCollectorService performanceCollectorService)
|
UiSharedService uiSharedService, PairManager pairManager, SyncshellDiscoveryService syncshellDiscoveryService,
|
||||||
|
GroupFullInfoDto groupFullInfo, PerformanceCollectorService performanceCollectorService, NotificationTracker notificationTracker)
|
||||||
: base(logger, mediator, "Syncshell Admin Panel (" + groupFullInfo.GroupAliasOrGID + ")", performanceCollectorService)
|
: base(logger, mediator, "Syncshell Admin Panel (" + groupFullInfo.GroupAliasOrGID + ")", performanceCollectorService)
|
||||||
{
|
{
|
||||||
GroupFullInfo = groupFullInfo;
|
GroupFullInfo = groupFullInfo;
|
||||||
_apiController = apiController;
|
_apiController = apiController;
|
||||||
_uiSharedService = uiSharedService;
|
_uiSharedService = uiSharedService;
|
||||||
_pairManager = pairManager;
|
_pairManager = pairManager;
|
||||||
|
_syncshellDiscoveryService = syncshellDiscoveryService;
|
||||||
|
_notificationTracker = notificationTracker;
|
||||||
_isOwner = string.Equals(GroupFullInfo.OwnerUID, _apiController.UID, StringComparison.Ordinal);
|
_isOwner = string.Equals(GroupFullInfo.OwnerUID, _apiController.UID, StringComparison.Ordinal);
|
||||||
_isModerator = GroupFullInfo.GroupUserInfo.IsModerator();
|
_isModerator = GroupFullInfo.GroupUserInfo.IsModerator();
|
||||||
_newPassword = string.Empty;
|
_newPassword = string.Empty;
|
||||||
_multiInvites = 30;
|
_multiInvites = 30;
|
||||||
_pwChangeSuccess = true;
|
_pwChangeSuccess = true;
|
||||||
|
_autoDetectVisible = groupFullInfo.AutoDetectVisible;
|
||||||
|
_autoDetectDesiredVisibility = _autoDetectVisible;
|
||||||
|
_autoDetectPasswordDisabled = groupFullInfo.PasswordTemporarilyDisabled;
|
||||||
|
Mediator.Subscribe<SyncshellAutoDetectStateChanged>(this, OnSyncshellAutoDetectStateChanged);
|
||||||
IsOpen = true;
|
IsOpen = true;
|
||||||
SizeConstraints = new WindowSizeConstraints()
|
SizeConstraints = new WindowSizeConstraints()
|
||||||
{
|
{
|
||||||
@@ -59,6 +90,11 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
|||||||
if (!_isModerator && !_isOwner) return;
|
if (!_isModerator && !_isOwner) return;
|
||||||
|
|
||||||
GroupFullInfo = _pairManager.Groups[GroupFullInfo.Group];
|
GroupFullInfo = _pairManager.Groups[GroupFullInfo.Group];
|
||||||
|
if (!_autoDetectToggleInFlight && !_autoDetectStateLoading)
|
||||||
|
{
|
||||||
|
_autoDetectVisible = GroupFullInfo.AutoDetectVisible;
|
||||||
|
_autoDetectPasswordDisabled = GroupFullInfo.PasswordTemporarilyDisabled;
|
||||||
|
}
|
||||||
|
|
||||||
using var id = ImRaii.PushId("syncshell_admin_" + GroupFullInfo.GID);
|
using var id = ImRaii.PushId("syncshell_admin_" + GroupFullInfo.GID);
|
||||||
|
|
||||||
@@ -68,6 +104,9 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
|||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
var perm = GroupFullInfo.GroupPermissions;
|
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);
|
using var tabbar = ImRaii.TabBar("syncshell_tab_" + GroupFullInfo.GID);
|
||||||
|
|
||||||
if (tabbar)
|
if (tabbar)
|
||||||
@@ -363,6 +402,13 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
mgmtTab.Dispose();
|
mgmtTab.Dispose();
|
||||||
|
|
||||||
|
var discoveryTab = ImRaii.TabItem("AutoDetect");
|
||||||
|
if (discoveryTab)
|
||||||
|
{
|
||||||
|
DrawAutoDetectTab();
|
||||||
|
}
|
||||||
|
discoveryTab.Dispose();
|
||||||
|
|
||||||
var permissionTab = ImRaii.TabItem("Permissions");
|
var permissionTab = ImRaii.TabItem("Permissions");
|
||||||
if (permissionTab)
|
if (permissionTab)
|
||||||
{
|
{
|
||||||
@@ -448,8 +494,283 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void DrawAutoDetectTab()
|
||||||
|
{
|
||||||
|
if (!_autoDetectStateInitialized && !_autoDetectStateLoading)
|
||||||
|
{
|
||||||
|
_autoDetectStateInitialized = true;
|
||||||
|
_autoDetectStateLoading = true;
|
||||||
|
_ = EnsureAutoDetectStateAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
UiSharedService.TextWrapped("Activer l'affichage AutoDetect rend la Syncshell visible dans l'onglet AutoDetect et désactive temporairement le mot de passe.");
|
||||||
|
ImGuiHelpers.ScaledDummy(4);
|
||||||
|
|
||||||
|
if (_autoDetectStateLoading)
|
||||||
|
{
|
||||||
|
ImGui.TextDisabled("Chargement de l'état en cours...");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(_autoDetectMessage))
|
||||||
|
{
|
||||||
|
UiSharedService.ColorTextWrapped(_autoDetectMessage!, ImGuiColors.DalamudYellow);
|
||||||
|
}
|
||||||
|
|
||||||
|
using (ImRaii.Disabled(_autoDetectToggleInFlight || _autoDetectStateLoading))
|
||||||
|
{
|
||||||
|
if (ImGui.Checkbox("Afficher cette Syncshell dans l'AutoDetect", ref _autoDetectDesiredVisibility))
|
||||||
|
{
|
||||||
|
// 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);
|
||||||
|
using (ImRaii.Disabled(_autoDetectToggleInFlight || _autoDetectStateLoading))
|
||||||
|
{
|
||||||
|
if (ImGui.Button("Valider et envoyer"))
|
||||||
|
{
|
||||||
|
_ = SubmitAutoDetectAsync();
|
||||||
|
}
|
||||||
|
ImGui.SameLine();
|
||||||
|
if (ImGui.Button("Recharger l'état"))
|
||||||
|
{
|
||||||
|
_autoDetectStateLoading = true;
|
||||||
|
_ = EnsureAutoDetectStateAsync(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task EnsureAutoDetectStateAsync(bool force = false)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var state = await _syncshellDiscoveryService.GetStateAsync(GroupFullInfo.GID, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
if (state != null)
|
||||||
|
{
|
||||||
|
ApplyAutoDetectState(state.AutoDetectVisible, state.PasswordTemporarilyDisabled, true);
|
||||||
|
_autoDetectMessage = null;
|
||||||
|
}
|
||||||
|
else if (force)
|
||||||
|
{
|
||||||
|
_autoDetectMessage = "Impossible de récupérer l'état AutoDetect.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_autoDetectMessage = force ? $"Erreur lors du rafraîchissement : {ex.Message}" : _autoDetectMessage;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_autoDetectStateLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ToggleAutoDetectAsync(bool desiredVisibility)
|
||||||
|
{
|
||||||
|
if (_autoDetectToggleInFlight)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_autoDetectToggleInFlight = true;
|
||||||
|
_autoDetectMessage = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var success = await _syncshellDiscoveryService.SetVisibilityAsync(GroupFullInfo.GID, desiredVisibility, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
if (!success)
|
||||||
|
{
|
||||||
|
_autoDetectMessage = "Impossible de mettre à jour la visibilité AutoDetect.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await EnsureAutoDetectStateAsync(true).ConfigureAwait(false);
|
||||||
|
_autoDetectMessage = desiredVisibility
|
||||||
|
? "La Syncshell est désormais visible dans AutoDetect."
|
||||||
|
: "La Syncshell n'est plus visible dans AutoDetect.";
|
||||||
|
|
||||||
|
if (desiredVisibility)
|
||||||
|
{
|
||||||
|
PublishSyncshellPublicNotification();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_autoDetectMessage = $"Erreur lors de la mise à jour AutoDetect : {ex.Message}";
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_autoDetectToggleInFlight = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
_autoDetectPasswordDisabled = passwordDisabled;
|
||||||
|
if (fromServer)
|
||||||
|
{
|
||||||
|
GroupFullInfo.AutoDetectVisible = visible;
|
||||||
|
GroupFullInfo.PasswordTemporarilyDisabled = passwordDisabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSyncshellAutoDetectStateChanged(SyncshellAutoDetectStateChanged msg)
|
||||||
|
{
|
||||||
|
if (!string.Equals(msg.Gid, GroupFullInfo.GID, StringComparison.OrdinalIgnoreCase)) return;
|
||||||
|
ApplyAutoDetectState(msg.Visible, msg.PasswordTemporarilyDisabled, true);
|
||||||
|
_autoDetectMessage = null;
|
||||||
|
}
|
||||||
|
|
||||||
public override void OnClose()
|
public override void OnClose()
|
||||||
{
|
{
|
||||||
Mediator.Publish(new RemoveWindowMessage(this));
|
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 Dalamud.Plugin.Services;
|
||||||
using MareSynchronos.API.Data;
|
using MareSynchronos.API.Data;
|
||||||
using MareSynchronos.API.Data.Extensions;
|
using MareSynchronos.API.Data.Extensions;
|
||||||
|
using MareSynchronos.API.Data.Enum;
|
||||||
using MareSynchronos.MareConfiguration;
|
using MareSynchronos.MareConfiguration;
|
||||||
using MareSynchronos.MareConfiguration.Models;
|
using MareSynchronos.MareConfiguration.Models;
|
||||||
using MareSynchronos.PlayerData.Pairs;
|
using MareSynchronos.PlayerData.Pairs;
|
||||||
@@ -75,6 +76,10 @@ public sealed class TypingIndicatorOverlay : WindowMediatorSubscriberBase
|
|||||||
if (!_clientState.IsLoggedIn)
|
if (!_clientState.IsLoggedIn)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
var typingEnabled = _configService.Current.TypingIndicatorEnabled;
|
||||||
|
if (!typingEnabled)
|
||||||
|
return;
|
||||||
|
|
||||||
var showParty = _configService.Current.TypingIndicatorShowOnPartyList;
|
var showParty = _configService.Current.TypingIndicatorShowOnPartyList;
|
||||||
var showNameplates = _configService.Current.TypingIndicatorShowOnNameplates;
|
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)
|
bool selfActive, DateTime now, DateTime selfStart, DateTime selfLast)
|
||||||
{
|
{
|
||||||
var partyAddon = (AtkUnitBase*)_gameGui.GetAddonByName("_PartyList", 1).Address;
|
var partyAddon = (AtkUnitBase*)_gameGui.GetAddonByName("_PartyList", 1).Address;
|
||||||
if (partyAddon == null || !partyAddon->IsVisible)
|
if (partyAddon == null || !partyAddon->IsVisible)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
var showSelf = _configService.Current.TypingIndicatorShowSelf;
|
||||||
if (selfActive
|
if (selfActive
|
||||||
|
&& showSelf
|
||||||
&& (now - selfStart) >= TypingDisplayDelay
|
&& (now - selfStart) >= TypingDisplayDelay
|
||||||
&& (now - selfLast) <= TypingDisplayFade)
|
&& (now - selfLast) <= TypingDisplayFade)
|
||||||
{
|
{
|
||||||
@@ -180,14 +187,16 @@ public sealed class TypingIndicatorOverlay : WindowMediatorSubscriberBase
|
|||||||
ImGui.ColorConvertFloat4ToU32(new Vector4(1f, 1f, 1f, 0.9f)));
|
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)
|
bool selfActive, DateTime now, DateTime selfStart, DateTime selfLast)
|
||||||
{
|
{
|
||||||
var iconWrap = _textureProvider.GetFromGameIcon(NameplateIconId).GetWrapOrEmpty();
|
var iconWrap = _textureProvider.GetFromGameIcon(NameplateIconId).GetWrapOrEmpty();
|
||||||
if (iconWrap == null || iconWrap.Handle == IntPtr.Zero)
|
if (iconWrap == null || iconWrap.Handle == IntPtr.Zero)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
var showSelf = _configService.Current.TypingIndicatorShowSelf;
|
||||||
if (selfActive
|
if (selfActive
|
||||||
|
&& showSelf
|
||||||
&& _clientState.LocalPlayer != null
|
&& _clientState.LocalPlayer != null
|
||||||
&& (now - selfStart) >= TypingDisplayDelay
|
&& (now - selfStart) >= TypingDisplayDelay
|
||||||
&& (now - selfLast) <= TypingDisplayFade)
|
&& (now - selfLast) <= TypingDisplayFade)
|
||||||
@@ -212,11 +221,22 @@ public sealed class TypingIndicatorOverlay : WindowMediatorSubscriberBase
|
|||||||
var pairName = pair?.PlayerName ?? entry.User.AliasOrUID ?? string.Empty;
|
var pairName = pair?.PlayerName ?? entry.User.AliasOrUID ?? string.Empty;
|
||||||
var pairIdent = pair?.Ident ?? string.Empty;
|
var pairIdent = pair?.Ident ?? string.Empty;
|
||||||
var isPartyMember = IsPartyMember(objectId, pairName);
|
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);
|
var isRelevantMember = IsPlayerRelevant(pair, isPartyMember);
|
||||||
|
|
||||||
if (objectId != uint.MaxValue && objectId != 0 && TryDrawNameplateBubble(drawList, iconWrap, objectId))
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,13 +246,20 @@ public sealed class TypingIndicatorOverlay : WindowMediatorSubscriberBase
|
|||||||
if (!isRelevantMember && !isNearby)
|
if (!isRelevantMember && !isNearby)
|
||||||
continue;
|
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)
|
if (pair == null)
|
||||||
{
|
{
|
||||||
_typedLogger.LogTrace("TypingIndicator: no pair found for {uid}, attempting fallback", uid);
|
_typedLogger.LogTrace("TypingIndicator: no pair found for {uid}, attempting fallback", uid);
|
||||||
}
|
}
|
||||||
|
|
||||||
_typedLogger.LogTrace("TypingIndicator: fallback draw for {uid} (objectId={objectId}, name={name}, ident={ident})",
|
_typedLogger.LogTrace("TypingIndicator: fallback draw for {uid} (objectId={objectId}, name={name}, ident={ident}, scope={scope})",
|
||||||
uid, objectId, pairName, pairIdent);
|
uid, objectId, pairName, pairIdent, entry.Scope);
|
||||||
|
|
||||||
if (hasWorldPosition)
|
if (hasWorldPosition)
|
||||||
{
|
{
|
||||||
@@ -318,26 +345,6 @@ public sealed class TypingIndicatorOverlay : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
if (!iconVisible)
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -124,7 +124,11 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
|||||||
e.OnPreBuild(tk => tk.AddDalamudAssetFont(Dalamud.DalamudAsset.NotoSansJpMedium, new()
|
e.OnPreBuild(tk => tk.AddDalamudAssetFont(Dalamud.DalamudAsset.NotoSansJpMedium, new()
|
||||||
{
|
{
|
||||||
SizePx = 27,
|
SizePx = 27,
|
||||||
GlyphRanges = [0x20, 0x7E, 0]
|
GlyphRanges = [
|
||||||
|
0x0020, 0x007E,
|
||||||
|
0x00A0, 0x017F,
|
||||||
|
0
|
||||||
|
]
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
GameFont = _pluginInterface.UiBuilder.FontAtlas.NewGameFontHandle(new(GameFontFamilyAndSize.Axis12));
|
GameFont = _pluginInterface.UiBuilder.FontAtlas.NewGameFontHandle(new(GameFontFamilyAndSize.Axis12));
|
||||||
@@ -537,6 +541,100 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
return result;
|
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)
|
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);
|
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)
|
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)));
|
Logger.LogInformation("Pushing character data for {hash} to {charas}", character.DataHash.Value, string.Join(", ", visibleCharacters.Select(c => c.AliasOrUID)));
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using MareSynchronos.API.Dto.McdfShare;
|
||||||
|
using Microsoft.AspNetCore.SignalR.Client;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace MareSynchronos.WebAPI;
|
||||||
|
|
||||||
|
public sealed partial class ApiController
|
||||||
|
{
|
||||||
|
public async Task<List<McdfShareEntryDto>> McdfShareGetOwn()
|
||||||
|
{
|
||||||
|
if (!IsConnected) return [];
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await _mareHub!.InvokeAsync<List<McdfShareEntryDto>>(nameof(McdfShareGetOwn)).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(ex, "Error during {method}", nameof(McdfShareGetOwn));
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<McdfShareEntryDto>> McdfShareGetShared()
|
||||||
|
{
|
||||||
|
if (!IsConnected) return [];
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await _mareHub!.InvokeAsync<List<McdfShareEntryDto>>(nameof(McdfShareGetShared)).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(ex, "Error during {method}", nameof(McdfShareGetShared));
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task McdfShareUpload(McdfShareUploadRequestDto requestDto)
|
||||||
|
{
|
||||||
|
if (!IsConnected) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _mareHub!.InvokeAsync(nameof(McdfShareUpload), requestDto).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(ex, "Error during {method}", nameof(McdfShareUpload));
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<McdfSharePayloadDto?> McdfShareDownload(Guid shareId)
|
||||||
|
{
|
||||||
|
if (!IsConnected) return null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await _mareHub!.InvokeAsync<McdfSharePayloadDto?>(nameof(McdfShareDownload), shareId).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(ex, "Error during {method}", nameof(McdfShareDownload));
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> McdfShareDelete(Guid shareId)
|
||||||
|
{
|
||||||
|
if (!IsConnected) return false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await _mareHub!.InvokeAsync<bool>(nameof(McdfShareDelete), shareId).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(ex, "Error during {method}", nameof(McdfShareDelete));
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<McdfShareEntryDto?> McdfShareUpdate(McdfShareUpdateRequestDto requestDto)
|
||||||
|
{
|
||||||
|
if (!IsConnected) return null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await _mareHub!.InvokeAsync<McdfShareEntryDto?>(nameof(McdfShareUpdate), requestDto).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(ex, "Error during {method}", nameof(McdfShareUpdate));
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using MareSynchronos.API.Dto.Group;
|
||||||
|
using Microsoft.AspNetCore.SignalR.Client;
|
||||||
|
|
||||||
|
namespace MareSynchronos.WebAPI;
|
||||||
|
|
||||||
|
public partial class ApiController
|
||||||
|
{
|
||||||
|
public async Task<List<SyncshellDiscoveryEntryDto>> SyncshellDiscoveryList()
|
||||||
|
{
|
||||||
|
CheckConnection();
|
||||||
|
return await _mareHub!.InvokeAsync<List<SyncshellDiscoveryEntryDto>>(nameof(SyncshellDiscoveryList)).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<SyncshellDiscoveryStateDto?> SyncshellDiscoveryGetState(GroupDto group)
|
||||||
|
{
|
||||||
|
CheckConnection();
|
||||||
|
return await _mareHub!.InvokeAsync<SyncshellDiscoveryStateDto?>(nameof(SyncshellDiscoveryGetState), group).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> SyncshellDiscoverySetVisibility(SyncshellDiscoveryVisibilityRequestDto request)
|
||||||
|
{
|
||||||
|
CheckConnection();
|
||||||
|
return await _mareHub!.InvokeAsync<bool>(nameof(SyncshellDiscoverySetVisibility), request).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> SyncshellDiscoveryJoin(GroupDto group)
|
||||||
|
{
|
||||||
|
CheckConnection();
|
||||||
|
return await _mareHub!.InvokeAsync<bool>(nameof(SyncshellDiscoveryJoin), group).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -180,7 +180,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM
|
|||||||
InitializeApiHooks();
|
InitializeApiHooks();
|
||||||
|
|
||||||
await _mareHub.StartAsync(token).ConfigureAwait(false);
|
await _mareHub.StartAsync(token).ConfigureAwait(false);
|
||||||
|
|
||||||
_connectionDto = await GetConnectionDto().ConfigureAwait(false);
|
_connectionDto = await GetConnectionDto().ConfigureAwait(false);
|
||||||
|
|
||||||
ServerState = ServerState.Connected;
|
ServerState = ServerState.Connected;
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ using MareSynchronos.Services;
|
|||||||
using MareSynchronos.Services.Mediator;
|
using MareSynchronos.Services.Mediator;
|
||||||
using MareSynchronos.Services.ServerConfiguration;
|
using MareSynchronos.Services.ServerConfiguration;
|
||||||
using MareSynchronos.WebAPI.SignalR.Utils;
|
using MareSynchronos.WebAPI.SignalR.Utils;
|
||||||
using MessagePack;
|
|
||||||
using MessagePack.Resolvers;
|
|
||||||
using Microsoft.AspNetCore.Http.Connections;
|
using Microsoft.AspNetCore.Http.Connections;
|
||||||
using Microsoft.AspNetCore.SignalR.Client;
|
using Microsoft.AspNetCore.SignalR.Client;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
@@ -172,25 +170,7 @@ public class HubFactory : MediatorSubscriberBase
|
|||||||
options.SkipNegotiation = hubConfig.SkipNegotiation && (transports == HttpTransportType.WebSockets);
|
options.SkipNegotiation = hubConfig.SkipNegotiation && (transports == HttpTransportType.WebSockets);
|
||||||
options.Transports = transports;
|
options.Transports = transports;
|
||||||
})
|
})
|
||||||
.AddMessagePackProtocol(opt =>
|
.AddJsonProtocol()
|
||||||
{
|
|
||||||
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);
|
|
||||||
})
|
|
||||||
.WithAutomaticReconnect(new ForeverRetryPolicy(Mediator))
|
.WithAutomaticReconnect(new ForeverRetryPolicy(Mediator))
|
||||||
.ConfigureLogging(a =>
|
.ConfigureLogging(a =>
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user