Compare commits
13 Commits
main-legac
...
78089a9fc7
| Author | SHA1 | Date | |
|---|---|---|---|
|
78089a9fc7
|
|||
|
3c81e1f243
|
|||
|
0808266887
|
|||
|
a2071b9c05
|
|||
|
612e7c88a2
|
|||
|
1755b5cb54
|
|||
|
4a388dcfa9
|
|||
|
a0957715a5
|
|||
|
04a8ee3186
|
|||
|
b79a51748f
|
|||
|
95d9f65068
|
|||
|
a70968d30c
|
|||
|
6ebb73040b
|
3
.gitignore
vendored
3
.gitignore
vendored
@@ -10,6 +10,9 @@
|
|||||||
*.userosscache
|
*.userosscache
|
||||||
*.sln.docstates
|
*.sln.docstates
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
MareSynchronos/.DS_Store
|
||||||
|
*.zip
|
||||||
|
UmbraServer_extracted/
|
||||||
|
|
||||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||||
*.userprefs
|
*.userprefs
|
||||||
|
|||||||
2
.gitmodules
vendored
2
.gitmodules
vendored
@@ -1,6 +1,6 @@
|
|||||||
[submodule "MareAPI"]
|
[submodule "MareAPI"]
|
||||||
path = MareAPI
|
path = MareAPI
|
||||||
url = https://git.umbra-sync.net/SirConstance/UmbraAPI.git
|
url = ssh://git@git.umbra-sync.net:1222/Keda/UmbraAPI.git
|
||||||
branch = main
|
branch = main
|
||||||
[submodule "Penumbra.Api"]
|
[submodule "Penumbra.Api"]
|
||||||
path = Penumbra.Api
|
path = Penumbra.Api
|
||||||
|
|||||||
2
MareAPI
2
MareAPI
Submodule MareAPI updated: ff262bf690...fa9b7bce43
BIN
MareSynchronos/.DS_Store
vendored
BIN
MareSynchronos/.DS_Store
vendored
Binary file not shown.
@@ -1,15 +1,72 @@
|
|||||||
using MareSynchronos.WebAPI;
|
using MareSynchronos.WebAPI;
|
||||||
|
using System.Text.Json;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace MareSynchronos.MareConfiguration;
|
namespace MareSynchronos.MareConfiguration;
|
||||||
|
|
||||||
public class ConfigurationMigrator(ILogger<ConfigurationMigrator> logger) : IHostedService
|
public class ConfigurationMigrator(ILogger<ConfigurationMigrator> logger, MareConfigService mareConfig) : IHostedService
|
||||||
{
|
{
|
||||||
private readonly ILogger<ConfigurationMigrator> _logger = logger;
|
private readonly ILogger<ConfigurationMigrator> _logger = logger;
|
||||||
|
private readonly MareConfigService _mareConfig = mareConfig;
|
||||||
|
|
||||||
public void Migrate()
|
public void Migrate()
|
||||||
{
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var path = _mareConfig.ConfigurationPath;
|
||||||
|
if (!File.Exists(path)) return;
|
||||||
|
|
||||||
|
using var doc = JsonDocument.Parse(File.ReadAllText(path));
|
||||||
|
var root = doc.RootElement;
|
||||||
|
|
||||||
|
bool changed = false;
|
||||||
|
|
||||||
|
if (root.TryGetProperty("EnableAutoSyncDiscovery", out var enableAutoSync))
|
||||||
|
{
|
||||||
|
var val = enableAutoSync.GetBoolean();
|
||||||
|
if (_mareConfig.Current.EnableAutoDetectDiscovery != val)
|
||||||
|
{
|
||||||
|
_mareConfig.Current.EnableAutoDetectDiscovery = val;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (root.TryGetProperty("AllowAutoSyncPairRequests", out var allowAutoSync))
|
||||||
|
{
|
||||||
|
var val = allowAutoSync.GetBoolean();
|
||||||
|
if (_mareConfig.Current.AllowAutoDetectPairRequests != val)
|
||||||
|
{
|
||||||
|
_mareConfig.Current.AllowAutoDetectPairRequests = val;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (root.TryGetProperty("AutoSyncMaxDistanceMeters", out var maxDistSync) && maxDistSync.TryGetInt32(out var md))
|
||||||
|
{
|
||||||
|
if (_mareConfig.Current.AutoDetectMaxDistanceMeters != md)
|
||||||
|
{
|
||||||
|
_mareConfig.Current.AutoDetectMaxDistanceMeters = md;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (root.TryGetProperty("AutoSyncMuteMinutes", out var muteSync) && muteSync.TryGetInt32(out var mm))
|
||||||
|
{
|
||||||
|
if (_mareConfig.Current.AutoDetectMuteMinutes != mm)
|
||||||
|
{
|
||||||
|
_mareConfig.Current.AutoDetectMuteMinutes = mm;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Migrated config: AutoSync -> AutoDetect fields");
|
||||||
|
_mareConfig.Save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Configuration migration failed");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task StartAsync(CancellationToken cancellationToken)
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
|||||||
@@ -59,6 +59,10 @@ public class MareConfig : IMareConfiguration
|
|||||||
public bool ShowUploading { get; set; } = true;
|
public bool ShowUploading { get; set; } = true;
|
||||||
public bool ShowUploadingBigText { get; set; } = true;
|
public bool ShowUploadingBigText { get; set; } = true;
|
||||||
public bool ShowVisibleUsersSeparately { get; set; } = true;
|
public bool ShowVisibleUsersSeparately { get; set; } = true;
|
||||||
|
public bool EnableAutoDetectDiscovery { get; set; } = false;
|
||||||
|
public bool AllowAutoDetectPairRequests { get; set; } = false;
|
||||||
|
public int AutoDetectMaxDistanceMeters { get; set; } = 40;
|
||||||
|
public int AutoDetectMuteMinutes { get; set; } = 5;
|
||||||
public int TimeSpanBetweenScansInSeconds { get; set; } = 30;
|
public int TimeSpanBetweenScansInSeconds { get; set; } = 30;
|
||||||
public int TransferBarsHeight { get; set; } = 12;
|
public int TransferBarsHeight { get; set; } = 12;
|
||||||
public bool TransferBarsShowText { get; set; } = true;
|
public bool TransferBarsShowText { get; set; } = true;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<AssemblyName>UmbraSync</AssemblyName>
|
<AssemblyName>UmbraSync</AssemblyName>
|
||||||
<RootNamespace>UmbraSync</RootNamespace>
|
<RootNamespace>UmbraSync</RootNamespace>
|
||||||
<Version>0.1.1.0</Version>
|
<Version>0.1.8.0</Version>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -210,9 +210,16 @@ public sealed class PairManager : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
public void SetGroupInfo(GroupInfoDto dto)
|
public void SetGroupInfo(GroupInfoDto dto)
|
||||||
{
|
{
|
||||||
_allGroups[dto.Group].Group = dto.Group;
|
if (!_allGroups.TryGetValue(dto.Group, out var groupInfo))
|
||||||
_allGroups[dto.Group].Owner = dto.Owner;
|
{
|
||||||
_allGroups[dto.Group].GroupPermissions = dto.GroupPermissions;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
groupInfo.Group = dto.Group;
|
||||||
|
groupInfo.Owner = dto.Owner;
|
||||||
|
groupInfo.GroupPermissions = dto.GroupPermissions;
|
||||||
|
groupInfo.IsTemporary = dto.IsTemporary;
|
||||||
|
groupInfo.ExpiresAt = dto.ExpiresAt;
|
||||||
|
|
||||||
RecreateLazy();
|
RecreateLazy();
|
||||||
}
|
}
|
||||||
@@ -400,4 +407,4 @@ public sealed class PairManager : DisposableMediatorSubscriberBase
|
|||||||
_directPairsInternal = DirectPairsLazy();
|
_directPairsInternal = DirectPairsLazy();
|
||||||
_groupPairsInternal = GroupPairsLazy();
|
_groupPairsInternal = GroupPairsLazy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,6 +96,11 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
collection.AddSingleton<HubFactory>();
|
collection.AddSingleton<HubFactory>();
|
||||||
collection.AddSingleton<FileUploadManager>();
|
collection.AddSingleton<FileUploadManager>();
|
||||||
collection.AddSingleton<FileTransferOrchestrator>();
|
collection.AddSingleton<FileTransferOrchestrator>();
|
||||||
|
collection.AddSingleton<MareSynchronos.Services.AutoDetect.DiscoveryConfigProvider>();
|
||||||
|
collection.AddSingleton<MareSynchronos.WebAPI.AutoDetect.DiscoveryApiClient>();
|
||||||
|
collection.AddSingleton<MareSynchronos.Services.AutoDetect.AutoDetectRequestService>();
|
||||||
|
collection.AddSingleton<MareSynchronos.Services.AutoDetect.NearbyDiscoveryService>();
|
||||||
|
collection.AddSingleton<MareSynchronos.Services.AutoDetect.NearbyPendingService>();
|
||||||
collection.AddSingleton<MarePlugin>();
|
collection.AddSingleton<MarePlugin>();
|
||||||
collection.AddSingleton<MareProfileManager>();
|
collection.AddSingleton<MareProfileManager>();
|
||||||
collection.AddSingleton<GameObjectHandlerFactory>();
|
collection.AddSingleton<GameObjectHandlerFactory>();
|
||||||
@@ -140,6 +145,7 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
collection.AddSingleton<IpcCallerMare>();
|
collection.AddSingleton<IpcCallerMare>();
|
||||||
collection.AddSingleton<IpcManager>();
|
collection.AddSingleton<IpcManager>();
|
||||||
collection.AddSingleton<NotificationService>();
|
collection.AddSingleton<NotificationService>();
|
||||||
|
collection.AddSingleton<TemporarySyncshellNotificationService>();
|
||||||
|
|
||||||
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));
|
||||||
@@ -175,6 +181,7 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
collection.AddScoped<WindowMediatorSubscriberBase, CompactUi>();
|
collection.AddScoped<WindowMediatorSubscriberBase, CompactUi>();
|
||||||
collection.AddScoped<WindowMediatorSubscriberBase, IntroUi>();
|
collection.AddScoped<WindowMediatorSubscriberBase, IntroUi>();
|
||||||
collection.AddScoped<WindowMediatorSubscriberBase, DownloadUi>();
|
collection.AddScoped<WindowMediatorSubscriberBase, DownloadUi>();
|
||||||
|
collection.AddScoped<WindowMediatorSubscriberBase, AutoDetectUi>();
|
||||||
collection.AddScoped<WindowMediatorSubscriberBase, PopoutProfileUi>();
|
collection.AddScoped<WindowMediatorSubscriberBase, PopoutProfileUi>();
|
||||||
collection.AddScoped<WindowMediatorSubscriberBase, DataAnalysisUi>();
|
collection.AddScoped<WindowMediatorSubscriberBase, DataAnalysisUi>();
|
||||||
collection.AddScoped<WindowMediatorSubscriberBase, EventViewerUI>();
|
collection.AddScoped<WindowMediatorSubscriberBase, EventViewerUI>();
|
||||||
@@ -197,6 +204,7 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
collection.AddHostedService(p => p.GetRequiredService<ConfigurationSaveService>());
|
collection.AddHostedService(p => p.GetRequiredService<ConfigurationSaveService>());
|
||||||
collection.AddHostedService(p => p.GetRequiredService<MareMediator>());
|
collection.AddHostedService(p => p.GetRequiredService<MareMediator>());
|
||||||
collection.AddHostedService(p => p.GetRequiredService<NotificationService>());
|
collection.AddHostedService(p => p.GetRequiredService<NotificationService>());
|
||||||
|
collection.AddHostedService(p => p.GetRequiredService<TemporarySyncshellNotificationService>());
|
||||||
collection.AddHostedService(p => p.GetRequiredService<FileCacheManager>());
|
collection.AddHostedService(p => p.GetRequiredService<FileCacheManager>());
|
||||||
collection.AddHostedService(p => p.GetRequiredService<ConfigurationMigrator>());
|
collection.AddHostedService(p => p.GetRequiredService<ConfigurationMigrator>());
|
||||||
collection.AddHostedService(p => p.GetRequiredService<DalamudUtilService>());
|
collection.AddHostedService(p => p.GetRequiredService<DalamudUtilService>());
|
||||||
@@ -205,6 +213,7 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
collection.AddHostedService(p => p.GetRequiredService<EventAggregator>());
|
collection.AddHostedService(p => p.GetRequiredService<EventAggregator>());
|
||||||
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>());
|
||||||
})
|
})
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
@@ -225,4 +234,4 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
_host.StopAsync().GetAwaiter().GetResult();
|
_host.StopAsync().GetAwaiter().GetResult();
|
||||||
_host.Dispose();
|
_host.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using MareSynchronos.WebAPI.AutoDetect;
|
||||||
|
using MareSynchronos.MareConfiguration;
|
||||||
|
using MareSynchronos.Services;
|
||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using NotificationType = MareSynchronos.MareConfiguration.Models.NotificationType;
|
||||||
|
|
||||||
|
namespace MareSynchronos.Services.AutoDetect;
|
||||||
|
|
||||||
|
public class AutoDetectRequestService
|
||||||
|
{
|
||||||
|
private readonly ILogger<AutoDetectRequestService> _logger;
|
||||||
|
private readonly DiscoveryConfigProvider _configProvider;
|
||||||
|
private readonly DiscoveryApiClient _client;
|
||||||
|
private readonly MareConfigService _configService;
|
||||||
|
private readonly DalamudUtilService _dalamud;
|
||||||
|
private readonly MareMediator _mediator;
|
||||||
|
|
||||||
|
public AutoDetectRequestService(ILogger<AutoDetectRequestService> logger, DiscoveryConfigProvider configProvider, DiscoveryApiClient client, MareConfigService configService, MareMediator mediator, DalamudUtilService dalamudUtilService)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_configProvider = configProvider;
|
||||||
|
_client = client;
|
||||||
|
_configService = configService;
|
||||||
|
_mediator = mediator;
|
||||||
|
_dalamud = dalamudUtilService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> SendRequestAsync(string token, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
if (!_configService.Current.AllowAutoDetectPairRequests)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Nearby request blocked: AllowAutoDetectPairRequests is disabled");
|
||||||
|
_mediator.Publish(new NotificationMessage("Nearby request blocked", "Enable 'Allow pair requests' in Settings to send requests.", NotificationType.Info));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var endpoint = _configProvider.RequestEndpoint;
|
||||||
|
if (string.IsNullOrEmpty(endpoint))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("No request endpoint configured");
|
||||||
|
_mediator.Publish(new NotificationMessage("Nearby request failed", "Server does not expose request endpoint.", NotificationType.Error));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
string? displayName = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var me = await _dalamud.RunOnFrameworkThread(() => _dalamud.GetPlayerCharacter()).ConfigureAwait(false);
|
||||||
|
displayName = me?.Name.TextValue;
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
|
||||||
|
_logger.LogInformation("Nearby: sending pair request via {endpoint}", endpoint);
|
||||||
|
var ok = await _client.SendRequestAsync(endpoint!, token, displayName, ct).ConfigureAwait(false);
|
||||||
|
if (ok)
|
||||||
|
{
|
||||||
|
_mediator.Publish(new NotificationMessage("Nearby request sent", "The other user will receive a request notification.", NotificationType.Info));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_mediator.Publish(new NotificationMessage("Nearby request failed", "The server rejected the request. Try again soon.", NotificationType.Warning));
|
||||||
|
}
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> SendAcceptNotifyAsync(string targetUid, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var endpoint = _configProvider.AcceptEndpoint;
|
||||||
|
if (string.IsNullOrEmpty(endpoint))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("No accept endpoint configured");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
string? displayName = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var me = await _dalamud.RunOnFrameworkThread(() => _dalamud.GetPlayerCharacter()).ConfigureAwait(false);
|
||||||
|
displayName = me?.Name.TextValue;
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
_logger.LogInformation("Nearby: sending accept notify via {endpoint}", endpoint);
|
||||||
|
return await _client.SendAcceptAsync(endpoint!, targetUid, displayName, ct).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
173
MareSynchronos/Services/AutoDetect/DiscoveryConfigProvider.cs
Normal file
173
MareSynchronos/Services/AutoDetect/DiscoveryConfigProvider.cs
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using MareSynchronos.Services.ServerConfiguration;
|
||||||
|
using MareSynchronos.WebAPI.SignalR;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace MareSynchronos.Services.AutoDetect;
|
||||||
|
|
||||||
|
public class DiscoveryConfigProvider
|
||||||
|
{
|
||||||
|
private readonly ILogger<DiscoveryConfigProvider> _logger;
|
||||||
|
private readonly ServerConfigurationManager _serverManager;
|
||||||
|
private readonly TokenProvider _tokenProvider;
|
||||||
|
|
||||||
|
private WellKnownRoot? _config;
|
||||||
|
private DateTimeOffset _lastLoad = DateTimeOffset.MinValue;
|
||||||
|
|
||||||
|
public DiscoveryConfigProvider(ILogger<DiscoveryConfigProvider> logger, ServerConfigurationManager serverManager, TokenProvider tokenProvider)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_serverManager = serverManager;
|
||||||
|
_tokenProvider = tokenProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HasConfig => _config != null;
|
||||||
|
public bool NearbyEnabled => _config?.NearbyDiscovery?.Enabled ?? false;
|
||||||
|
public byte[]? Salt => _config?.NearbyDiscovery?.SaltBytes;
|
||||||
|
public string? SaltB64 => _config?.NearbyDiscovery?.SaltB64;
|
||||||
|
public DateTimeOffset? SaltExpiresAt => _config?.NearbyDiscovery?.SaltExpiresAt;
|
||||||
|
public int RefreshSec => _config?.NearbyDiscovery?.RefreshSec ?? 300;
|
||||||
|
public int MinQueryIntervalMs => _config?.NearbyDiscovery?.Policies?.MinQueryIntervalMs ?? 2000;
|
||||||
|
public int MaxQueryBatch => _config?.NearbyDiscovery?.Policies?.MaxQueryBatch ?? 100;
|
||||||
|
public string? PublishEndpoint => _config?.NearbyDiscovery?.Endpoints?.Publish;
|
||||||
|
public string? QueryEndpoint => _config?.NearbyDiscovery?.Endpoints?.Query;
|
||||||
|
public string? RequestEndpoint => _config?.NearbyDiscovery?.Endpoints?.Request;
|
||||||
|
public string? AcceptEndpoint => _config?.NearbyDiscovery?.Endpoints?.Accept;
|
||||||
|
|
||||||
|
public bool TryLoadFromStapled()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var json = _tokenProvider.GetStapledWellKnown(_serverManager.CurrentApiUrl);
|
||||||
|
if (string.IsNullOrEmpty(json)) return false;
|
||||||
|
|
||||||
|
var root = JsonSerializer.Deserialize<WellKnownRoot>(json!);
|
||||||
|
if (root == null) return false;
|
||||||
|
|
||||||
|
root.NearbyDiscovery?.Hydrate();
|
||||||
|
_config = root;
|
||||||
|
_lastLoad = DateTimeOffset.UtcNow;
|
||||||
|
_logger.LogDebug("Loaded Nearby well-known (stapled), enabled={enabled}, expires={exp}", NearbyEnabled, _config?.NearbyDiscovery?.SaltExpiresAt);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to parse stapled well-known");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> TryFetchFromServerAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var baseUrl = _serverManager.CurrentApiUrl
|
||||||
|
.Replace("wss://", "https://", StringComparison.OrdinalIgnoreCase)
|
||||||
|
.Replace("ws://", "http://", StringComparison.OrdinalIgnoreCase);
|
||||||
|
// Try likely candidates based on nginx config
|
||||||
|
string[] candidates =
|
||||||
|
[
|
||||||
|
"/.well-known/Umbra/client", // matches provided nginx
|
||||||
|
"/.well-known/umbra", // lowercase variant
|
||||||
|
];
|
||||||
|
|
||||||
|
using var http = new HttpClient();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var ver = Assembly.GetExecutingAssembly().GetName().Version!;
|
||||||
|
http.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("MareSynchronos", $"{ver.Major}.{ver.Minor}.{ver.Build}"));
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
|
||||||
|
foreach (var path in candidates)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var uri = new Uri(new Uri(baseUrl), path);
|
||||||
|
var json = await http.GetStringAsync(uri, ct).ConfigureAwait(false);
|
||||||
|
if (string.IsNullOrEmpty(json)) continue;
|
||||||
|
|
||||||
|
var root = JsonSerializer.Deserialize<WellKnownRoot>(json);
|
||||||
|
if (root == null) continue;
|
||||||
|
|
||||||
|
root.NearbyDiscovery?.Hydrate();
|
||||||
|
_config = root;
|
||||||
|
_lastLoad = DateTimeOffset.UtcNow;
|
||||||
|
_logger.LogInformation("Loaded Nearby well-known (http {path}), enabled={enabled}", path, NearbyEnabled);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogDebug(ex, "Nearby well-known fetch failed for {path}", path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Nearby well-known not found via HTTP candidates");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to fetch Nearby well-known via HTTP");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsExpired()
|
||||||
|
{
|
||||||
|
if (_config?.NearbyDiscovery?.SaltExpiresAt == null) return false;
|
||||||
|
return DateTimeOffset.UtcNow > _config.NearbyDiscovery.SaltExpiresAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// DTOs for well-known JSON
|
||||||
|
private sealed class WellKnownRoot
|
||||||
|
{
|
||||||
|
[JsonPropertyName("features")] public Features? Features { get; set; }
|
||||||
|
[JsonPropertyName("nearby_discovery")] public Nearby? NearbyDiscovery { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class Features
|
||||||
|
{
|
||||||
|
[JsonPropertyName("nearby_discovery")] public bool NearbyDiscovery { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class Nearby
|
||||||
|
{
|
||||||
|
[JsonPropertyName("enabled")] public bool Enabled { get; set; }
|
||||||
|
[JsonPropertyName("hash_algo")] public string? HashAlgo { get; set; }
|
||||||
|
[JsonPropertyName("salt_b64")] public string? SaltB64 { get; set; }
|
||||||
|
[JsonPropertyName("salt_expires_at")] public string? SaltExpiresAtRaw { get; set; }
|
||||||
|
[JsonPropertyName("refresh_sec")] public int RefreshSec { get; set; } = 300;
|
||||||
|
[JsonPropertyName("endpoints")] public Endpoints? Endpoints { get; set; }
|
||||||
|
[JsonPropertyName("policies")] public Policies? Policies { get; set; }
|
||||||
|
|
||||||
|
[JsonIgnore] public byte[]? SaltBytes { get; private set; }
|
||||||
|
[JsonIgnore] public DateTimeOffset? SaltExpiresAt { get; private set; }
|
||||||
|
|
||||||
|
public void Hydrate()
|
||||||
|
{
|
||||||
|
try { SaltBytes = string.IsNullOrEmpty(SaltB64) ? null : Convert.FromBase64String(SaltB64!); } catch { SaltBytes = null; }
|
||||||
|
if (DateTimeOffset.TryParse(SaltExpiresAtRaw, out var dto)) SaltExpiresAt = dto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class Endpoints
|
||||||
|
{
|
||||||
|
[JsonPropertyName("publish")] public string? Publish { get; set; }
|
||||||
|
[JsonPropertyName("query")] public string? Query { get; set; }
|
||||||
|
[JsonPropertyName("request")] public string? Request { get; set; }
|
||||||
|
[JsonPropertyName("accept")] public string? Accept { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class Policies
|
||||||
|
{
|
||||||
|
[JsonPropertyName("max_query_batch")] public int MaxQueryBatch { get; set; } = 100;
|
||||||
|
[JsonPropertyName("min_query_interval_ms")] public int MinQueryIntervalMs { get; set; } = 2000;
|
||||||
|
[JsonPropertyName("rate_limit_per_min")] public int RateLimitPerMin { get; set; } = 30;
|
||||||
|
[JsonPropertyName("token_ttl_sec")] public int TokenTtlSec { get; set; } = 120;
|
||||||
|
}
|
||||||
|
}
|
||||||
477
MareSynchronos/Services/AutoDetect/NearbyDiscoveryService.cs
Normal file
477
MareSynchronos/Services/AutoDetect/NearbyDiscoveryService.cs
Normal file
@@ -0,0 +1,477 @@
|
|||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using MareSynchronos.MareConfiguration;
|
||||||
|
using MareSynchronos.Services.ServerConfiguration;
|
||||||
|
using MareSynchronos.WebAPI.AutoDetect;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using System.Numerics;
|
||||||
|
using System.Linq;
|
||||||
|
using MareSynchronos.Utils;
|
||||||
|
|
||||||
|
namespace MareSynchronos.Services.AutoDetect;
|
||||||
|
|
||||||
|
public class NearbyDiscoveryService : IHostedService, IMediatorSubscriber
|
||||||
|
{
|
||||||
|
private readonly ILogger<NearbyDiscoveryService> _logger;
|
||||||
|
private readonly MareMediator _mediator;
|
||||||
|
private readonly MareConfigService _config;
|
||||||
|
private readonly DiscoveryConfigProvider _configProvider;
|
||||||
|
private readonly DalamudUtilService _dalamud;
|
||||||
|
private readonly IObjectTable _objectTable;
|
||||||
|
private readonly DiscoveryApiClient _api;
|
||||||
|
private CancellationTokenSource? _loopCts;
|
||||||
|
private string? _lastPublishedSignature;
|
||||||
|
private bool _loggedLocalOnly;
|
||||||
|
private int _lastLocalCount = -1;
|
||||||
|
private int _lastMatchCount = -1;
|
||||||
|
private bool _loggedConfigReady;
|
||||||
|
private string? _lastSnapshotSig;
|
||||||
|
private volatile bool _isConnected;
|
||||||
|
private bool _notifiedDisabled;
|
||||||
|
private bool _notifiedEnabled;
|
||||||
|
private bool _disableSent;
|
||||||
|
private bool _lastAutoDetectState;
|
||||||
|
private DateTime _lastHeartbeat = DateTime.MinValue;
|
||||||
|
private static readonly TimeSpan HeartbeatInterval = TimeSpan.FromSeconds(75);
|
||||||
|
|
||||||
|
public NearbyDiscoveryService(ILogger<NearbyDiscoveryService> logger, MareMediator mediator,
|
||||||
|
MareConfigService config, DiscoveryConfigProvider configProvider, DalamudUtilService dalamudUtilService,
|
||||||
|
IObjectTable objectTable, DiscoveryApiClient api)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_mediator = mediator;
|
||||||
|
_config = config;
|
||||||
|
_configProvider = configProvider;
|
||||||
|
_dalamud = dalamudUtilService;
|
||||||
|
_objectTable = objectTable;
|
||||||
|
_api = api;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MareMediator Mediator => _mediator;
|
||||||
|
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_loopCts = new CancellationTokenSource();
|
||||||
|
_mediator.Subscribe<ConnectedMessage>(this, _ => { _isConnected = true; _configProvider.TryLoadFromStapled(); });
|
||||||
|
_mediator.Subscribe<DisconnectedMessage>(this, _ => { _isConnected = false; _lastPublishedSignature = null; });
|
||||||
|
_mediator.Subscribe<AllowPairRequestsToggled>(this, OnAllowPairRequestsToggled);
|
||||||
|
_ = Task.Run(() => Loop(_loopCts.Token));
|
||||||
|
_lastAutoDetectState = _config.Current.EnableAutoDetectDiscovery;
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
private async void OnAllowPairRequestsToggled(AllowPairRequestsToggled msg)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!_config.Current.EnableAutoDetectDiscovery) return;
|
||||||
|
// Force a publish now so the server immediately reflects the new allow/deny state
|
||||||
|
_lastPublishedSignature = null; // ensure next loop won't skip
|
||||||
|
await PublishSelfOnceAsync(CancellationToken.None).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogDebug(ex, "OnAllowPairRequestsToggled failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task PublishSelfOnceAsync(CancellationToken ct)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!_config.Current.EnableAutoDetectDiscovery || !_dalamud.IsLoggedIn || !_isConnected) return;
|
||||||
|
|
||||||
|
if (!_configProvider.HasConfig || _configProvider.IsExpired())
|
||||||
|
{
|
||||||
|
if (!_configProvider.TryLoadFromStapled())
|
||||||
|
{
|
||||||
|
await _configProvider.TryFetchFromServerAsync(ct).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var ep = _configProvider.PublishEndpoint;
|
||||||
|
var saltBytes = _configProvider.Salt;
|
||||||
|
if (string.IsNullOrEmpty(ep) || saltBytes is not { Length: > 0 }) return;
|
||||||
|
|
||||||
|
var saltHex = Convert.ToHexString(saltBytes);
|
||||||
|
string? displayName = null;
|
||||||
|
ushort meWorld = 0;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var me = await _dalamud.RunOnFrameworkThread(() => _dalamud.GetPlayerCharacter()).ConfigureAwait(false);
|
||||||
|
if (me != null)
|
||||||
|
{
|
||||||
|
displayName = me.Name.TextValue;
|
||||||
|
if (me is Dalamud.Game.ClientState.Objects.SubKinds.IPlayerCharacter mePc)
|
||||||
|
meWorld = (ushort)mePc.HomeWorld.RowId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(displayName)) return;
|
||||||
|
|
||||||
|
var selfHash = (saltHex + displayName + meWorld.ToString()).GetHash256();
|
||||||
|
var ok = await _api.PublishAsync(ep!, new[] { selfHash }, displayName, ct, _config.Current.AllowAutoDetectPairRequests).ConfigureAwait(false);
|
||||||
|
_logger.LogInformation("Nearby publish (manual/immediate): {result}", ok ? "success" : "failed");
|
||||||
|
if (ok)
|
||||||
|
{
|
||||||
|
_lastPublishedSignature = selfHash;
|
||||||
|
_lastHeartbeat = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogDebug(ex, "Immediate publish failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StopAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_mediator.UnsubscribeAll(this);
|
||||||
|
try { _loopCts?.Cancel(); } catch { }
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task Loop(CancellationToken ct)
|
||||||
|
{
|
||||||
|
_configProvider.TryLoadFromStapled();
|
||||||
|
|
||||||
|
while (!ct.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
bool currentState = _config.Current.EnableAutoDetectDiscovery;
|
||||||
|
if (currentState != _lastAutoDetectState)
|
||||||
|
{
|
||||||
|
_lastAutoDetectState = currentState;
|
||||||
|
if (currentState)
|
||||||
|
{
|
||||||
|
// Force immediate publish on toggle ON
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Ensure well-known is present
|
||||||
|
if (!_configProvider.HasConfig || _configProvider.IsExpired())
|
||||||
|
{
|
||||||
|
if (!_configProvider.TryLoadFromStapled())
|
||||||
|
{
|
||||||
|
await _configProvider.TryFetchFromServerAsync(ct).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var ep = _configProvider.PublishEndpoint;
|
||||||
|
var saltBytes = _configProvider.Salt;
|
||||||
|
if (!string.IsNullOrEmpty(ep) && saltBytes is { Length: > 0 })
|
||||||
|
{
|
||||||
|
var saltHex = Convert.ToHexString(saltBytes);
|
||||||
|
string? displayName = null;
|
||||||
|
ushort meWorld = 0;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var me = await _dalamud.RunOnFrameworkThread(() => _dalamud.GetPlayerCharacter()).ConfigureAwait(false);
|
||||||
|
if (me != null)
|
||||||
|
{
|
||||||
|
displayName = me.Name.TextValue;
|
||||||
|
if (me is Dalamud.Game.ClientState.Objects.SubKinds.IPlayerCharacter mePc)
|
||||||
|
meWorld = (ushort)mePc.HomeWorld.RowId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(displayName))
|
||||||
|
{
|
||||||
|
var selfHash = (saltHex + displayName + meWorld.ToString()).GetHash256();
|
||||||
|
_lastPublishedSignature = null; // ensure future loop doesn't skip
|
||||||
|
var okNow = await _api.PublishAsync(ep!, new[] { selfHash }, displayName, ct, _config.Current.AllowAutoDetectPairRequests).ConfigureAwait(false);
|
||||||
|
_logger.LogInformation("Nearby immediate publish on toggle ON: {result}", okNow ? "success" : "failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogDebug(ex, "Nearby immediate publish on toggle ON failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_notifiedEnabled)
|
||||||
|
{
|
||||||
|
_mediator.Publish(new NotificationMessage("Nearby Detection", "AutoDetect enabled : you are now visible.", default));
|
||||||
|
_notifiedEnabled = true;
|
||||||
|
_notifiedDisabled = false;
|
||||||
|
_disableSent = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var ep = _configProvider.PublishEndpoint;
|
||||||
|
if (!string.IsNullOrEmpty(ep) && !_disableSent)
|
||||||
|
{
|
||||||
|
var disableUrl = ep.Replace("/publish", "/disable");
|
||||||
|
try { await _api.DisableAsync(disableUrl, ct).ConfigureAwait(false); _disableSent = true; } catch { }
|
||||||
|
}
|
||||||
|
if (!_notifiedDisabled)
|
||||||
|
{
|
||||||
|
_mediator.Publish(new NotificationMessage("Nearby Detection", "AutoDetect disabled : you are not visible.", default));
|
||||||
|
_notifiedDisabled = true;
|
||||||
|
_notifiedEnabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!_config.Current.EnableAutoDetectDiscovery || !_dalamud.IsLoggedIn || !_isConnected)
|
||||||
|
{
|
||||||
|
if (!_config.Current.EnableAutoDetectDiscovery && !string.IsNullOrEmpty(_configProvider.PublishEndpoint))
|
||||||
|
{
|
||||||
|
var disableUrl = _configProvider.PublishEndpoint.Replace("/publish", "/disable");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!_disableSent)
|
||||||
|
{
|
||||||
|
await _api.DisableAsync(disableUrl, ct).ConfigureAwait(false);
|
||||||
|
_disableSent = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
|
||||||
|
if (!_notifiedDisabled)
|
||||||
|
{
|
||||||
|
_mediator.Publish(new NotificationMessage("Nearby Detection", "AutoDetect disabled : you are not visible.", default));
|
||||||
|
_notifiedDisabled = true;
|
||||||
|
_notifiedEnabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await Task.Delay(1000, ct).ConfigureAwait(false);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!_configProvider.HasConfig || _configProvider.IsExpired())
|
||||||
|
{
|
||||||
|
if (!_configProvider.TryLoadFromStapled())
|
||||||
|
{
|
||||||
|
await _configProvider.TryFetchFromServerAsync(ct).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (!_loggedConfigReady && _configProvider.NearbyEnabled)
|
||||||
|
{
|
||||||
|
_loggedConfigReady = true;
|
||||||
|
_logger.LogInformation("Nearby: well-known loaded and enabled; refresh={refresh}s, expires={exp}", _configProvider.RefreshSec, _configProvider.SaltExpiresAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
var entries = await GetLocalNearbyAsync().ConfigureAwait(false);
|
||||||
|
// Log when local count changes (including 0) to indicate activity
|
||||||
|
if (entries.Count != _lastLocalCount)
|
||||||
|
{
|
||||||
|
_lastLocalCount = entries.Count;
|
||||||
|
_logger.LogTrace("Nearby: {count} players detected locally", _lastLocalCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try query server if config and endpoints are present
|
||||||
|
if (_configProvider.NearbyEnabled && !_configProvider.IsExpired() &&
|
||||||
|
_configProvider.Salt is { Length: > 0 })
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var saltHex = Convert.ToHexString(_configProvider.Salt!);
|
||||||
|
// map hash->index for result matching
|
||||||
|
Dictionary<string, int> hashToIndex = new(StringComparer.Ordinal);
|
||||||
|
List<string> hashes = new(entries.Count);
|
||||||
|
foreach (var (entry, idx) in entries.Select((e, i) => (e, i)))
|
||||||
|
{
|
||||||
|
var h = (saltHex + entry.Name + entry.WorldId.ToString()).GetHash256();
|
||||||
|
hashToIndex[h] = idx;
|
||||||
|
hashes.Add(h);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var snapSig = string.Join(',', hashes.OrderBy(s => s, StringComparer.Ordinal)).GetHash256();
|
||||||
|
if (!string.Equals(snapSig, _lastSnapshotSig, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
_lastSnapshotSig = snapSig;
|
||||||
|
var sample = entries.Take(5).Select(e =>
|
||||||
|
{
|
||||||
|
var hh = (saltHex + e.Name + e.WorldId.ToString()).GetHash256();
|
||||||
|
var shortH = hh.Length > 8 ? hh[..8] : hh;
|
||||||
|
return $"{e.Name}({e.WorldId})->{shortH}";
|
||||||
|
});
|
||||||
|
var saltShort = saltHex.Length > 8 ? saltHex[..8] : saltHex;
|
||||||
|
_logger.LogTrace("Nearby snapshot: {count} entries; salt={saltShort}…; samples=[{samples}]",
|
||||||
|
entries.Count, saltShort, string.Join(", ", sample));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(_configProvider.PublishEndpoint))
|
||||||
|
{
|
||||||
|
string? displayName = null;
|
||||||
|
string? selfHash = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var me = await _dalamud.RunOnFrameworkThread(() => _dalamud.GetPlayerCharacter()).ConfigureAwait(false);
|
||||||
|
if (me != null)
|
||||||
|
{
|
||||||
|
displayName = me.Name.TextValue;
|
||||||
|
ushort meWorld = 0;
|
||||||
|
if (me is Dalamud.Game.ClientState.Objects.SubKinds.IPlayerCharacter mePc)
|
||||||
|
meWorld = (ushort)mePc.HomeWorld.RowId;
|
||||||
|
_logger.LogTrace("Nearby self ident: {name} ({world})", displayName, meWorld);
|
||||||
|
selfHash = (saltHex + displayName + meWorld.ToString()).GetHash256();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { /* ignore */ }
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(selfHash))
|
||||||
|
{
|
||||||
|
var sig = selfHash!;
|
||||||
|
if (!string.Equals(sig, _lastPublishedSignature, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
_lastPublishedSignature = sig;
|
||||||
|
var shortSelf = selfHash!.Length > 8 ? selfHash[..8] : selfHash;
|
||||||
|
_logger.LogDebug("Nearby publish: self presence updated (hash={hash})", shortSelf);
|
||||||
|
var ok = await _api.PublishAsync(_configProvider.PublishEndpoint!, new[] { selfHash! }, displayName, ct, _config.Current.AllowAutoDetectPairRequests).ConfigureAwait(false);
|
||||||
|
_logger.LogInformation("Nearby publish result: {result}", ok ? "success" : "failed");
|
||||||
|
if (ok) _lastHeartbeat = DateTime.UtcNow;
|
||||||
|
if (ok)
|
||||||
|
{
|
||||||
|
if (!_notifiedEnabled)
|
||||||
|
{
|
||||||
|
_mediator.Publish(new NotificationMessage("Nearby Detection", "AutoDetect enabled : you are now visible.", default));
|
||||||
|
_notifiedEnabled = true;
|
||||||
|
_notifiedDisabled = false;
|
||||||
|
_disableSent = false; // allow future /disable when turning off again
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// No changes; perform heartbeat publish if interval elapsed
|
||||||
|
if (DateTime.UtcNow - _lastHeartbeat >= HeartbeatInterval)
|
||||||
|
{
|
||||||
|
var okHb = await _api.PublishAsync(_configProvider.PublishEndpoint!, new[] { selfHash! }, displayName, ct, _config.Current.AllowAutoDetectPairRequests).ConfigureAwait(false);
|
||||||
|
_logger.LogDebug("Nearby heartbeat publish: {result}", okHb ? "success" : "failed");
|
||||||
|
if (okHb) _lastHeartbeat = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Nearby publish skipped (no changes)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// else: no self character available; skip publish silently
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query for matches if endpoint is available
|
||||||
|
if (!string.IsNullOrEmpty(_configProvider.QueryEndpoint))
|
||||||
|
{
|
||||||
|
// chunked queries
|
||||||
|
int batch = Math.Max(1, _configProvider.MaxQueryBatch);
|
||||||
|
List<ServerMatch> allMatches = new();
|
||||||
|
for (int i = 0; i < hashes.Count; i += batch)
|
||||||
|
{
|
||||||
|
var slice = hashes.Skip(i).Take(batch).ToArray();
|
||||||
|
var res = await _api.QueryAsync(_configProvider.QueryEndpoint!, slice, ct).ConfigureAwait(false);
|
||||||
|
if (res != null && res.Count > 0) allMatches.AddRange(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allMatches.Count > 0)
|
||||||
|
{
|
||||||
|
foreach (var m in allMatches)
|
||||||
|
{
|
||||||
|
if (hashToIndex.TryGetValue(m.Hash, out var idx))
|
||||||
|
{
|
||||||
|
var e = entries[idx];
|
||||||
|
entries[idx] = new NearbyEntry(e.Name, e.WorldId, e.Distance, true, m.Token, m.DisplayName, m.Uid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (allMatches.Count > 0)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Nearby: server returned {count} matches", allMatches.Count);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogTrace("Nearby: server returned {count} matches", allMatches.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log change in number of Umbra matches
|
||||||
|
int matchCount = entries.Count(e => e.IsMatch);
|
||||||
|
if (matchCount != _lastMatchCount)
|
||||||
|
{
|
||||||
|
_lastMatchCount = matchCount;
|
||||||
|
if (matchCount > 0)
|
||||||
|
{
|
||||||
|
var matchSamples = entries.Where(e => e.IsMatch).Take(5)
|
||||||
|
.Select(e => string.IsNullOrEmpty(e.DisplayName) ? e.Name : e.DisplayName!);
|
||||||
|
_logger.LogInformation("Nearby: {count} Umbra users nearby [{samples}]",
|
||||||
|
matchCount, string.Join(", ", matchSamples));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogTrace("Nearby: {count} Umbra users nearby", matchCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogDebug(ex, "Nearby query failed; falling back to local list");
|
||||||
|
if (ex.Message.Contains("DISCOVERY_SALT_EXPIRED", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Nearby: salt expired, refetching well-known");
|
||||||
|
try { await _configProvider.TryFetchFromServerAsync(ct).ConfigureAwait(false); } catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!_loggedLocalOnly)
|
||||||
|
{
|
||||||
|
_loggedLocalOnly = true;
|
||||||
|
_logger.LogDebug("Nearby: well-known not available or disabled; running in local-only mode");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_mediator.Publish(new DiscoveryListUpdated(entries));
|
||||||
|
|
||||||
|
var delayMs = Math.Max(1000, _configProvider.MinQueryIntervalMs);
|
||||||
|
if (entries.Count == 0) delayMs = Math.Max(delayMs, 5000);
|
||||||
|
await Task.Delay(delayMs, ct).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) { }
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogDebug(ex, "NearbyDiscoveryService loop error");
|
||||||
|
await Task.Delay(2000, ct).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<List<NearbyEntry>> GetLocalNearbyAsync()
|
||||||
|
{
|
||||||
|
var list = new List<NearbyEntry>();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var local = await _dalamud.RunOnFrameworkThread(() => _dalamud.GetPlayerCharacter()).ConfigureAwait(false);
|
||||||
|
var localPos = local?.Position ?? Vector3.Zero;
|
||||||
|
int maxDist = Math.Clamp(_config.Current.AutoDetectMaxDistanceMeters, 5, 100);
|
||||||
|
|
||||||
|
int limit = Math.Min(200, _objectTable.Length);
|
||||||
|
for (int i = 0; i < limit; i++)
|
||||||
|
{
|
||||||
|
var obj = await _dalamud.RunOnFrameworkThread(() => _objectTable[i]).ConfigureAwait(false);
|
||||||
|
if (obj == null || obj.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player) continue;
|
||||||
|
if (local != null && obj.Address == local.Address) continue;
|
||||||
|
|
||||||
|
float dist = local == null ? float.NaN : Vector3.Distance(localPos, obj.Position);
|
||||||
|
if (!float.IsNaN(dist) && dist > maxDist) continue;
|
||||||
|
|
||||||
|
string name = obj.Name.TextValue;
|
||||||
|
ushort worldId = 0;
|
||||||
|
if (obj is Dalamud.Game.ClientState.Objects.SubKinds.IPlayerCharacter pc)
|
||||||
|
worldId = (ushort)pc.HomeWorld.RowId;
|
||||||
|
|
||||||
|
list.Add(new NearbyEntry(name, worldId, dist, false, null, null, null));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
}
|
||||||
87
MareSynchronos/Services/AutoDetect/NearbyPendingService.cs
Normal file
87
MareSynchronos/Services/AutoDetect/NearbyPendingService.cs
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using MareSynchronos.WebAPI;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace MareSynchronos.Services.AutoDetect;
|
||||||
|
|
||||||
|
public sealed class NearbyPendingService : IMediatorSubscriber
|
||||||
|
{
|
||||||
|
private readonly ILogger<NearbyPendingService> _logger;
|
||||||
|
private readonly MareMediator _mediator;
|
||||||
|
private readonly ApiController _api;
|
||||||
|
private readonly AutoDetectRequestService _requestService;
|
||||||
|
private readonly ConcurrentDictionary<string, string> _pending = new(StringComparer.Ordinal);
|
||||||
|
private static readonly Regex ReqRegex = new(@"^Nearby Request: (.+) \[(?<uid>[A-Z0-9]+)\]$", RegexOptions.Compiled);
|
||||||
|
private static readonly Regex AcceptRegex = new(@"^Nearby Accept: (.+) \[(?<uid>[A-Z0-9]+)\]$", RegexOptions.Compiled);
|
||||||
|
|
||||||
|
public NearbyPendingService(ILogger<NearbyPendingService> logger, MareMediator mediator, ApiController api, AutoDetectRequestService requestService)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_mediator = mediator;
|
||||||
|
_api = api;
|
||||||
|
_requestService = requestService;
|
||||||
|
_mediator.Subscribe<NotificationMessage>(this, OnNotification);
|
||||||
|
}
|
||||||
|
|
||||||
|
public MareMediator Mediator => _mediator;
|
||||||
|
|
||||||
|
public IReadOnlyDictionary<string, string> Pending => _pending;
|
||||||
|
|
||||||
|
private void OnNotification(NotificationMessage msg)
|
||||||
|
{
|
||||||
|
// Watch info messages for Nearby request pattern
|
||||||
|
if (msg.Type != MareSynchronos.MareConfiguration.Models.NotificationType.Info) return;
|
||||||
|
var ma = AcceptRegex.Match(msg.Message);
|
||||||
|
if (ma.Success)
|
||||||
|
{
|
||||||
|
var uidA = ma.Groups["uid"].Value;
|
||||||
|
if (!string.IsNullOrEmpty(uidA))
|
||||||
|
{
|
||||||
|
_ = _api.UserAddPair(new MareSynchronos.API.Dto.User.UserDto(new MareSynchronos.API.Data.UserData(uidA)));
|
||||||
|
_pending.TryRemove(uidA, out _);
|
||||||
|
_logger.LogInformation("NearbyPending: auto-accepted pairing with {uid}", uidA);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var m = ReqRegex.Match(msg.Message);
|
||||||
|
if (!m.Success) return;
|
||||||
|
var uid = m.Groups["uid"].Value;
|
||||||
|
if (string.IsNullOrEmpty(uid)) return;
|
||||||
|
// Try to extract name as everything before space and '['
|
||||||
|
var name = msg.Message;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var idx = msg.Message.IndexOf(':');
|
||||||
|
if (idx >= 0) name = msg.Message[(idx + 1)..].Trim();
|
||||||
|
var br = name.LastIndexOf('[');
|
||||||
|
if (br > 0) name = name[..br].Trim();
|
||||||
|
}
|
||||||
|
catch { name = uid; }
|
||||||
|
_pending[uid] = name;
|
||||||
|
_logger.LogInformation("NearbyPending: received request from {uid} ({name})", uid, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Remove(string uid)
|
||||||
|
{
|
||||||
|
_pending.TryRemove(uid, out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> AcceptAsync(string uid)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _api.UserAddPair(new MareSynchronos.API.Dto.User.UserDto(new MareSynchronos.API.Data.UserData(uid))).ConfigureAwait(false);
|
||||||
|
_pending.TryRemove(uid, out _);
|
||||||
|
_ = _requestService.SendAcceptNotifyAsync(uid);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "NearbyPending: accept failed for {uid}", uid);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,10 +14,8 @@ namespace MareSynchronos.Services;
|
|||||||
|
|
||||||
public sealed class CommandManagerService : IDisposable
|
public sealed class CommandManagerService : IDisposable
|
||||||
{
|
{
|
||||||
private const string _commandName = "/sync";
|
private const string _commandName = "/usync";
|
||||||
private const string _commandName2 = "/usync";
|
private const string _ssCommandPrefix = "/ums";
|
||||||
|
|
||||||
private const string _ssCommandPrefix = "/ss";
|
|
||||||
|
|
||||||
private readonly ApiController _apiController;
|
private readonly ApiController _apiController;
|
||||||
private readonly ICommandManager _commandManager;
|
private readonly ICommandManager _commandManager;
|
||||||
@@ -42,11 +40,7 @@ public sealed class CommandManagerService : IDisposable
|
|||||||
_mareConfigService = mareConfigService;
|
_mareConfigService = mareConfigService;
|
||||||
_commandManager.AddHandler(_commandName, new CommandInfo(OnCommand)
|
_commandManager.AddHandler(_commandName, new CommandInfo(OnCommand)
|
||||||
{
|
{
|
||||||
HelpMessage = "Opens the Umbra UI"
|
HelpMessage = "Opens the UmbraSync UI"
|
||||||
});
|
|
||||||
_commandManager.AddHandler(_commandName2, new CommandInfo(OnCommand)
|
|
||||||
{
|
|
||||||
HelpMessage = "Opens the Umbra UI"
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Lazy registration of all possible /ss# commands which tbf is what the game does for linkshells anyway
|
// Lazy registration of all possible /ss# commands which tbf is what the game does for linkshells anyway
|
||||||
@@ -62,7 +56,7 @@ public sealed class CommandManagerService : IDisposable
|
|||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_commandManager.RemoveHandler(_commandName);
|
_commandManager.RemoveHandler(_commandName);
|
||||||
_commandManager.RemoveHandler(_commandName2);
|
|
||||||
|
|
||||||
for (int i = 1; i <= ChatService.CommandMaxNumber; ++i)
|
for (int i = 1; i <= ChatService.CommandMaxNumber; ++i)
|
||||||
_commandManager.RemoveHandler($"{_ssCommandPrefix}{i}");
|
_commandManager.RemoveHandler($"{_ssCommandPrefix}{i}");
|
||||||
@@ -147,7 +141,6 @@ public sealed class CommandManagerService : IDisposable
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// FIXME: Chat content seems to already be stripped of any special characters here?
|
|
||||||
byte[] chatBytes = Encoding.UTF8.GetBytes(args);
|
byte[] chatBytes = Encoding.UTF8.GetBytes(args);
|
||||||
_chatService.SendChatShell(shellNumber, chatBytes);
|
_chatService.SendChatShell(shellNumber, chatBytes);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ using System.Numerics;
|
|||||||
|
|
||||||
namespace MareSynchronos.Services.Mediator;
|
namespace MareSynchronos.Services.Mediator;
|
||||||
|
|
||||||
#pragma warning disable MA0048 // File name must match type name
|
#pragma warning disable MA0048
|
||||||
#pragma warning disable S2094
|
#pragma warning disable S2094
|
||||||
public record SwitchToIntroUiMessage : MessageBase;
|
public record SwitchToIntroUiMessage : MessageBase;
|
||||||
public record SwitchToMainUiMessage : MessageBase;
|
public record SwitchToMainUiMessage : MessageBase;
|
||||||
@@ -108,6 +108,11 @@ public record GPoseLobbyReceiveCharaData(CharaDataDownloadDto CharaDataDownloadD
|
|||||||
public record GPoseLobbyReceivePoseData(UserData UserData, PoseData PoseData) : MessageBase;
|
public record GPoseLobbyReceivePoseData(UserData UserData, PoseData PoseData) : MessageBase;
|
||||||
public record GPoseLobbyReceiveWorldData(UserData UserData, WorldData WorldData) : MessageBase;
|
public record GPoseLobbyReceiveWorldData(UserData UserData, WorldData WorldData) : MessageBase;
|
||||||
|
|
||||||
|
public record NearbyEntry(string Name, ushort WorldId, float Distance, bool IsMatch, string? Token, string? DisplayName, string? Uid, bool AcceptPairRequests = true);
|
||||||
|
public record DiscoveryListUpdated(List<NearbyEntry> Entries) : MessageBase;
|
||||||
|
public record NearbyDetectionToggled(bool Enabled) : MessageBase;
|
||||||
|
public record AllowPairRequestsToggled(bool Enabled) : 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
|
||||||
#pragma warning restore MA0048 // File name must match type name
|
#pragma warning restore MA0048
|
||||||
|
|||||||
225
MareSynchronos/Services/TemporarySyncshellNotificationService.cs
Normal file
225
MareSynchronos/Services/TemporarySyncshellNotificationService.cs
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using System.Threading;
|
||||||
|
using MareSynchronos.API.Dto.Group;
|
||||||
|
using MareSynchronos.MareConfiguration.Models;
|
||||||
|
using MareSynchronos.PlayerData.Pairs;
|
||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using MareSynchronos.WebAPI;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace MareSynchronos.Services;
|
||||||
|
|
||||||
|
public sealed class TemporarySyncshellNotificationService : MediatorSubscriberBase, IHostedService
|
||||||
|
{
|
||||||
|
private static readonly int[] NotificationThresholdMinutes = [30, 15, 5, 1];
|
||||||
|
private readonly ApiController _apiController;
|
||||||
|
private readonly PairManager _pairManager;
|
||||||
|
private readonly Lock _stateLock = new();
|
||||||
|
private readonly Dictionary<string, TrackedGroup> _trackedGroups = new(StringComparer.Ordinal);
|
||||||
|
private CancellationTokenSource? _loopCts;
|
||||||
|
private Task? _loopTask;
|
||||||
|
|
||||||
|
public TemporarySyncshellNotificationService(ILogger<TemporarySyncshellNotificationService> logger, MareMediator mediator, PairManager pairManager, ApiController apiController)
|
||||||
|
: base(logger, mediator)
|
||||||
|
{
|
||||||
|
_pairManager = pairManager;
|
||||||
|
_apiController = apiController;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_loopCts = new CancellationTokenSource();
|
||||||
|
Mediator.Subscribe<ConnectedMessage>(this, _ => ResetTrackedGroups());
|
||||||
|
Mediator.Subscribe<DisconnectedMessage>(this, _ => ResetTrackedGroups());
|
||||||
|
_loopTask = Task.Run(() => MonitorLoopAsync(_loopCts.Token), _loopCts.Token);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task StopAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
Mediator.UnsubscribeAll(this);
|
||||||
|
if (_loopCts == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_loopCts.Cancel();
|
||||||
|
if (_loopTask != null)
|
||||||
|
{
|
||||||
|
await _loopTask.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_loopTask = null;
|
||||||
|
_loopCts.Dispose();
|
||||||
|
_loopCts = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task MonitorLoopAsync(CancellationToken ct)
|
||||||
|
{
|
||||||
|
var delay = TimeSpan.FromSeconds(30);
|
||||||
|
while (!ct.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
CheckGroups();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogDebug(ex, "Failed to check temporary syncshell expirations");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.Delay(delay, ct).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CheckGroups()
|
||||||
|
{
|
||||||
|
var nowUtc = DateTime.UtcNow;
|
||||||
|
var groupsSnapshot = _pairManager.Groups.Values.ToList();
|
||||||
|
var notifications = new List<NotificationPayload>();
|
||||||
|
var expiredGroups = new List<GroupFullInfoDto>();
|
||||||
|
var seenTemporaryGids = new HashSet<string>(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
using (var guard = _stateLock.EnterScope())
|
||||||
|
{
|
||||||
|
foreach (var group in groupsSnapshot)
|
||||||
|
{
|
||||||
|
if (!group.IsTemporary || group.ExpiresAt == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(_apiController.UID) || !string.Equals(group.OwnerUID, _apiController.UID, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var gid = group.Group.GID;
|
||||||
|
seenTemporaryGids.Add(gid);
|
||||||
|
var expiresAtUtc = NormalizeToUtc(group.ExpiresAt.Value);
|
||||||
|
var remaining = expiresAtUtc - nowUtc;
|
||||||
|
|
||||||
|
if (!_trackedGroups.TryGetValue(gid, out var state))
|
||||||
|
{
|
||||||
|
state = new TrackedGroup(expiresAtUtc);
|
||||||
|
_trackedGroups[gid] = state;
|
||||||
|
}
|
||||||
|
else if (state.ExpiresAtUtc != expiresAtUtc)
|
||||||
|
{
|
||||||
|
state.UpdateExpiresAt(expiresAtUtc);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remaining <= TimeSpan.Zero)
|
||||||
|
{
|
||||||
|
_trackedGroups.Remove(gid);
|
||||||
|
expiredGroups.Add(group);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!state.LastRemaining.HasValue)
|
||||||
|
{
|
||||||
|
state.UpdateRemaining(remaining);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var previousRemaining = state.LastRemaining.Value;
|
||||||
|
|
||||||
|
foreach (var thresholdMinutes in NotificationThresholdMinutes)
|
||||||
|
{
|
||||||
|
var threshold = TimeSpan.FromMinutes(thresholdMinutes);
|
||||||
|
if (previousRemaining > threshold && remaining <= threshold)
|
||||||
|
{
|
||||||
|
notifications.Add(new NotificationPayload(group, thresholdMinutes, expiresAtUtc));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.UpdateRemaining(remaining);
|
||||||
|
}
|
||||||
|
|
||||||
|
var toRemove = _trackedGroups.Keys.Where(k => !seenTemporaryGids.Contains(k)).ToList();
|
||||||
|
foreach (var gid in toRemove)
|
||||||
|
{
|
||||||
|
_trackedGroups.Remove(gid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var expiredGroup in expiredGroups)
|
||||||
|
{
|
||||||
|
Logger.LogInformation("Temporary syncshell {gid} expired locally; removing", expiredGroup.Group.GID);
|
||||||
|
_pairManager.RemoveGroup(expiredGroup.Group);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var notification in notifications)
|
||||||
|
{
|
||||||
|
PublishNotification(notification.Group, notification.ThresholdMinutes, notification.ExpiresAtUtc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PublishNotification(GroupFullInfoDto group, int thresholdMinutes, DateTime expiresAtUtc)
|
||||||
|
{
|
||||||
|
string displayName = string.IsNullOrWhiteSpace(group.GroupAlias) ? group.Group.GID : group.GroupAlias!;
|
||||||
|
string threshold = thresholdMinutes == 1 ? "1 minute" : $"{thresholdMinutes} minutes";
|
||||||
|
string expiresLocal = expiresAtUtc.ToLocalTime().ToString("t", CultureInfo.CurrentCulture);
|
||||||
|
|
||||||
|
string message = $"La Syncshell temporaire \"{displayName}\" sera supprimee dans {threshold} (a {expiresLocal}).";
|
||||||
|
Mediator.Publish(new NotificationMessage("Syncshell temporaire", message, NotificationType.Warning, TimeSpan.FromSeconds(6)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DateTime NormalizeToUtc(DateTime expiresAt)
|
||||||
|
{
|
||||||
|
return expiresAt.Kind switch
|
||||||
|
{
|
||||||
|
DateTimeKind.Utc => expiresAt,
|
||||||
|
DateTimeKind.Local => expiresAt.ToUniversalTime(),
|
||||||
|
_ => DateTime.SpecifyKind(expiresAt, DateTimeKind.Utc)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ResetTrackedGroups()
|
||||||
|
{
|
||||||
|
using (var guard = _stateLock.EnterScope())
|
||||||
|
{
|
||||||
|
_trackedGroups.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TrackedGroup
|
||||||
|
{
|
||||||
|
public TrackedGroup(DateTime expiresAtUtc)
|
||||||
|
{
|
||||||
|
ExpiresAtUtc = expiresAtUtc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DateTime ExpiresAtUtc { get; private set; }
|
||||||
|
public TimeSpan? LastRemaining { get; private set; }
|
||||||
|
|
||||||
|
public void UpdateExpiresAt(DateTime expiresAtUtc)
|
||||||
|
{
|
||||||
|
ExpiresAtUtc = expiresAtUtc;
|
||||||
|
LastRemaining = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateRemaining(TimeSpan remaining)
|
||||||
|
{
|
||||||
|
LastRemaining = remaining;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed record NotificationPayload(GroupFullInfoDto Group, int ThresholdMinutes, DateTime ExpiresAtUtc);
|
||||||
|
}
|
||||||
197
MareSynchronos/UI/AutoDetectUi.cs
Normal file
197
MareSynchronos/UI/AutoDetectUi.cs
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
using Dalamud.Bindings.ImGui;
|
||||||
|
using Dalamud.Interface.Colors;
|
||||||
|
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||||
|
using Dalamud.Interface.Utility;
|
||||||
|
using Dalamud.Interface.Utility.Raii;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using MareSynchronos.MareConfiguration;
|
||||||
|
using MareSynchronos.Services;
|
||||||
|
using MareSynchronos.PlayerData.Pairs;
|
||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Numerics;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace MareSynchronos.UI;
|
||||||
|
|
||||||
|
public class AutoDetectUi : WindowMediatorSubscriberBase
|
||||||
|
{
|
||||||
|
private readonly MareConfigService _configService;
|
||||||
|
private readonly DalamudUtilService _dalamud;
|
||||||
|
private readonly IObjectTable _objectTable;
|
||||||
|
private readonly Services.AutoDetect.AutoDetectRequestService _requestService;
|
||||||
|
private readonly PairManager _pairManager;
|
||||||
|
private List<Services.Mediator.NearbyEntry> _entries = new();
|
||||||
|
|
||||||
|
public AutoDetectUi(ILogger<AutoDetectUi> logger, MareMediator mediator,
|
||||||
|
MareConfigService configService, DalamudUtilService dalamudUtilService, IObjectTable objectTable,
|
||||||
|
Services.AutoDetect.AutoDetectRequestService requestService, PairManager pairManager,
|
||||||
|
PerformanceCollectorService performanceCollectorService)
|
||||||
|
: base(logger, mediator, "AutoDetect", performanceCollectorService)
|
||||||
|
{
|
||||||
|
_configService = configService;
|
||||||
|
_dalamud = dalamudUtilService;
|
||||||
|
_objectTable = objectTable;
|
||||||
|
_requestService = requestService;
|
||||||
|
_pairManager = pairManager;
|
||||||
|
|
||||||
|
Flags |= ImGuiWindowFlags.NoScrollbar;
|
||||||
|
SizeConstraints = new WindowSizeConstraints()
|
||||||
|
{
|
||||||
|
MinimumSize = new Vector2(350, 220),
|
||||||
|
MaximumSize = new Vector2(600, 600),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool DrawConditions()
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void DrawInternal()
|
||||||
|
{
|
||||||
|
using var idScope = ImRaii.PushId("autodetect-ui");
|
||||||
|
|
||||||
|
if (!_configService.Current.EnableAutoDetectDiscovery)
|
||||||
|
{
|
||||||
|
UiSharedService.ColorTextWrapped("Nearby detection is disabled. Enable it in Settings to start detecting nearby Umbra users.", ImGuiColors.DalamudYellow);
|
||||||
|
ImGuiHelpers.ScaledDummy(6);
|
||||||
|
}
|
||||||
|
|
||||||
|
int maxDist = Math.Clamp(_configService.Current.AutoDetectMaxDistanceMeters, 5, 100);
|
||||||
|
ImGui.AlignTextToFramePadding();
|
||||||
|
ImGui.TextUnformatted("Max distance (m)");
|
||||||
|
ImGui.SameLine();
|
||||||
|
ImGui.SetNextItemWidth(120 * ImGuiHelpers.GlobalScale);
|
||||||
|
if (ImGui.SliderInt("##autodetect-dist", ref maxDist, 5, 100))
|
||||||
|
{
|
||||||
|
_configService.Current.AutoDetectMaxDistanceMeters = maxDist;
|
||||||
|
_configService.Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGuiHelpers.ScaledDummy(6);
|
||||||
|
|
||||||
|
// Table header
|
||||||
|
if (ImGui.BeginTable("autodetect-nearby", 5, ImGuiTableFlags.SizingStretchProp))
|
||||||
|
{
|
||||||
|
ImGui.TableSetupColumn("Name");
|
||||||
|
ImGui.TableSetupColumn("World");
|
||||||
|
ImGui.TableSetupColumn("Distance");
|
||||||
|
ImGui.TableSetupColumn("Status");
|
||||||
|
ImGui.TableSetupColumn("Action");
|
||||||
|
ImGui.TableHeadersRow();
|
||||||
|
|
||||||
|
var data = _entries.Count > 0 ? _entries.Where(e => e.IsMatch).ToList() : new List<Services.Mediator.NearbyEntry>();
|
||||||
|
foreach (var e in data)
|
||||||
|
{
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.TextUnformatted(e.Name);
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.TextUnformatted(e.WorldId == 0 ? "-" : (_dalamud.WorldData.Value.TryGetValue(e.WorldId, out var w) ? w : e.WorldId.ToString()));
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.TextUnformatted(float.IsNaN(e.Distance) ? "-" : $"{e.Distance:0.0} m");
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
bool alreadyPaired = IsAlreadyPairedByUidOrAlias(e);
|
||||||
|
string status = alreadyPaired ? "Paired" : (string.IsNullOrEmpty(e.Token) ? "Requests disabled" : "On Umbra");
|
||||||
|
ImGui.TextUnformatted(status);
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
using (ImRaii.Disabled(alreadyPaired || string.IsNullOrEmpty(e.Token)))
|
||||||
|
{
|
||||||
|
if (alreadyPaired)
|
||||||
|
{
|
||||||
|
ImGui.Button($"Already sync##{e.Name}");
|
||||||
|
}
|
||||||
|
else if (string.IsNullOrEmpty(e.Token))
|
||||||
|
{
|
||||||
|
ImGui.Button($"Requests disabled##{e.Name}");
|
||||||
|
}
|
||||||
|
else if (ImGui.Button($"Send request##{e.Name}"))
|
||||||
|
{
|
||||||
|
_ = _requestService.SendRequestAsync(e.Token!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.EndTable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnOpen()
|
||||||
|
{
|
||||||
|
base.OnOpen();
|
||||||
|
Mediator.Subscribe<Services.Mediator.DiscoveryListUpdated>(this, OnDiscoveryUpdated);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnClose()
|
||||||
|
{
|
||||||
|
Mediator.Unsubscribe<Services.Mediator.DiscoveryListUpdated>(this);
|
||||||
|
base.OnClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDiscoveryUpdated(Services.Mediator.DiscoveryListUpdated msg)
|
||||||
|
{
|
||||||
|
_entries = msg.Entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Services.Mediator.NearbyEntry> BuildLocalSnapshot(int maxDist)
|
||||||
|
{
|
||||||
|
var list = new List<Services.Mediator.NearbyEntry>();
|
||||||
|
var local = _dalamud.GetPlayerCharacter();
|
||||||
|
var localPos = local?.Position ?? Vector3.Zero;
|
||||||
|
for (int i = 0; i < 200; i += 2)
|
||||||
|
{
|
||||||
|
var obj = _objectTable[i];
|
||||||
|
if (obj == null || obj.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player) continue;
|
||||||
|
if (local != null && obj.Address == local.Address) continue;
|
||||||
|
float dist = local == null ? float.NaN : Vector3.Distance(localPos, obj.Position);
|
||||||
|
if (!float.IsNaN(dist) && dist > maxDist) continue;
|
||||||
|
string name = obj.Name.ToString();
|
||||||
|
ushort worldId = 0;
|
||||||
|
if (obj is IPlayerCharacter pc) worldId = (ushort)pc.HomeWorld.RowId;
|
||||||
|
list.Add(new Services.Mediator.NearbyEntry(name, worldId, dist, false, null, null, null));
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsAlreadyPairedByUidOrAlias(Services.Mediator.NearbyEntry e)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 1) Match by UID when available (authoritative)
|
||||||
|
if (!string.IsNullOrEmpty(e.Uid))
|
||||||
|
{
|
||||||
|
foreach (var p in _pairManager.DirectPairs)
|
||||||
|
{
|
||||||
|
if (string.Equals(p.UserData.UID, e.Uid, StringComparison.Ordinal))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var key = NormalizeKey(e.DisplayName ?? e.Name);
|
||||||
|
if (string.IsNullOrEmpty(key)) return false;
|
||||||
|
foreach (var p in _pairManager.DirectPairs)
|
||||||
|
{
|
||||||
|
if (NormalizeKey(p.UserData.AliasOrUID) == key) return true;
|
||||||
|
if (!string.IsNullOrEmpty(p.UserData.Alias) && NormalizeKey(p.UserData.Alias!) == key) return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizeKey(string? input)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(input)) return string.Empty;
|
||||||
|
var formD = input.Normalize(NormalizationForm.FormD);
|
||||||
|
var sb = new StringBuilder(formD.Length);
|
||||||
|
foreach (var ch in formD)
|
||||||
|
{
|
||||||
|
var cat = CharUnicodeInfo.GetUnicodeCategory(ch);
|
||||||
|
if (cat != UnicodeCategory.NonSpacingMark)
|
||||||
|
sb.Append(char.ToLowerInvariant(ch));
|
||||||
|
}
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ using MareSynchronos.PlayerData.Pairs;
|
|||||||
using MareSynchronos.Services;
|
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.UI.Components;
|
using MareSynchronos.UI.Components;
|
||||||
using MareSynchronos.UI.Handlers;
|
using MareSynchronos.UI.Handlers;
|
||||||
using MareSynchronos.WebAPI;
|
using MareSynchronos.WebAPI;
|
||||||
@@ -24,6 +25,7 @@ using System.Diagnostics;
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
namespace MareSynchronos.UI;
|
namespace MareSynchronos.UI;
|
||||||
|
|
||||||
@@ -43,6 +45,8 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
private readonly ServerConfigurationManager _serverManager;
|
private readonly ServerConfigurationManager _serverManager;
|
||||||
private readonly Stopwatch _timeout = new();
|
private readonly Stopwatch _timeout = new();
|
||||||
private readonly CharaDataManager _charaDataManager;
|
private readonly CharaDataManager _charaDataManager;
|
||||||
|
private readonly NearbyPendingService _nearbyPending;
|
||||||
|
private readonly AutoDetectRequestService _autoDetectRequestService;
|
||||||
private readonly UidDisplayHandler _uidDisplayHandler;
|
private readonly UidDisplayHandler _uidDisplayHandler;
|
||||||
private readonly UiSharedService _uiSharedService;
|
private readonly UiSharedService _uiSharedService;
|
||||||
private bool _buttonState;
|
private bool _buttonState;
|
||||||
@@ -56,11 +60,15 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
private bool _showModalForUserAddition;
|
private bool _showModalForUserAddition;
|
||||||
private bool _showSyncShells;
|
private bool _showSyncShells;
|
||||||
private bool _wasOpen;
|
private bool _wasOpen;
|
||||||
|
private bool _nearbyOpen = true;
|
||||||
|
private List<Services.Mediator.NearbyEntry> _nearbyEntries = new();
|
||||||
|
|
||||||
public CompactUi(ILogger<CompactUi> logger, UiSharedService uiShared, MareConfigService configService, ApiController apiController, PairManager pairManager, ChatService chatService,
|
public CompactUi(ILogger<CompactUi> logger, UiSharedService uiShared, MareConfigService configService, ApiController apiController, PairManager pairManager, ChatService chatService,
|
||||||
ServerConfigurationManager serverManager, MareMediator mediator, FileUploadManager fileTransferManager, UidDisplayHandler uidDisplayHandler, CharaDataManager charaDataManager,
|
ServerConfigurationManager serverManager, MareMediator mediator, FileUploadManager fileTransferManager, UidDisplayHandler uidDisplayHandler, CharaDataManager charaDataManager,
|
||||||
|
NearbyPendingService nearbyPendingService,
|
||||||
|
AutoDetectRequestService autoDetectRequestService,
|
||||||
PerformanceCollectorService performanceCollectorService)
|
PerformanceCollectorService performanceCollectorService)
|
||||||
: base(logger, mediator, "###UmbraSyncSyncMainUI", performanceCollectorService)
|
: base(logger, mediator, "###UmbraSyncMainUI", performanceCollectorService)
|
||||||
{
|
{
|
||||||
_uiSharedService = uiShared;
|
_uiSharedService = uiShared;
|
||||||
_configService = configService;
|
_configService = configService;
|
||||||
@@ -70,6 +78,8 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
_fileTransferManager = fileTransferManager;
|
_fileTransferManager = fileTransferManager;
|
||||||
_uidDisplayHandler = uidDisplayHandler;
|
_uidDisplayHandler = uidDisplayHandler;
|
||||||
_charaDataManager = charaDataManager;
|
_charaDataManager = charaDataManager;
|
||||||
|
_nearbyPending = nearbyPendingService;
|
||||||
|
_autoDetectRequestService = autoDetectRequestService;
|
||||||
var tagHandler = new TagHandler(_serverManager);
|
var tagHandler = new TagHandler(_serverManager);
|
||||||
|
|
||||||
_groupPanel = new(this, uiShared, _pairManager, chatService, uidDisplayHandler, _configService, _serverManager, _charaDataManager);
|
_groupPanel = new(this, uiShared, _pairManager, chatService, uidDisplayHandler, _configService, _serverManager, _charaDataManager);
|
||||||
@@ -80,11 +90,11 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
#if DEBUG
|
#if DEBUG
|
||||||
string dev = "Dev Build";
|
string dev = "Dev Build";
|
||||||
var ver = Assembly.GetExecutingAssembly().GetName().Version!;
|
var ver = Assembly.GetExecutingAssembly().GetName().Version!;
|
||||||
WindowName = $"UmbraSync {dev} ({ver.Major}.{ver.Minor}.{ver.Build})###UmbraSyncSyncMainUIDev";
|
WindowName = $"UmbraSync {dev} ({ver.Major}.{ver.Minor}.{ver.Build})###UmbraSyncMainUIDev";
|
||||||
Toggle();
|
Toggle();
|
||||||
#else
|
#else
|
||||||
var ver = Assembly.GetExecutingAssembly().GetName().Version!;
|
var ver = Assembly.GetExecutingAssembly().GetName().Version!;
|
||||||
WindowName = "UmbraSync " + ver.Major + "." + ver.Minor + "." + ver.Build + "###UmbraSyncSyncMainUI";
|
WindowName = "UmbraSync " + ver.Major + "." + ver.Minor + "." + ver.Build + "###UmbracSyncMainUI";
|
||||||
#endif
|
#endif
|
||||||
Mediator.Subscribe<SwitchToMainUiMessage>(this, (_) => IsOpen = true);
|
Mediator.Subscribe<SwitchToMainUiMessage>(this, (_) => IsOpen = true);
|
||||||
Mediator.Subscribe<SwitchToIntroUiMessage>(this, (_) => IsOpen = false);
|
Mediator.Subscribe<SwitchToIntroUiMessage>(this, (_) => IsOpen = false);
|
||||||
@@ -92,6 +102,7 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
Mediator.Subscribe<CutsceneEndMessage>(this, (_) => UiSharedService_GposeEnd());
|
Mediator.Subscribe<CutsceneEndMessage>(this, (_) => UiSharedService_GposeEnd());
|
||||||
Mediator.Subscribe<DownloadStartedMessage>(this, (msg) => _currentDownloads[msg.DownloadId] = msg.DownloadStatus);
|
Mediator.Subscribe<DownloadStartedMessage>(this, (msg) => _currentDownloads[msg.DownloadId] = msg.DownloadStatus);
|
||||||
Mediator.Subscribe<DownloadFinishedMessage>(this, (msg) => _currentDownloads.TryRemove(msg.DownloadId, out _));
|
Mediator.Subscribe<DownloadFinishedMessage>(this, (msg) => _currentDownloads.TryRemove(msg.DownloadId, out _));
|
||||||
|
Mediator.Subscribe<DiscoveryListUpdated>(this, (msg) => _nearbyEntries = msg.Entries);
|
||||||
|
|
||||||
Flags |= ImGuiWindowFlags.NoDocking;
|
Flags |= ImGuiWindowFlags.NoDocking;
|
||||||
|
|
||||||
@@ -104,7 +115,7 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
protected override void DrawInternal()
|
protected override void DrawInternal()
|
||||||
{
|
{
|
||||||
UiSharedService.AccentColor = new Vector4(0.2f, 0.6f, 1f, 1f); // custom blue
|
UiSharedService.AccentColor = new Vector4(0.63f, 0.25f, 1f, 1f);
|
||||||
ImGui.SetCursorPosY(ImGui.GetCursorPosY() - ImGui.GetStyle().WindowPadding.Y - 1f * ImGuiHelpers.GlobalScale + ImGui.GetStyle().ItemSpacing.Y);
|
ImGui.SetCursorPosY(ImGui.GetCursorPosY() - ImGui.GetStyle().WindowPadding.Y - 1f * ImGuiHelpers.GlobalScale + ImGui.GetStyle().ItemSpacing.Y);
|
||||||
WindowContentWidth = UiSharedService.GetWindowContentRegionWidth();
|
WindowContentWidth = UiSharedService.GetWindowContentRegionWidth();
|
||||||
if (!_apiController.IsCurrentVersion)
|
if (!_apiController.IsCurrentVersion)
|
||||||
@@ -367,6 +378,121 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
_pairGroupsUi.Draw(visibleUsers, onlineUsers, offlineUsers);
|
_pairGroupsUi.Draw(visibleUsers, onlineUsers, offlineUsers);
|
||||||
|
|
||||||
|
// Always show a Nearby group when detection is enabled, even if empty
|
||||||
|
if (_configService.Current.EnableAutoDetectDiscovery)
|
||||||
|
{
|
||||||
|
using (ImRaii.PushId("group-Nearby"))
|
||||||
|
{
|
||||||
|
var icon = _nearbyOpen ? FontAwesomeIcon.CaretSquareDown : FontAwesomeIcon.CaretSquareRight;
|
||||||
|
_uiSharedService.IconText(icon);
|
||||||
|
if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) _nearbyOpen = !_nearbyOpen;
|
||||||
|
ImGui.SameLine();
|
||||||
|
var onUmbra = _nearbyEntries?.Count(e => e.IsMatch) ?? 0;
|
||||||
|
ImGui.TextUnformatted($"Nearby ({onUmbra})");
|
||||||
|
if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) _nearbyOpen = !_nearbyOpen;
|
||||||
|
var btnWidth = _uiSharedService.GetIconTextButtonSize(FontAwesomeIcon.UserPlus, "Nearby");
|
||||||
|
var headerRight = ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth();
|
||||||
|
ImGui.SameLine();
|
||||||
|
ImGui.SetCursorPosX(headerRight - btnWidth);
|
||||||
|
if (_uiSharedService.IconTextButton(FontAwesomeIcon.UserPlus, "Nearby", btnWidth))
|
||||||
|
{
|
||||||
|
Mediator.Publish(new UiToggleMessage(typeof(AutoDetectUi)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_nearbyOpen)
|
||||||
|
{
|
||||||
|
ImGui.Indent();
|
||||||
|
var nearby = _nearbyEntries == null
|
||||||
|
? new List<Services.Mediator.NearbyEntry>()
|
||||||
|
: _nearbyEntries.Where(e => e.IsMatch)
|
||||||
|
.OrderBy(e => e.Distance)
|
||||||
|
.ToList();
|
||||||
|
if (nearby.Count == 0)
|
||||||
|
{
|
||||||
|
UiSharedService.ColorTextWrapped("No nearby players detected.", ImGuiColors.DalamudGrey3);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach (var e in nearby)
|
||||||
|
{
|
||||||
|
var name = e.DisplayName ?? e.Name;
|
||||||
|
ImGui.AlignTextToFramePadding();
|
||||||
|
ImGui.TextUnformatted(name);
|
||||||
|
var right = ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth();
|
||||||
|
ImGui.SameLine();
|
||||||
|
|
||||||
|
bool isPaired = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
isPaired = _pairManager.DirectPairs.Any(p => string.Equals(p.UserData.UID, e.Uid, StringComparison.Ordinal));
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
var key = (e.DisplayName ?? e.Name) ?? string.Empty;
|
||||||
|
isPaired = _pairManager.DirectPairs.Any(p => string.Equals(p.UserData.AliasOrUID, key, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
var statusButtonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.UserPlus);
|
||||||
|
ImGui.SetCursorPosX(right - statusButtonSize.X);
|
||||||
|
|
||||||
|
if (isPaired)
|
||||||
|
{
|
||||||
|
_uiSharedService.IconText(FontAwesomeIcon.Check, ImGuiColors.ParsedGreen);
|
||||||
|
UiSharedService.AttachToolTip("Déjà apparié sur Umbra");
|
||||||
|
}
|
||||||
|
else 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))
|
||||||
|
{
|
||||||
|
if (_uiSharedService.IconButton(FontAwesomeIcon.UserPlus))
|
||||||
|
{
|
||||||
|
_ = _autoDetectRequestService.SendRequestAsync(e.Token!);
|
||||||
|
}
|
||||||
|
UiSharedService.AttachToolTip("Envoyer une invitation Umbra");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_uiSharedService.IconText(FontAwesomeIcon.QuestionCircle, ImGuiColors.DalamudGrey3);
|
||||||
|
UiSharedService.AttachToolTip("Impossible d'inviter ce joueur");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var inbox = _nearbyPending;
|
||||||
|
if (inbox != null && inbox.Pending.Count > 0)
|
||||||
|
{
|
||||||
|
ImGuiHelpers.ScaledDummy(6);
|
||||||
|
_uiSharedService.BigText("Incoming requests");
|
||||||
|
foreach (var kv in inbox.Pending)
|
||||||
|
{
|
||||||
|
ImGui.AlignTextToFramePadding();
|
||||||
|
ImGui.TextUnformatted($"{kv.Value} [{kv.Key}]");
|
||||||
|
ImGui.SameLine();
|
||||||
|
if (_uiSharedService.IconButton(FontAwesomeIcon.Check))
|
||||||
|
{
|
||||||
|
_ = inbox.AcceptAsync(kv.Key);
|
||||||
|
}
|
||||||
|
UiSharedService.AttachToolTip("Accept and add as pair");
|
||||||
|
ImGui.SameLine();
|
||||||
|
if (_uiSharedService.IconButton(FontAwesomeIcon.Times))
|
||||||
|
{
|
||||||
|
inbox.Remove(kv.Key);
|
||||||
|
}
|
||||||
|
UiSharedService.AttachToolTip("Dismiss request");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
ImGui.Unindent();
|
||||||
|
ImGui.Separator();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ImGui.EndChild();
|
ImGui.EndChild();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -384,7 +510,7 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth()) / 2 - (userSize.X + textSize.X) / 2 - ImGui.GetStyle().ItemSpacing.X / 2);
|
ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth()) / 2 - (userSize.X + textSize.X) / 2 - ImGui.GetStyle().ItemSpacing.X / 2);
|
||||||
if (!printShard) ImGui.AlignTextToFramePadding();
|
if (!printShard) ImGui.AlignTextToFramePadding();
|
||||||
ImGui.TextColored(ImGuiColors.ParsedBlue, userCount);
|
ImGui.TextColored(UiSharedService.AccentColor, userCount);
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
if (!printShard) ImGui.AlignTextToFramePadding();
|
if (!printShard) ImGui.AlignTextToFramePadding();
|
||||||
ImGui.TextUnformatted("Users Online");
|
ImGui.TextUnformatted("Users Online");
|
||||||
@@ -407,8 +533,9 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
ImGui.SetCursorPosY(ImGui.GetCursorPosY() - ((userSize.Y + textSize.Y) / 2 + shardTextSize.Y) / 2 - ImGui.GetStyle().ItemSpacing.Y + buttonSize.Y / 2);
|
ImGui.SetCursorPosY(ImGui.GetCursorPosY() - ((userSize.Y + textSize.Y) / 2 + shardTextSize.Y) / 2 - ImGui.GetStyle().ItemSpacing.Y + buttonSize.Y / 2);
|
||||||
}
|
}
|
||||||
var color = UiSharedService.GetBoolColor(!_serverManager.CurrentServer!.FullPause);
|
var isLinked = !_serverManager.CurrentServer!.FullPause;
|
||||||
var connectedIcon = !_serverManager.CurrentServer.FullPause ? FontAwesomeIcon.Link : FontAwesomeIcon.Unlink;
|
var color = isLinked ? new Vector4(0.63f, 0.25f, 1f, 1f) : UiSharedService.GetBoolColor(isLinked);
|
||||||
|
var connectedIcon = isLinked ? FontAwesomeIcon.Link : FontAwesomeIcon.Unlink;
|
||||||
|
|
||||||
if (_apiController.ServerState is ServerState.Connected)
|
if (_apiController.ServerState is ServerState.Connected)
|
||||||
{
|
{
|
||||||
@@ -483,22 +610,19 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
ImGui.SameLine(WindowContentWidth - textSize.X);
|
ImGui.SameLine(WindowContentWidth - textSize.X);
|
||||||
ImGui.TextUnformatted(downloadText);
|
ImGui.TextUnformatted(downloadText);
|
||||||
}
|
}
|
||||||
|
var spacing = ImGui.GetStyle().ItemSpacing.X;
|
||||||
var bottomButtonWidth = (WindowContentWidth - ImGui.GetStyle().ItemSpacing.X) / 2;
|
var bottomButtonWidth = (WindowContentWidth - spacing) / 2f;
|
||||||
|
|
||||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.PersonCircleQuestion, "Character Analysis", bottomButtonWidth))
|
if (_uiSharedService.IconTextButton(FontAwesomeIcon.PersonCircleQuestion, "Character Analysis", bottomButtonWidth))
|
||||||
{
|
{
|
||||||
Mediator.Publish(new UiToggleMessage(typeof(DataAnalysisUi)));
|
Mediator.Publish(new UiToggleMessage(typeof(DataAnalysisUi)));
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
|
|
||||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Running, "Character Data Hub", bottomButtonWidth))
|
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Running, "Character Data Hub", bottomButtonWidth))
|
||||||
{
|
{
|
||||||
Mediator.Publish(new UiToggleMessage(typeof(CharaDataHubUi)));
|
Mediator.Publish(new UiToggleMessage(typeof(CharaDataHubUi)));
|
||||||
}
|
}
|
||||||
|
ImGuiHelpers.ScaledDummy(2);
|
||||||
ImGui.SameLine();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawUIDHeader()
|
private void DrawUIDHeader()
|
||||||
@@ -524,7 +648,7 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
UiSharedService.AttachToolTip("Open the UmbraSync Settings");
|
UiSharedService.AttachToolTip("Open the UmbraSync Settings");
|
||||||
|
|
||||||
ImGui.SameLine(); //Important to draw the uidText consistently
|
ImGui.SameLine();
|
||||||
ImGui.SetCursorPos(originalPos);
|
ImGui.SetCursorPos(originalPos);
|
||||||
|
|
||||||
if (_apiController.ServerState is ServerState.Connected)
|
if (_apiController.ServerState is ServerState.Connected)
|
||||||
@@ -592,7 +716,7 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
ServerState.Connecting => ImGuiColors.DalamudYellow,
|
ServerState.Connecting => ImGuiColors.DalamudYellow,
|
||||||
ServerState.Reconnecting => ImGuiColors.DalamudRed,
|
ServerState.Reconnecting => ImGuiColors.DalamudRed,
|
||||||
ServerState.Connected => new Vector4(0.2f, 0.6f, 1f, 1f), // custom blue
|
ServerState.Connected => new Vector4(0.63f, 0.25f, 1f, 1f), // custom violet
|
||||||
ServerState.Disconnected => ImGuiColors.DalamudYellow,
|
ServerState.Disconnected => ImGuiColors.DalamudYellow,
|
||||||
ServerState.Disconnecting => ImGuiColors.DalamudYellow,
|
ServerState.Disconnecting => ImGuiColors.DalamudYellow,
|
||||||
ServerState.Unauthorized => ImGuiColors.DalamudRed,
|
ServerState.Unauthorized => ImGuiColors.DalamudRed,
|
||||||
@@ -634,4 +758,4 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
_wasOpen = IsOpen;
|
_wasOpen = IsOpen;
|
||||||
IsOpen = false;
|
IsOpen = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Dalamud.Bindings.ImGui;
|
using System.Numerics;
|
||||||
|
using Dalamud.Bindings.ImGui;
|
||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
using Dalamud.Interface.Colors;
|
using Dalamud.Interface.Colors;
|
||||||
using Dalamud.Interface.Utility;
|
using Dalamud.Interface.Utility;
|
||||||
@@ -38,15 +39,15 @@ public class DrawGroupPair : DrawPairBase
|
|||||||
var entryIsMod = _fullInfoDto.GroupPairStatusInfo.IsModerator();
|
var entryIsMod = _fullInfoDto.GroupPairStatusInfo.IsModerator();
|
||||||
var entryIsOwner = string.Equals(_pair.UserData.UID, _group.OwnerUID, StringComparison.Ordinal);
|
var entryIsOwner = string.Equals(_pair.UserData.UID, _group.OwnerUID, StringComparison.Ordinal);
|
||||||
var entryIsPinned = _fullInfoDto.GroupPairStatusInfo.IsPinned();
|
var entryIsPinned = _fullInfoDto.GroupPairStatusInfo.IsPinned();
|
||||||
var presenceIcon = _pair.IsVisible ? FontAwesomeIcon.Eye : (_pair.IsOnline ? FontAwesomeIcon.Link : FontAwesomeIcon.Unlink);
|
var presenceIcon = _pair.IsVisible ? FontAwesomeIcon.Eye : FontAwesomeIcon.CloudMoon;
|
||||||
var presenceColor = (_pair.IsOnline || _pair.IsVisible) ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed;
|
var presenceColor = (_pair.IsOnline || _pair.IsVisible) ? new Vector4(0.63f, 0.25f, 1f, 1f) : ImGuiColors.DalamudGrey;
|
||||||
var presenceText = entryUID + " is offline";
|
var presenceText = entryUID + " is offline";
|
||||||
|
|
||||||
ImGui.SetCursorPosY(textPosY);
|
ImGui.SetCursorPosY(textPosY);
|
||||||
|
bool drewPrefixIcon = false;
|
||||||
|
|
||||||
if (_pair.IsPaused)
|
if (_pair.IsPaused)
|
||||||
{
|
{
|
||||||
presenceIcon = FontAwesomeIcon.Question;
|
|
||||||
presenceColor = ImGuiColors.DalamudGrey;
|
|
||||||
presenceText = entryUID + " online status is unknown (paused)";
|
presenceText = entryUID + " online status is unknown (paused)";
|
||||||
|
|
||||||
ImGui.PushFont(UiBuilder.IconFont);
|
ImGui.PushFont(UiBuilder.IconFont);
|
||||||
@@ -54,24 +55,32 @@ public class DrawGroupPair : DrawPairBase
|
|||||||
ImGui.PopFont();
|
ImGui.PopFont();
|
||||||
|
|
||||||
UiSharedService.AttachToolTip("Pairing status with " + entryUID + " is paused");
|
UiSharedService.AttachToolTip("Pairing status with " + entryUID + " is paused");
|
||||||
|
drewPrefixIcon = true;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
ImGui.PushFont(UiBuilder.IconFont);
|
bool individuallyPaired = _pair.UserPair != null;
|
||||||
UiSharedService.ColorText(FontAwesomeIcon.Check.ToIconString(), ImGuiColors.ParsedGreen);
|
var violet = new Vector4(0.63f, 0.25f, 1f, 1f);
|
||||||
ImGui.PopFont();
|
if (individuallyPaired && (_pair.IsOnline || _pair.IsVisible))
|
||||||
|
{
|
||||||
UiSharedService.AttachToolTip("You are paired with " + entryUID);
|
ImGui.PushFont(UiBuilder.IconFont);
|
||||||
|
UiSharedService.ColorText(FontAwesomeIcon.Moon.ToIconString(), violet);
|
||||||
|
ImGui.PopFont();
|
||||||
|
UiSharedService.AttachToolTip("You are individually paired with " + entryUID);
|
||||||
|
drewPrefixIcon = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
if (drewPrefixIcon)
|
||||||
|
ImGui.SameLine();
|
||||||
|
|
||||||
if (_pair.IsOnline && !_pair.IsVisible) presenceText = entryUID + " is online";
|
|
||||||
else if (_pair.IsOnline && _pair.IsVisible) presenceText = entryUID + " is visible: " + _pair.PlayerName + Environment.NewLine + "Click to target this player";
|
|
||||||
|
|
||||||
ImGui.SameLine();
|
|
||||||
ImGui.SetCursorPosY(textPosY);
|
ImGui.SetCursorPosY(textPosY);
|
||||||
ImGui.PushFont(UiBuilder.IconFont);
|
ImGui.PushFont(UiBuilder.IconFont);
|
||||||
UiSharedService.ColorText(presenceIcon.ToIconString(), presenceColor);
|
UiSharedService.ColorText(presenceIcon.ToIconString(), presenceColor);
|
||||||
ImGui.PopFont();
|
ImGui.PopFont();
|
||||||
|
|
||||||
|
if (_pair.IsOnline && !_pair.IsVisible) presenceText = entryUID + " is online";
|
||||||
|
else if (_pair.IsOnline && _pair.IsVisible) presenceText = entryUID + " is visible: " + _pair.PlayerName + Environment.NewLine + "Click to target this player";
|
||||||
|
|
||||||
if (_pair.IsVisible)
|
if (_pair.IsVisible)
|
||||||
{
|
{
|
||||||
if (ImGui.IsItemClicked())
|
if (ImGui.IsItemClicked())
|
||||||
@@ -94,6 +103,7 @@ public class DrawGroupPair : DrawPairBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
UiSharedService.AttachToolTip(presenceText);
|
UiSharedService.AttachToolTip(presenceText);
|
||||||
|
|
||||||
if (entryIsOwner)
|
if (entryIsOwner)
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ namespace MareSynchronos.UI.Components;
|
|||||||
|
|
||||||
public class DrawUserPair : DrawPairBase
|
public class DrawUserPair : DrawPairBase
|
||||||
{
|
{
|
||||||
|
private static readonly Vector4 Violet = new(0.63f, 0.25f, 1f, 1f);
|
||||||
protected readonly MareMediator _mediator;
|
protected readonly MareMediator _mediator;
|
||||||
private readonly SelectGroupForPairUi _selectGroupForPairUi;
|
private readonly SelectGroupForPairUi _selectGroupForPairUi;
|
||||||
private readonly CharaDataManager _charaDataManager;
|
private readonly CharaDataManager _charaDataManager;
|
||||||
@@ -38,39 +39,40 @@ public class DrawUserPair : DrawPairBase
|
|||||||
|
|
||||||
protected override void DrawLeftSide(float textPosY, float originalY)
|
protected override void DrawLeftSide(float textPosY, float originalY)
|
||||||
{
|
{
|
||||||
FontAwesomeIcon connectionIcon;
|
var online = _pair.IsOnline;
|
||||||
Vector4 connectionColor;
|
var offlineGrey = ImGuiColors.DalamudGrey3;
|
||||||
string connectionText;
|
|
||||||
if (!(_pair.UserPair!.OwnPermissions.IsPaired() && _pair.UserPair!.OtherPermissions.IsPaired()))
|
|
||||||
{
|
|
||||||
connectionIcon = FontAwesomeIcon.ArrowUp;
|
|
||||||
connectionText = _pair.UserData.AliasOrUID + " has not added you back";
|
|
||||||
connectionColor = ImGuiColors.DalamudRed;
|
|
||||||
}
|
|
||||||
else if (_pair.UserPair!.OwnPermissions.IsPaused() || _pair.UserPair!.OtherPermissions.IsPaused())
|
|
||||||
{
|
|
||||||
connectionIcon = FontAwesomeIcon.PauseCircle;
|
|
||||||
connectionText = "Pairing status with " + _pair.UserData.AliasOrUID + " is paused";
|
|
||||||
connectionColor = ImGuiColors.DalamudYellow;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
connectionIcon = FontAwesomeIcon.Check;
|
|
||||||
connectionText = "You are paired with " + _pair.UserData.AliasOrUID;
|
|
||||||
connectionColor = ImGuiColors.ParsedGreen;
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.SetCursorPosY(textPosY);
|
ImGui.SetCursorPosY(textPosY);
|
||||||
ImGui.PushFont(UiBuilder.IconFont);
|
ImGui.PushFont(UiBuilder.IconFont);
|
||||||
UiSharedService.ColorText(connectionIcon.ToIconString(), connectionColor);
|
UiSharedService.ColorText(FontAwesomeIcon.Moon.ToIconString(), online ? Violet : offlineGrey);
|
||||||
ImGui.PopFont();
|
ImGui.PopFont();
|
||||||
UiSharedService.AttachToolTip(connectionText);
|
UiSharedService.AttachToolTip(online
|
||||||
|
? "User is online"
|
||||||
|
: "User is offline");
|
||||||
|
if (!(_pair.UserPair!.OwnPermissions.IsPaired() && _pair.UserPair!.OtherPermissions.IsPaired()))
|
||||||
|
{
|
||||||
|
ImGui.SameLine();
|
||||||
|
ImGui.SetCursorPosY(textPosY);
|
||||||
|
ImGui.PushFont(UiBuilder.IconFont);
|
||||||
|
UiSharedService.ColorText(FontAwesomeIcon.ArrowUp.ToIconString(), ImGuiColors.DalamudRed);
|
||||||
|
ImGui.PopFont();
|
||||||
|
UiSharedService.AttachToolTip(_pair.UserData.AliasOrUID + " has not added you back");
|
||||||
|
}
|
||||||
|
else if (_pair.UserPair!.OwnPermissions.IsPaused() || _pair.UserPair!.OtherPermissions.IsPaused())
|
||||||
|
{
|
||||||
|
ImGui.SameLine();
|
||||||
|
ImGui.SetCursorPosY(textPosY);
|
||||||
|
ImGui.PushFont(UiBuilder.IconFont);
|
||||||
|
UiSharedService.ColorText(FontAwesomeIcon.PauseCircle.ToIconString(), ImGuiColors.DalamudYellow);
|
||||||
|
ImGui.PopFont();
|
||||||
|
UiSharedService.AttachToolTip("Pairing with " + _pair.UserData.AliasOrUID + " is paused");
|
||||||
|
}
|
||||||
if (_pair is { IsOnline: true, IsVisible: true })
|
if (_pair is { IsOnline: true, IsVisible: true })
|
||||||
{
|
{
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
ImGui.SetCursorPosY(textPosY);
|
ImGui.SetCursorPosY(textPosY);
|
||||||
ImGui.PushFont(UiBuilder.IconFont);
|
ImGui.PushFont(UiBuilder.IconFont);
|
||||||
UiSharedService.ColorText(FontAwesomeIcon.Eye.ToIconString(), ImGuiColors.ParsedGreen);
|
UiSharedService.ColorText(FontAwesomeIcon.Eye.ToIconString(), Violet);
|
||||||
if (ImGui.IsItemClicked())
|
if (ImGui.IsItemClicked())
|
||||||
{
|
{
|
||||||
_mediator.Publish(new TargetPairMessage(_pair));
|
_mediator.Publish(new TargetPairMessage(_pair));
|
||||||
@@ -141,8 +143,6 @@ public class DrawUserPair : DrawPairBase
|
|||||||
var individualSoundsDisabled = (_pair.UserPair?.OwnPermissions.IsDisableSounds() ?? false) || (_pair.UserPair?.OtherPermissions.IsDisableSounds() ?? false);
|
var individualSoundsDisabled = (_pair.UserPair?.OwnPermissions.IsDisableSounds() ?? false) || (_pair.UserPair?.OtherPermissions.IsDisableSounds() ?? false);
|
||||||
var individualAnimDisabled = (_pair.UserPair?.OwnPermissions.IsDisableAnimations() ?? false) || (_pair.UserPair?.OtherPermissions.IsDisableAnimations() ?? false);
|
var individualAnimDisabled = (_pair.UserPair?.OwnPermissions.IsDisableAnimations() ?? false) || (_pair.UserPair?.OtherPermissions.IsDisableAnimations() ?? false);
|
||||||
var individualVFXDisabled = (_pair.UserPair?.OwnPermissions.IsDisableVFX() ?? false) || (_pair.UserPair?.OtherPermissions.IsDisableVFX() ?? false);
|
var individualVFXDisabled = (_pair.UserPair?.OwnPermissions.IsDisableVFX() ?? false) || (_pair.UserPair?.OtherPermissions.IsDisableVFX() ?? false);
|
||||||
|
|
||||||
// Icon for individually applied permissions
|
|
||||||
if (individualSoundsDisabled || individualAnimDisabled || individualVFXDisabled)
|
if (individualSoundsDisabled || individualAnimDisabled || individualVFXDisabled)
|
||||||
{
|
{
|
||||||
var icon = FontAwesomeIcon.ExclamationTriangle;
|
var icon = FontAwesomeIcon.ExclamationTriangle;
|
||||||
@@ -197,8 +197,6 @@ public class DrawUserPair : DrawPairBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Icon for shared character data
|
|
||||||
if (_charaDataManager.SharedWithYouData.TryGetValue(_pair.UserData, out var sharedData))
|
if (_charaDataManager.SharedWithYouData.TryGetValue(_pair.UserData, out var sharedData))
|
||||||
{
|
{
|
||||||
var icon = FontAwesomeIcon.Running;
|
var icon = FontAwesomeIcon.Running;
|
||||||
@@ -303,4 +301,4 @@ public class DrawUserPair : DrawPairBase
|
|||||||
}
|
}
|
||||||
UiSharedService.AttachToolTip("Hold CTRL and click to unpair permanently from " + entryUID);
|
UiSharedService.AttachToolTip("Hold CTRL and click to unpair permanently from " + entryUID);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using Dalamud.Bindings.ImGui;
|
using Dalamud.Bindings.ImGui;
|
||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
using Dalamud.Interface.Colors;
|
using Dalamud.Interface.Colors;
|
||||||
using Dalamud.Interface.Utility;
|
using Dalamud.Interface.Utility;
|
||||||
@@ -17,6 +17,7 @@ using MareSynchronos.Services.ServerConfiguration;
|
|||||||
using MareSynchronos.UI.Components;
|
using MareSynchronos.UI.Components;
|
||||||
using MareSynchronos.UI.Handlers;
|
using MareSynchronos.UI.Handlers;
|
||||||
using MareSynchronos.WebAPI;
|
using MareSynchronos.WebAPI;
|
||||||
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
|
||||||
@@ -40,6 +41,7 @@ internal sealed class GroupPanel
|
|||||||
private string _editGroupComment = string.Empty;
|
private string _editGroupComment = string.Empty;
|
||||||
private string _editGroupEntry = string.Empty;
|
private string _editGroupEntry = string.Empty;
|
||||||
private bool _errorGroupCreate = false;
|
private bool _errorGroupCreate = false;
|
||||||
|
private string _errorGroupCreateMessage = string.Empty;
|
||||||
private bool _errorGroupJoin;
|
private bool _errorGroupJoin;
|
||||||
private bool _isPasswordValid;
|
private bool _isPasswordValid;
|
||||||
private GroupPasswordDto? _lastCreatedGroup = null;
|
private GroupPasswordDto? _lastCreatedGroup = null;
|
||||||
@@ -52,6 +54,21 @@ internal sealed class GroupPanel
|
|||||||
private bool _showModalChangePassword;
|
private bool _showModalChangePassword;
|
||||||
private bool _showModalCreateGroup;
|
private bool _showModalCreateGroup;
|
||||||
private bool _showModalEnterPassword;
|
private bool _showModalEnterPassword;
|
||||||
|
private string _newSyncShellAlias = string.Empty;
|
||||||
|
private bool _createIsTemporary = false;
|
||||||
|
private int _tempSyncshellDurationHours = 24;
|
||||||
|
private readonly int[] _temporaryDurationOptions = new[]
|
||||||
|
{
|
||||||
|
1,
|
||||||
|
12,
|
||||||
|
24,
|
||||||
|
48,
|
||||||
|
72,
|
||||||
|
96,
|
||||||
|
120,
|
||||||
|
144,
|
||||||
|
168
|
||||||
|
};
|
||||||
private string _syncShellPassword = string.Empty;
|
private string _syncShellPassword = string.Empty;
|
||||||
private string _syncShellToJoin = string.Empty;
|
private string _syncShellToJoin = string.Empty;
|
||||||
|
|
||||||
@@ -82,7 +99,7 @@ internal sealed class GroupPanel
|
|||||||
{
|
{
|
||||||
var buttonSize = _uiShared.GetIconButtonSize(FontAwesomeIcon.Plus);
|
var buttonSize = _uiShared.GetIconButtonSize(FontAwesomeIcon.Plus);
|
||||||
ImGui.SetNextItemWidth(UiSharedService.GetWindowContentRegionWidth() - ImGui.GetWindowContentRegionMin().X - buttonSize.X);
|
ImGui.SetNextItemWidth(UiSharedService.GetWindowContentRegionWidth() - ImGui.GetWindowContentRegionMin().X - buttonSize.X);
|
||||||
ImGui.InputTextWithHint("##syncshellid", "Syncshell GID/Alias (leave empty to create)", ref _syncShellToJoin, 20);
|
ImGui.InputTextWithHint("##syncshellid", "Syncshell GID/Alias (leave empty to create)", ref _syncShellToJoin, 50);
|
||||||
ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth() - buttonSize.X);
|
ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth() - buttonSize.X);
|
||||||
|
|
||||||
bool userCanJoinMoreGroups = _pairManager.GroupPairs.Count < ApiController.ServerInfo.MaxGroupsJoinedByUser;
|
bool userCanJoinMoreGroups = _pairManager.GroupPairs.Count < ApiController.ServerInfo.MaxGroupsJoinedByUser;
|
||||||
@@ -108,6 +125,10 @@ internal sealed class GroupPanel
|
|||||||
{
|
{
|
||||||
_lastCreatedGroup = null;
|
_lastCreatedGroup = null;
|
||||||
_errorGroupCreate = false;
|
_errorGroupCreate = false;
|
||||||
|
_newSyncShellAlias = string.Empty;
|
||||||
|
_createIsTemporary = false;
|
||||||
|
_tempSyncshellDurationHours = 24;
|
||||||
|
_errorGroupCreateMessage = string.Empty;
|
||||||
_showModalCreateGroup = true;
|
_showModalCreateGroup = true;
|
||||||
ImGui.OpenPopup("Create Syncshell");
|
ImGui.OpenPopup("Create Syncshell");
|
||||||
}
|
}
|
||||||
@@ -150,18 +171,97 @@ internal sealed class GroupPanel
|
|||||||
|
|
||||||
if (ImGui.BeginPopupModal("Create Syncshell", ref _showModalCreateGroup, UiSharedService.PopupWindowFlags))
|
if (ImGui.BeginPopupModal("Create Syncshell", ref _showModalCreateGroup, UiSharedService.PopupWindowFlags))
|
||||||
{
|
{
|
||||||
UiSharedService.TextWrapped("Press the button below to create a new Syncshell.");
|
UiSharedService.TextWrapped("Choisissez le type de Syncshell à créer.");
|
||||||
|
bool showPermanent = !_createIsTemporary;
|
||||||
|
if (ImGui.RadioButton("Permanente", showPermanent))
|
||||||
|
{
|
||||||
|
_createIsTemporary = false;
|
||||||
|
}
|
||||||
|
ImGui.SameLine();
|
||||||
|
if (ImGui.RadioButton("Temporaire", _createIsTemporary))
|
||||||
|
{
|
||||||
|
_createIsTemporary = true;
|
||||||
|
_newSyncShellAlias = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_createIsTemporary)
|
||||||
|
{
|
||||||
|
UiSharedService.TextWrapped("Donnez un nom à votre Syncshell (optionnel) puis créez-la.");
|
||||||
|
ImGui.SetNextItemWidth(-1);
|
||||||
|
ImGui.InputTextWithHint("##syncshellalias", "Nom du Syncshell", ref _newSyncShellAlias, 50);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_newSyncShellAlias = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_createIsTemporary)
|
||||||
|
{
|
||||||
|
UiSharedService.TextWrapped("Durée maximale d'une Syncshell temporaire : 7 jours.");
|
||||||
|
if (_tempSyncshellDurationHours > 168) _tempSyncshellDurationHours = 168;
|
||||||
|
for (int i = 0; i < _temporaryDurationOptions.Length; i++)
|
||||||
|
{
|
||||||
|
var option = _temporaryDurationOptions[i];
|
||||||
|
var isSelected = _tempSyncshellDurationHours == option;
|
||||||
|
string label = option switch
|
||||||
|
{
|
||||||
|
>= 24 when option % 24 == 0 => option == 24 ? "24h" : $"{option / 24}j",
|
||||||
|
_ => option + "h"
|
||||||
|
};
|
||||||
|
|
||||||
|
if (ImGui.RadioButton(label, isSelected))
|
||||||
|
{
|
||||||
|
_tempSyncshellDurationHours = option;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start a new line after every 3 buttons
|
||||||
|
if ((i + 1) % 3 == 0)
|
||||||
|
{
|
||||||
|
ImGui.NewLine();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ImGui.SameLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var expiresLocal = DateTime.Now.AddHours(_tempSyncshellDurationHours);
|
||||||
|
UiSharedService.TextWrapped($"Expiration le {expiresLocal:g} (heure locale).");
|
||||||
|
}
|
||||||
|
|
||||||
|
UiSharedService.TextWrapped("Appuyez sur le bouton ci-dessous pour créer une nouvelle Syncshell.");
|
||||||
ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale);
|
ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale);
|
||||||
if (ImGui.Button("Create Syncshell"))
|
if (ImGui.Button("Create Syncshell"))
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_lastCreatedGroup = ApiController.GroupCreate().Result;
|
if (_createIsTemporary)
|
||||||
|
{
|
||||||
|
var expiresAtUtc = DateTime.UtcNow.AddHours(_tempSyncshellDurationHours);
|
||||||
|
_lastCreatedGroup = ApiController.GroupCreateTemporary(expiresAtUtc).Result;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var aliasInput = string.IsNullOrWhiteSpace(_newSyncShellAlias) ? null : _newSyncShellAlias.Trim();
|
||||||
|
_lastCreatedGroup = ApiController.GroupCreate(aliasInput).Result;
|
||||||
|
if (_lastCreatedGroup != null)
|
||||||
|
{
|
||||||
|
_newSyncShellAlias = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_lastCreatedGroup = null;
|
_lastCreatedGroup = null;
|
||||||
_errorGroupCreate = true;
|
_errorGroupCreate = true;
|
||||||
|
if (ex.Message.Contains("name is already in use", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
_errorGroupCreateMessage = "Le nom de la Syncshell est déjà utilisé.";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_errorGroupCreateMessage = ex.Message;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,6 +269,11 @@ internal sealed class GroupPanel
|
|||||||
{
|
{
|
||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
_errorGroupCreate = false;
|
_errorGroupCreate = false;
|
||||||
|
_errorGroupCreateMessage = string.Empty;
|
||||||
|
if (!string.IsNullOrWhiteSpace(_lastCreatedGroup.Group.Alias))
|
||||||
|
{
|
||||||
|
ImGui.TextUnformatted("Syncshell Name: " + _lastCreatedGroup.Group.Alias);
|
||||||
|
}
|
||||||
ImGui.TextUnformatted("Syncshell ID: " + _lastCreatedGroup.Group.GID);
|
ImGui.TextUnformatted("Syncshell ID: " + _lastCreatedGroup.Group.GID);
|
||||||
ImGui.AlignTextToFramePadding();
|
ImGui.AlignTextToFramePadding();
|
||||||
ImGui.TextUnformatted("Syncshell Password: " + _lastCreatedGroup.Password);
|
ImGui.TextUnformatted("Syncshell Password: " + _lastCreatedGroup.Password);
|
||||||
@@ -178,12 +283,19 @@ internal sealed class GroupPanel
|
|||||||
ImGui.SetClipboardText(_lastCreatedGroup.Password);
|
ImGui.SetClipboardText(_lastCreatedGroup.Password);
|
||||||
}
|
}
|
||||||
UiSharedService.TextWrapped("You can change the Syncshell password later at any time.");
|
UiSharedService.TextWrapped("You can change the Syncshell password later at any time.");
|
||||||
|
if (_lastCreatedGroup.IsTemporary && _lastCreatedGroup.ExpiresAt != null)
|
||||||
|
{
|
||||||
|
var expiresLocal = _lastCreatedGroup.ExpiresAt.Value.ToLocalTime();
|
||||||
|
UiSharedService.TextWrapped($"Cette Syncshell expirera le {expiresLocal:g} (heure locale).");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_errorGroupCreate)
|
if (_errorGroupCreate)
|
||||||
{
|
{
|
||||||
UiSharedService.ColorTextWrapped("You are already owner of the maximum amount of Syncshells (3) or joined the maximum amount of Syncshells (6). Relinquish ownership of your own Syncshells to someone else or leave existing Syncshells.",
|
var msg = string.IsNullOrWhiteSpace(_errorGroupCreateMessage)
|
||||||
new Vector4(1, 0, 0, 1));
|
? "You are already owner of the maximum amount of Syncshells (3) or joined the maximum amount of Syncshells (6). Relinquish ownership of your own Syncshells to someone else or leave existing Syncshells."
|
||||||
|
: _errorGroupCreateMessage;
|
||||||
|
UiSharedService.ColorTextWrapped(msg, new Vector4(1, 0, 0, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
UiSharedService.SetScaledWindowSize(350);
|
UiSharedService.SetScaledWindowSize(350);
|
||||||
@@ -243,11 +355,13 @@ internal sealed class GroupPanel
|
|||||||
if (!string.Equals(_editGroupEntry, groupDto.GID, StringComparison.Ordinal))
|
if (!string.Equals(_editGroupEntry, groupDto.GID, StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
var shellConfig = _serverConfigurationManager.GetShellConfigForGid(groupDto.GID);
|
var shellConfig = _serverConfigurationManager.GetShellConfigForGid(groupDto.GID);
|
||||||
if (!_mareConfig.Current.DisableSyncshellChat && shellConfig.Enabled)
|
var totalMembers = pairsInGroup.Count + 1;
|
||||||
{
|
var connectedMembers = pairsInGroup.Count(p => p.IsOnline) + 1;
|
||||||
ImGui.TextUnformatted($"[{shellNumber}]");
|
var maxCapacity = ApiController.ServerInfo.MaxGroupUserCount;
|
||||||
UiSharedService.AttachToolTip("Chat command prefix: /ss" + shellNumber);
|
ImGui.TextUnformatted($"{connectedMembers}/{totalMembers}");
|
||||||
}
|
UiSharedService.AttachToolTip("Membres connectés / membres totaux" + Environment.NewLine +
|
||||||
|
$"Capacité maximale : {maxCapacity}" + Environment.NewLine +
|
||||||
|
"Syncshell ID: " + groupDto.Group.GID);
|
||||||
if (textIsGid) ImGui.PushFont(UiBuilder.MonoFont);
|
if (textIsGid) ImGui.PushFont(UiBuilder.MonoFont);
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
ImGui.TextUnformatted(groupName);
|
ImGui.TextUnformatted(groupName);
|
||||||
@@ -255,6 +369,20 @@ internal sealed class GroupPanel
|
|||||||
UiSharedService.AttachToolTip("Left click to switch between GID display and comment" + Environment.NewLine +
|
UiSharedService.AttachToolTip("Left click to switch between GID display and comment" + Environment.NewLine +
|
||||||
"Right click to change comment for " + groupName + Environment.NewLine + Environment.NewLine
|
"Right click to change comment for " + groupName + Environment.NewLine + Environment.NewLine
|
||||||
+ "Users: " + (pairsInGroup.Count + 1) + ", Owner: " + groupDto.OwnerAliasOrUID);
|
+ "Users: " + (pairsInGroup.Count + 1) + ", Owner: " + groupDto.OwnerAliasOrUID);
|
||||||
|
if (groupDto.IsTemporary)
|
||||||
|
{
|
||||||
|
ImGui.SameLine();
|
||||||
|
UiSharedService.ColorText("(Temp)", ImGuiColors.DalamudOrange);
|
||||||
|
if (groupDto.ExpiresAt != null)
|
||||||
|
{
|
||||||
|
var tempExpireLocal = groupDto.ExpiresAt.Value.ToLocalTime();
|
||||||
|
UiSharedService.AttachToolTip($"Expire le {tempExpireLocal:g}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
UiSharedService.AttachToolTip("Syncshell temporaire");
|
||||||
|
}
|
||||||
|
}
|
||||||
if (ImGui.IsItemClicked(ImGuiMouseButton.Left))
|
if (ImGui.IsItemClicked(ImGuiMouseButton.Left))
|
||||||
{
|
{
|
||||||
var prevState = textIsGid;
|
var prevState = textIsGid;
|
||||||
@@ -464,7 +592,9 @@ internal sealed class GroupPanel
|
|||||||
|
|
||||||
if (offlineUsers.Count > 0)
|
if (offlineUsers.Count > 0)
|
||||||
{
|
{
|
||||||
|
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey);
|
||||||
ImGui.TextUnformatted("Offline/Unknown");
|
ImGui.TextUnformatted("Offline/Unknown");
|
||||||
|
ImGui.PopStyleColor();
|
||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
if (hideOfflineUsers)
|
if (hideOfflineUsers)
|
||||||
{
|
{
|
||||||
@@ -700,4 +830,4 @@ internal sealed class GroupPanel
|
|||||||
}
|
}
|
||||||
ImGui.EndChild();
|
ImGui.EndChild();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,17 +54,12 @@ public class PairGroupsUi
|
|||||||
ImGui.SameLine(buttonPauseOffset);
|
ImGui.SameLine(buttonPauseOffset);
|
||||||
if (_uiSharedService.IconButton(pauseButton))
|
if (_uiSharedService.IconButton(pauseButton))
|
||||||
{
|
{
|
||||||
// If all of the currently visible pairs (after applying filters to the pairs)
|
|
||||||
// are paused we display a resume button to resume all currently visible (after filters)
|
|
||||||
// pairs. Otherwise, we just pause all the remaining pairs.
|
|
||||||
if (allArePaused)
|
if (allArePaused)
|
||||||
{
|
{
|
||||||
// If all are paused => resume all
|
|
||||||
ResumeAllPairs(availablePairsInThisTag);
|
ResumeAllPairs(availablePairsInThisTag);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// otherwise pause all remaining
|
|
||||||
PauseRemainingPairs(availablePairsInThisTag);
|
PauseRemainingPairs(availablePairsInThisTag);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -120,7 +115,6 @@ public class PairGroupsUi
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Avoid uncomfortably close group names
|
|
||||||
if (!_tagHandler.IsTagOpen(tag))
|
if (!_tagHandler.IsTagOpen(tag))
|
||||||
{
|
{
|
||||||
var size = ImGui.CalcTextSize("").Y + ImGui.GetStyle().FramePadding.Y * 2f;
|
var size = ImGui.CalcTextSize("").Y + ImGui.GetStyle().FramePadding.Y * 2f;
|
||||||
@@ -157,14 +151,12 @@ public class PairGroupsUi
|
|||||||
{
|
{
|
||||||
TagHandler.CustomUnpairedTag => "Unpaired",
|
TagHandler.CustomUnpairedTag => "Unpaired",
|
||||||
TagHandler.CustomOfflineTag => "Offline",
|
TagHandler.CustomOfflineTag => "Offline",
|
||||||
TagHandler.CustomOnlineTag => _mareConfig.Current.ShowOfflineUsersSeparately ? "Online/Paused" : "Contacts",
|
TagHandler.CustomOnlineTag => _mareConfig.Current.ShowOfflineUsersSeparately ? "Online" : "Contacts",
|
||||||
TagHandler.CustomVisibleTag => "Visible",
|
TagHandler.CustomVisibleTag => "Visible",
|
||||||
_ => tag
|
_ => tag
|
||||||
};
|
};
|
||||||
|
|
||||||
string resultFolderName = !isSpecialTag ? $"{displayedName} ({visible}/{online}/{total} Pairs)" : $"{displayedName} ({online} Pairs)";
|
string resultFolderName = !isSpecialTag ? $"{displayedName} ({visible}/{online}/{total} Pairs)" : $"{displayedName} ({online} Pairs)";
|
||||||
|
|
||||||
// FontAwesomeIcon.CaretSquareDown : FontAwesomeIcon.CaretSquareRight
|
|
||||||
var icon = _tagHandler.IsTagOpen(tag) ? FontAwesomeIcon.CaretSquareDown : FontAwesomeIcon.CaretSquareRight;
|
var icon = _tagHandler.IsTagOpen(tag) ? FontAwesomeIcon.CaretSquareDown : FontAwesomeIcon.CaretSquareRight;
|
||||||
_uiSharedService.IconText(icon);
|
_uiSharedService.IconText(icon);
|
||||||
if (ImGui.IsItemClicked(ImGuiMouseButton.Left))
|
if (ImGui.IsItemClicked(ImGuiMouseButton.Left))
|
||||||
|
|||||||
@@ -15,20 +15,8 @@ public class SelectGroupForPairUi
|
|||||||
private readonly UidDisplayHandler _uidDisplayHandler;
|
private readonly UidDisplayHandler _uidDisplayHandler;
|
||||||
private readonly UiSharedService _uiSharedService;
|
private readonly UiSharedService _uiSharedService;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The group UI is always open for a specific pair. This defines which pair the UI is open for.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
private Pair? _pair;
|
private Pair? _pair;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Should the panel show, yes/no
|
|
||||||
/// </summary>
|
|
||||||
private bool _show;
|
private bool _show;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// For the add category option, this stores the currently typed in tag name
|
|
||||||
/// </summary>
|
|
||||||
private string _tagNameToAdd = "";
|
private string _tagNameToAdd = "";
|
||||||
|
|
||||||
public SelectGroupForPairUi(TagHandler tagHandler, UidDisplayHandler uidDisplayHandler, UiSharedService uiSharedService)
|
public SelectGroupForPairUi(TagHandler tagHandler, UidDisplayHandler uidDisplayHandler, UiSharedService uiSharedService)
|
||||||
@@ -49,7 +37,6 @@ public class SelectGroupForPairUi
|
|||||||
|
|
||||||
var name = PairName(_pair);
|
var name = PairName(_pair);
|
||||||
var popupName = $"Choose Groups for {name}";
|
var popupName = $"Choose Groups for {name}";
|
||||||
// Is the popup supposed to show but did not open yet? Open it
|
|
||||||
if (_show)
|
if (_show)
|
||||||
{
|
{
|
||||||
ImGui.OpenPopup(popupName);
|
ImGui.OpenPopup(popupName);
|
||||||
@@ -91,10 +78,6 @@ public class SelectGroupForPairUi
|
|||||||
public void Open(Pair pair)
|
public void Open(Pair pair)
|
||||||
{
|
{
|
||||||
_pair = pair;
|
_pair = pair;
|
||||||
// Using "_show" here to de-couple the opening of the popup
|
|
||||||
// The popup name is derived from the name the user currently sees, which is
|
|
||||||
// based on the showUidForEntry dictionary.
|
|
||||||
// We'd have to derive the name here to open it popup modal here, when the Open() is called
|
|
||||||
_show = true;
|
_show = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -163,13 +163,13 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
|||||||
UiSharedService.Color(0, 0, 0, transparency), 1);
|
UiSharedService.Color(0, 0, 0, transparency), 1);
|
||||||
drawList.AddRectFilled(dlBarStart with { X = dlBarStart.X - dlBarBorder, Y = dlBarStart.Y - dlBarBorder },
|
drawList.AddRectFilled(dlBarStart with { X = dlBarStart.X - dlBarBorder, Y = dlBarStart.Y - dlBarBorder },
|
||||||
dlBarEnd with { X = dlBarEnd.X + dlBarBorder, Y = dlBarEnd.Y + dlBarBorder },
|
dlBarEnd with { X = dlBarEnd.X + dlBarBorder, Y = dlBarEnd.Y + dlBarBorder },
|
||||||
UiSharedService.Color(220, 220, 255, transparency), 1);
|
UiSharedService.Color(230, 200, 255, transparency), 1);
|
||||||
drawList.AddRectFilled(dlBarStart, dlBarEnd,
|
drawList.AddRectFilled(dlBarStart, dlBarEnd,
|
||||||
UiSharedService.Color(0, 0, 0, transparency), 1);
|
UiSharedService.Color(0, 0, 0, transparency), 1);
|
||||||
var dlProgressPercent = transferredBytes / (double)totalBytes;
|
var dlProgressPercent = transferredBytes / (double)totalBytes;
|
||||||
drawList.AddRectFilled(dlBarStart,
|
drawList.AddRectFilled(dlBarStart,
|
||||||
dlBarEnd with { X = dlBarStart.X + (float)(dlProgressPercent * dlBarWidth) },
|
dlBarEnd with { X = dlBarStart.X + (float)(dlProgressPercent * dlBarWidth) },
|
||||||
UiSharedService.Color(100, 100, 255, transparency), 1);
|
UiSharedService.Color(160, 64, 255, transparency), 1);
|
||||||
|
|
||||||
if (_configService.Current.TransferBarsShowText)
|
if (_configService.Current.TransferBarsShowText)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
using Dalamud.Game.Text.SeStringHandling;
|
using Dalamud.Game.Text.SeStringHandling;
|
||||||
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
|
using Dalamud.Interface;
|
||||||
using MareSynchronos.MareConfiguration;
|
using MareSynchronos.MareConfiguration;
|
||||||
using MareSynchronos.MareConfiguration.Configurations;
|
using MareSynchronos.MareConfiguration.Configurations;
|
||||||
using MareSynchronos.PlayerData.Pairs;
|
using MareSynchronos.PlayerData.Pairs;
|
||||||
@@ -15,6 +16,7 @@ namespace MareSynchronos.UI;
|
|||||||
|
|
||||||
public sealed class DtrEntry : IDisposable, IHostedService
|
public sealed class DtrEntry : IDisposable, IHostedService
|
||||||
{
|
{
|
||||||
|
public const string DefaultGlyph = "\u25CB";
|
||||||
private enum DtrStyle
|
private enum DtrStyle
|
||||||
{
|
{
|
||||||
Default,
|
Default,
|
||||||
@@ -196,7 +198,8 @@ public sealed class DtrEntry : IDisposable, IHostedService
|
|||||||
{
|
{
|
||||||
var style = (DtrStyle)styleNum;
|
var style = (DtrStyle)styleNum;
|
||||||
|
|
||||||
return style switch {
|
return style switch
|
||||||
|
{
|
||||||
DtrStyle.Style1 => $"\xE039 {text}",
|
DtrStyle.Style1 => $"\xE039 {text}",
|
||||||
DtrStyle.Style2 => $"\xE0BC {text}",
|
DtrStyle.Style2 => $"\xE0BC {text}",
|
||||||
DtrStyle.Style3 => $"\xE0BD {text}",
|
DtrStyle.Style3 => $"\xE0BD {text}",
|
||||||
@@ -206,7 +209,7 @@ public sealed class DtrEntry : IDisposable, IHostedService
|
|||||||
DtrStyle.Style7 => $"\xE05D {text}",
|
DtrStyle.Style7 => $"\xE05D {text}",
|
||||||
DtrStyle.Style8 => $"\xE03C{text}",
|
DtrStyle.Style8 => $"\xE03C{text}",
|
||||||
DtrStyle.Style9 => $"\xE040 {text} \xE041",
|
DtrStyle.Style9 => $"\xE040 {text} \xE041",
|
||||||
_ => $"\uE044 {text}"
|
_ => DefaultGlyph + " " + text
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ 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 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;
|
||||||
private bool? _notesSuccessfullyApplied = null;
|
private bool? _notesSuccessfullyApplied = null;
|
||||||
@@ -211,6 +212,61 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
_configService.Save();
|
_configService.Save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ImGui.Separator();
|
||||||
|
_uiShared.BigText("AutoDetect");
|
||||||
|
bool enableDiscovery = _configService.Current.EnableAutoDetectDiscovery;
|
||||||
|
if (ImGui.Checkbox("Enable Nearby detection (beta)", ref enableDiscovery))
|
||||||
|
{
|
||||||
|
_configService.Current.EnableAutoDetectDiscovery = enableDiscovery;
|
||||||
|
_configService.Save();
|
||||||
|
|
||||||
|
// notify services of toggle
|
||||||
|
Mediator.Publish(new NearbyDetectionToggled(enableDiscovery));
|
||||||
|
|
||||||
|
// if Nearby is turned OFF, force Allow Pair Requests OFF as well
|
||||||
|
if (!enableDiscovery && _configService.Current.AllowAutoDetectPairRequests)
|
||||||
|
{
|
||||||
|
_configService.Current.AllowAutoDetectPairRequests = false;
|
||||||
|
_configService.Save();
|
||||||
|
Mediator.Publish(new AllowPairRequestsToggled(false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow Pair Requests is disabled when Nearby is OFF
|
||||||
|
using (ImRaii.Disabled(!enableDiscovery))
|
||||||
|
{
|
||||||
|
bool allowRequests = _configService.Current.AllowAutoDetectPairRequests;
|
||||||
|
if (ImGui.Checkbox("Allow pair requests", ref allowRequests))
|
||||||
|
{
|
||||||
|
_configService.Current.AllowAutoDetectPairRequests = allowRequests;
|
||||||
|
_configService.Save();
|
||||||
|
|
||||||
|
// notify services of toggle
|
||||||
|
Mediator.Publish(new AllowPairRequestsToggled(allowRequests));
|
||||||
|
|
||||||
|
// user-facing info toast
|
||||||
|
Mediator.Publish(new NotificationMessage(
|
||||||
|
"Nearby Detection",
|
||||||
|
allowRequests ? "Pair requests enabled: others can invite you." : "Pair requests disabled: others cannot invite you.",
|
||||||
|
NotificationType.Info,
|
||||||
|
default));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Radius only available when both Nearby and Allow Pair Requests are ON
|
||||||
|
if (enableDiscovery && _configService.Current.AllowAutoDetectPairRequests)
|
||||||
|
{
|
||||||
|
ImGui.Indent();
|
||||||
|
int maxMeters = _configService.Current.AutoDetectMaxDistanceMeters;
|
||||||
|
ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale);
|
||||||
|
if (ImGui.SliderInt("Max distance (meters)", ref maxMeters, 5, 100))
|
||||||
|
{
|
||||||
|
_configService.Current.AutoDetectMaxDistanceMeters = maxMeters;
|
||||||
|
_configService.Save();
|
||||||
|
}
|
||||||
|
ImGui.Unindent();
|
||||||
|
}
|
||||||
|
|
||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
_uiShared.BigText("Transfer UI");
|
_uiShared.BigText("Transfer UI");
|
||||||
|
|
||||||
@@ -492,7 +548,6 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
if (ImGui.Checkbox($"Enable chat for this syncshell##{gid}", ref shellEnabled))
|
if (ImGui.Checkbox($"Enable chat for this syncshell##{gid}", ref shellEnabled))
|
||||||
{
|
{
|
||||||
// If there is an active group with the same syncshell number, pick a new one
|
|
||||||
int nextNumber = 1;
|
int nextNumber = 1;
|
||||||
bool conflict = false;
|
bool conflict = false;
|
||||||
foreach (var otherGroup in _pairManager.Groups)
|
foreach (var otherGroup in _pairManager.Groups)
|
||||||
@@ -512,17 +567,13 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
using var pushDisabled = ImRaii.Disabled(!shellEnabled);
|
using var pushDisabled = ImRaii.Disabled(!shellEnabled);
|
||||||
|
|
||||||
ImGui.SetNextItemWidth(50 * ImGuiHelpers.GlobalScale);
|
ImGui.SetNextItemWidth(50 * ImGuiHelpers.GlobalScale);
|
||||||
|
|
||||||
// _uiShared.DrawCombo() remembers the selected option -- we don't want that, because the value can change
|
|
||||||
if (ImGui.BeginCombo("Syncshell number##{gid}", $"{shellNumber}"))
|
if (ImGui.BeginCombo("Syncshell number##{gid}", $"{shellNumber}"))
|
||||||
{
|
{
|
||||||
// Same hard-coded number in CommandManagerService
|
|
||||||
for (int i = 1; i <= ChatService.CommandMaxNumber; ++i)
|
for (int i = 1; i <= ChatService.CommandMaxNumber; ++i)
|
||||||
{
|
{
|
||||||
if (ImGui.Selectable($"{i}", i == shellNumber))
|
if (ImGui.Selectable($"{i}", i == shellNumber))
|
||||||
{
|
{
|
||||||
// Find an active group with the same syncshell number as selected, and swap it
|
|
||||||
// This logic can leave duplicate IDs present in the config but its not critical
|
|
||||||
foreach (var otherGroup in _pairManager.Groups)
|
foreach (var otherGroup in _pairManager.Groups)
|
||||||
{
|
{
|
||||||
if (gid.Equals(otherGroup.Key.GID, StringComparison.Ordinal)) continue;
|
if (gid.Equals(otherGroup.Key.GID, StringComparison.Ordinal)) continue;
|
||||||
@@ -597,7 +648,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
ImGui.SameLine(300.0f * ImGuiHelpers.GlobalScale);
|
ImGui.SameLine(300.0f * ImGuiHelpers.GlobalScale);
|
||||||
if (_ipcProvider.ImpersonationActive)
|
if (_ipcProvider.ImpersonationActive)
|
||||||
{
|
{
|
||||||
UiSharedService.ColorTextWrapped("Umbra API active!", ImGuiColors.HealerGreen);
|
UiSharedService.ColorTextWrapped("Umbra API active!", UiSharedService.AccentColor);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -936,7 +987,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
_uiShared.DrawHelpText("If this option is selected all already existing notes for UIDs will be overwritten by the imported notes.");
|
_uiShared.DrawHelpText("If this option is selected all already existing notes for UIDs will be overwritten by the imported notes.");
|
||||||
if (_notesSuccessfullyApplied.HasValue && _notesSuccessfullyApplied.Value)
|
if (_notesSuccessfullyApplied.HasValue && _notesSuccessfullyApplied.Value)
|
||||||
{
|
{
|
||||||
UiSharedService.ColorTextWrapped("User Notes successfully imported", ImGuiColors.HealerGreen);
|
UiSharedService.ColorTextWrapped("User Notes successfully imported", UiSharedService.AccentColor);
|
||||||
}
|
}
|
||||||
else if (_notesSuccessfullyApplied.HasValue && !_notesSuccessfullyApplied.Value)
|
else if (_notesSuccessfullyApplied.HasValue && !_notesSuccessfullyApplied.Value)
|
||||||
{
|
{
|
||||||
@@ -999,13 +1050,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
_configService.Save();
|
_configService.Save();
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui.SetNextItemWidth(250 * ImGuiHelpers.GlobalScale);
|
DrawDtrStyleCombo();
|
||||||
_uiShared.DrawCombo("Server Info Bar style", Enumerable.Range(0, DtrEntry.NumStyles), (i) => DtrEntry.RenderDtrStyle(i, "123"),
|
|
||||||
(i) =>
|
|
||||||
{
|
|
||||||
_configService.Current.DtrStyle = i;
|
|
||||||
_configService.Save();
|
|
||||||
}, _configService.Current.DtrStyle);
|
|
||||||
|
|
||||||
if (ImGui.Checkbox("Color-code the Server Info Bar entry according to status", ref useColorsInDtr))
|
if (ImGui.Checkbox("Color-code the Server Info Bar entry according to status", ref useColorsInDtr))
|
||||||
{
|
{
|
||||||
@@ -1255,7 +1300,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
ImGui.TextUnformatted("Current VRAM utilization by all nearby players:");
|
ImGui.TextUnformatted("Current VRAM utilization by all nearby players:");
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.HealerGreen, totalVramBytes < 2.0 * 1024.0 * 1024.0 * 1024.0))
|
using (ImRaii.PushColor(ImGuiCol.Text, UiSharedService.AccentColor, totalVramBytes < 2.0 * 1024.0 * 1024.0 * 1024.0))
|
||||||
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudYellow, totalVramBytes >= 4.0 * 1024.0 * 1024.0 * 1024.0))
|
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudYellow, totalVramBytes >= 4.0 * 1024.0 * 1024.0 * 1024.0))
|
||||||
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed, totalVramBytes >= 6.0 * 1024.0 * 1024.0 * 1024.0))
|
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed, totalVramBytes >= 6.0 * 1024.0 * 1024.0 * 1024.0))
|
||||||
ImGui.TextUnformatted($"{totalVramBytes / 1024.0 / 1024.0 / 1024.0:0.00} GiB");
|
ImGui.TextUnformatted($"{totalVramBytes / 1024.0 / 1024.0 / 1024.0:0.00} GiB");
|
||||||
@@ -1610,8 +1655,6 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
string selectedKeyName = string.Empty;
|
string selectedKeyName = string.Empty;
|
||||||
if (selectedServer.SecretKeys.TryGetValue(item.SecretKeyIdx, out var selectedKey))
|
if (selectedServer.SecretKeys.TryGetValue(item.SecretKeyIdx, out var selectedKey))
|
||||||
selectedKeyName = selectedKey.FriendlyName;
|
selectedKeyName = selectedKey.FriendlyName;
|
||||||
|
|
||||||
// _uiShared.DrawCombo() remembers the selected option -- we don't want that, because the value can change
|
|
||||||
if (ImGui.BeginCombo($"##{item.CharacterName}{i}", selectedKeyName))
|
if (ImGui.BeginCombo($"##{item.CharacterName}{i}", selectedKeyName))
|
||||||
{
|
{
|
||||||
foreach (var key in selectedServer.SecretKeys)
|
foreach (var key in selectedServer.SecretKeys)
|
||||||
@@ -1848,11 +1891,11 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
ImGui.TextUnformatted("Service " + _serverConfigurationManager.CurrentServer!.ServerName + ":");
|
ImGui.TextUnformatted("Service " + _serverConfigurationManager.CurrentServer!.ServerName + ":");
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
ImGui.TextColored(ImGuiColors.ParsedGreen, "Available");
|
ImGui.TextColored(UiSharedService.AccentColor, "Available");
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
ImGui.TextUnformatted("(");
|
ImGui.TextUnformatted("(");
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
ImGui.TextColored(ImGuiColors.ParsedGreen, _apiController.OnlineUsers.ToString(CultureInfo.InvariantCulture));
|
ImGui.TextColored(UiSharedService.AccentColor, _apiController.OnlineUsers.ToString(CultureInfo.InvariantCulture));
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
ImGui.TextUnformatted("Users Online");
|
ImGui.TextUnformatted("Users Online");
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
@@ -1890,13 +1933,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
ImGui.BeginDisabled(_registrationInProgress);
|
ImGui.BeginDisabled(_registrationInProgress);
|
||||||
DrawServerConfiguration();
|
DrawServerConfiguration();
|
||||||
ImGui.EndTabItem();
|
ImGui.EndTabItem();
|
||||||
ImGui.EndDisabled(); // _registrationInProgress
|
ImGui.EndDisabled();
|
||||||
}
|
|
||||||
|
|
||||||
if (ImGui.BeginTabItem("Chat"))
|
|
||||||
{
|
|
||||||
DrawChatConfig();
|
|
||||||
ImGui.EndTabItem();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ImGui.BeginTabItem("Advanced"))
|
if (ImGui.BeginTabItem("Advanced"))
|
||||||
@@ -1909,6 +1946,38 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void DrawDtrStyleCombo()
|
||||||
|
{
|
||||||
|
var styleIndex = _configService.Current.DtrStyle;
|
||||||
|
string previewText = styleIndex == 0 ? DtrDefaultPreviewText : DtrEntry.RenderDtrStyle(styleIndex, "123");
|
||||||
|
|
||||||
|
ImGui.SetNextItemWidth(250 * ImGuiHelpers.GlobalScale);
|
||||||
|
bool comboOpen = ImGui.BeginCombo("Server Info Bar style", previewText);
|
||||||
|
|
||||||
|
if (comboOpen)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < DtrEntry.NumStyles; i++)
|
||||||
|
{
|
||||||
|
string label = i == 0 ? DtrDefaultPreviewText : DtrEntry.RenderDtrStyle(i, "123");
|
||||||
|
bool isSelected = i == styleIndex;
|
||||||
|
if (ImGui.Selectable(label, isSelected))
|
||||||
|
{
|
||||||
|
_configService.Current.DtrStyle = i;
|
||||||
|
_configService.Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSelected)
|
||||||
|
{
|
||||||
|
ImGui.SetItemDefaultFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.EndCombo();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
private void UiSharedService_GposeEnd()
|
private void UiSharedService_GposeEnd()
|
||||||
{
|
{
|
||||||
IsOpen = _wasOpen;
|
IsOpen = _wasOpen;
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
|||||||
ImGuiWindowFlags.NoScrollbar |
|
ImGuiWindowFlags.NoScrollbar |
|
||||||
ImGuiWindowFlags.NoScrollWithMouse;
|
ImGuiWindowFlags.NoScrollWithMouse;
|
||||||
|
|
||||||
public static Vector4 AccentColor { get; set; } = ImGuiColors.DalamudYellow;
|
public static Vector4 AccentColor { get; set; } = ImGuiColors.DalamudViolet;
|
||||||
|
|
||||||
public readonly FileDialogManager FileDialogManager;
|
public readonly FileDialogManager FileDialogManager;
|
||||||
|
|
||||||
@@ -310,7 +310,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Vector4 GetBoolColor(bool input) => input ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed;
|
public static Vector4 GetBoolColor(bool input) => input ? AccentColor : ImGuiColors.DalamudRed;
|
||||||
|
|
||||||
public float GetIconTextButtonSize(FontAwesomeIcon icon, string text)
|
public float GetIconTextButtonSize(FontAwesomeIcon icon, string text)
|
||||||
{
|
{
|
||||||
@@ -517,7 +517,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
public void BooleanToColoredIcon(bool value, bool inline = true)
|
public void BooleanToColoredIcon(bool value, bool inline = true)
|
||||||
{
|
{
|
||||||
using var colorgreen = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.HealerGreen, value);
|
using var colorgreen = ImRaii.PushColor(ImGuiCol.Text, AccentColor, value);
|
||||||
using var colorred = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed, !value);
|
using var colorred = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed, !value);
|
||||||
|
|
||||||
if (inline) ImGui.SameLine();
|
if (inline) ImGui.SameLine();
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
{
|
{
|
||||||
"Author": "SirConstance",
|
"Author": "Keda",
|
||||||
"Name": "UmbraSync",
|
"Name": "UmbraSync",
|
||||||
"Punchline": "Share your true self.",
|
"Punchline": "Parce que nous le valons bien.",
|
||||||
"Description": "This plugin will synchronize your Penumbra mods and current Glamourer state with other paired clients automatically.",
|
"Description": "Ce plugin synchronisera automatiquement vos mods Penumbra et l'état actuel de Glamourer avec les autres clients appairés.",
|
||||||
"InternalName": "UmbraSync",
|
"InternalName": "UmbraSync",
|
||||||
"ApplicableVersion": "any",
|
"ApplicableVersion": "any",
|
||||||
"Tags": [
|
"Tags": [
|
||||||
"customization"
|
"customization"
|
||||||
],
|
],
|
||||||
"IconUrl": "https://repo.umbra-sync.net/logo.png",
|
"IconUrl": "https://repo.umbra-sync.net/images/logo.png",
|
||||||
"RepoUrl": "https://repo.umbra-sync.net/plugin.json",
|
"RepoUrl": "https://repo.umbra-sync.net/plugin.json",
|
||||||
"CanUnloadAsync": true
|
"CanUnloadAsync": true
|
||||||
}
|
}
|
||||||
|
|||||||
209
MareSynchronos/WebAPI/AutoDetect/DiscoveryApiClient.cs
Normal file
209
MareSynchronos/WebAPI/AutoDetect/DiscoveryApiClient.cs
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using MareSynchronos.WebAPI.SignalR;
|
||||||
|
using MareSynchronos.Services.AutoDetect;
|
||||||
|
|
||||||
|
namespace MareSynchronos.WebAPI.AutoDetect;
|
||||||
|
|
||||||
|
public class DiscoveryApiClient
|
||||||
|
{
|
||||||
|
private readonly ILogger<DiscoveryApiClient> _logger;
|
||||||
|
private readonly TokenProvider _tokenProvider;
|
||||||
|
private readonly DiscoveryConfigProvider _configProvider;
|
||||||
|
private readonly HttpClient _httpClient = new();
|
||||||
|
private static readonly JsonSerializerOptions JsonOpt = new() { PropertyNameCaseInsensitive = true };
|
||||||
|
|
||||||
|
public DiscoveryApiClient(ILogger<DiscoveryApiClient> logger, TokenProvider tokenProvider, DiscoveryConfigProvider configProvider)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_tokenProvider = tokenProvider;
|
||||||
|
_configProvider = configProvider;
|
||||||
|
_httpClient.Timeout = TimeSpan.FromSeconds(30);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<ServerMatch>> QueryAsync(string endpoint, IEnumerable<string> hashes, CancellationToken ct)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var token = await _tokenProvider.GetOrUpdateToken(ct).ConfigureAwait(false);
|
||||||
|
if (string.IsNullOrEmpty(token)) return [];
|
||||||
|
var distinctHashes = hashes.Distinct(StringComparer.Ordinal).ToArray();
|
||||||
|
using var req = new HttpRequestMessage(HttpMethod.Post, endpoint);
|
||||||
|
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||||
|
var body = JsonSerializer.Serialize(new
|
||||||
|
{
|
||||||
|
hashes = distinctHashes,
|
||||||
|
salt = _configProvider.SaltB64
|
||||||
|
});
|
||||||
|
req.Content = new StringContent(body, Encoding.UTF8, "application/json");
|
||||||
|
var resp = await _httpClient.SendAsync(req, ct).ConfigureAwait(false);
|
||||||
|
if (resp.StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
||||||
|
{
|
||||||
|
var token2 = await _tokenProvider.ForceRefreshToken(ct).ConfigureAwait(false);
|
||||||
|
if (string.IsNullOrEmpty(token2)) return [];
|
||||||
|
using var req2 = new HttpRequestMessage(HttpMethod.Post, endpoint);
|
||||||
|
req2.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token2);
|
||||||
|
var body2 = JsonSerializer.Serialize(new
|
||||||
|
{
|
||||||
|
hashes = distinctHashes,
|
||||||
|
salt = _configProvider.SaltB64
|
||||||
|
});
|
||||||
|
req2.Content = new StringContent(body2, Encoding.UTF8, "application/json");
|
||||||
|
resp = await _httpClient.SendAsync(req2, ct).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
resp.EnsureSuccessStatusCode();
|
||||||
|
var json = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
|
||||||
|
var result = JsonSerializer.Deserialize<List<ServerMatch>>(json, JsonOpt) ?? [];
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Discovery query failed");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> SendRequestAsync(string endpoint, string token, string? displayName, CancellationToken ct)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var jwt = await _tokenProvider.GetOrUpdateToken(ct).ConfigureAwait(false);
|
||||||
|
if (string.IsNullOrEmpty(jwt)) return false;
|
||||||
|
using var req = new HttpRequestMessage(HttpMethod.Post, endpoint);
|
||||||
|
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwt);
|
||||||
|
var body = JsonSerializer.Serialize(new { token, displayName });
|
||||||
|
req.Content = new StringContent(body, Encoding.UTF8, "application/json");
|
||||||
|
var resp = await _httpClient.SendAsync(req, ct).ConfigureAwait(false);
|
||||||
|
if (resp.StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
||||||
|
{
|
||||||
|
var jwt2 = await _tokenProvider.ForceRefreshToken(ct).ConfigureAwait(false);
|
||||||
|
if (string.IsNullOrEmpty(jwt2)) return false;
|
||||||
|
using var req2 = new HttpRequestMessage(HttpMethod.Post, endpoint);
|
||||||
|
req2.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwt2);
|
||||||
|
var body2 = JsonSerializer.Serialize(new { token, displayName });
|
||||||
|
req2.Content = new StringContent(body2, Encoding.UTF8, "application/json");
|
||||||
|
resp = await _httpClient.SendAsync(req2, ct).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
if (!resp.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
string txt = string.Empty;
|
||||||
|
try { txt = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false); } catch { }
|
||||||
|
_logger.LogWarning("Discovery request failed: {code} {reason} {body}", (int)resp.StatusCode, resp.ReasonPhrase, txt);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Discovery send request failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> PublishAsync(string endpoint, IEnumerable<string> hashes, string? displayName, CancellationToken ct, bool allowRequests = true)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var jwt = await _tokenProvider.GetOrUpdateToken(ct).ConfigureAwait(false);
|
||||||
|
if (string.IsNullOrEmpty(jwt)) return false;
|
||||||
|
using var req = new HttpRequestMessage(HttpMethod.Post, endpoint);
|
||||||
|
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwt);
|
||||||
|
var bodyObj = new
|
||||||
|
{
|
||||||
|
hashes = hashes.Distinct(StringComparer.Ordinal).ToArray(),
|
||||||
|
displayName,
|
||||||
|
salt = _configProvider.SaltB64,
|
||||||
|
allowRequests
|
||||||
|
};
|
||||||
|
var body = JsonSerializer.Serialize(bodyObj);
|
||||||
|
req.Content = new StringContent(body, Encoding.UTF8, "application/json");
|
||||||
|
var resp = await _httpClient.SendAsync(req, ct).ConfigureAwait(false);
|
||||||
|
if (resp.StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
||||||
|
{
|
||||||
|
var jwt2 = await _tokenProvider.ForceRefreshToken(ct).ConfigureAwait(false);
|
||||||
|
if (string.IsNullOrEmpty(jwt2)) return false;
|
||||||
|
using var req2 = new HttpRequestMessage(HttpMethod.Post, endpoint);
|
||||||
|
req2.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwt2);
|
||||||
|
var body2 = JsonSerializer.Serialize(bodyObj);
|
||||||
|
req2.Content = new StringContent(body2, Encoding.UTF8, "application/json");
|
||||||
|
resp = await _httpClient.SendAsync(req2, ct).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
return resp.IsSuccessStatusCode;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Discovery publish failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> SendAcceptAsync(string endpoint, string targetUid, string? displayName, CancellationToken ct)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var jwt = await _tokenProvider.GetOrUpdateToken(ct).ConfigureAwait(false);
|
||||||
|
if (string.IsNullOrEmpty(jwt)) return false;
|
||||||
|
using var req = new HttpRequestMessage(HttpMethod.Post, endpoint);
|
||||||
|
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwt);
|
||||||
|
var bodyObj = new { targetUid, displayName };
|
||||||
|
var body = JsonSerializer.Serialize(bodyObj);
|
||||||
|
req.Content = new StringContent(body, Encoding.UTF8, "application/json");
|
||||||
|
var resp = await _httpClient.SendAsync(req, ct).ConfigureAwait(false);
|
||||||
|
if (resp.StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
||||||
|
{
|
||||||
|
var jwt2 = await _tokenProvider.ForceRefreshToken(ct).ConfigureAwait(false);
|
||||||
|
if (string.IsNullOrEmpty(jwt2)) return false;
|
||||||
|
using var req2 = new HttpRequestMessage(HttpMethod.Post, endpoint);
|
||||||
|
req2.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwt2);
|
||||||
|
var body2 = JsonSerializer.Serialize(bodyObj);
|
||||||
|
req2.Content = new StringContent(body2, Encoding.UTF8, "application/json");
|
||||||
|
resp = await _httpClient.SendAsync(req2, ct).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
return resp.IsSuccessStatusCode;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Discovery accept notify failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public async Task DisableAsync(string endpoint, CancellationToken ct)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var jwt = await _tokenProvider.GetOrUpdateToken(ct).ConfigureAwait(false);
|
||||||
|
if (string.IsNullOrEmpty(jwt)) return;
|
||||||
|
using var req = new HttpRequestMessage(HttpMethod.Post, endpoint);
|
||||||
|
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwt);
|
||||||
|
var resp = await _httpClient.SendAsync(req, ct).ConfigureAwait(false);
|
||||||
|
if (resp.StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
||||||
|
{
|
||||||
|
var jwt2 = await _tokenProvider.ForceRefreshToken(ct).ConfigureAwait(false);
|
||||||
|
if (string.IsNullOrEmpty(jwt2)) return;
|
||||||
|
using var req2 = new HttpRequestMessage(HttpMethod.Post, endpoint);
|
||||||
|
req2.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwt2);
|
||||||
|
resp = await _httpClient.SendAsync(req2, ct).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
if (!resp.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
string txt = string.Empty;
|
||||||
|
try { txt = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false); } catch { }
|
||||||
|
_logger.LogWarning("Discovery disable failed: {code} {reason} {body}", (int)resp.StatusCode, resp.ReasonPhrase, txt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Discovery disable failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class ServerMatch
|
||||||
|
{
|
||||||
|
public string Hash { get; set; } = string.Empty;
|
||||||
|
public string? Token { get; set; }
|
||||||
|
public string? Uid { get; set; }
|
||||||
|
public string? DisplayName { get; set; }
|
||||||
|
}
|
||||||
@@ -49,7 +49,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
public List<FileTransfer> ForbiddenTransfers => _orchestrator.ForbiddenTransfers;
|
public List<FileTransfer> ForbiddenTransfers => _orchestrator.ForbiddenTransfers;
|
||||||
|
|
||||||
public bool IsDownloading => !CurrentDownloads.Any();
|
public bool IsDownloading => CurrentDownloads.Any();
|
||||||
|
|
||||||
public void ClearDownload()
|
public void ClearDownload()
|
||||||
{
|
{
|
||||||
@@ -507,4 +507,4 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
_orchestrator.ClearDownloadRequest(requestId);
|
_orchestrator.ClearDownloadRequest(requestId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using MareSynchronos.API.Data;
|
using System;
|
||||||
|
using MareSynchronos.API.Data;
|
||||||
using MareSynchronos.API.Dto.Group;
|
using MareSynchronos.API.Dto.Group;
|
||||||
using MareSynchronos.WebAPI.SignalR.Utils;
|
using MareSynchronos.WebAPI.SignalR.Utils;
|
||||||
using Microsoft.AspNetCore.SignalR.Client;
|
using Microsoft.AspNetCore.SignalR.Client;
|
||||||
@@ -49,10 +50,21 @@ public partial class ApiController
|
|||||||
await _mareHub!.SendAsync(nameof(GroupClear), group).ConfigureAwait(false);
|
await _mareHub!.SendAsync(nameof(GroupClear), group).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<GroupPasswordDto> GroupCreate()
|
public Task<GroupPasswordDto> GroupCreate()
|
||||||
|
{
|
||||||
|
return GroupCreate(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<GroupPasswordDto> GroupCreate(string? alias)
|
||||||
{
|
{
|
||||||
CheckConnection();
|
CheckConnection();
|
||||||
return await _mareHub!.InvokeAsync<GroupPasswordDto>(nameof(GroupCreate)).ConfigureAwait(false);
|
return await _mareHub!.InvokeAsync<GroupPasswordDto>(nameof(GroupCreate), string.IsNullOrWhiteSpace(alias) ? null : alias.Trim()).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<GroupPasswordDto> GroupCreateTemporary(DateTime expiresAtUtc)
|
||||||
|
{
|
||||||
|
CheckConnection();
|
||||||
|
return await _mareHub!.InvokeAsync<GroupPasswordDto>(nameof(GroupCreateTemporary), expiresAtUtc).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<string>> GroupCreateTempInvite(GroupDto group, int amount)
|
public async Task<List<string>> GroupCreateTempInvite(GroupDto group, int amount)
|
||||||
@@ -125,4 +137,4 @@ public partial class ApiController
|
|||||||
{
|
{
|
||||||
if (ServerState is not (ServerState.Connected or ServerState.Connecting or ServerState.Reconnecting)) throw new InvalidDataException("Not connected");
|
if (ServerState is not (ServerState.Connected or ServerState.Connecting or ServerState.Reconnecting)) throw new InvalidDataException("Not connected");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -222,7 +222,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
await LoadIninitialPairs().ConfigureAwait(false);
|
await LoadInitialPairs().ConfigureAwait(false);
|
||||||
await LoadOnlinePairs().ConfigureAwait(false);
|
await LoadOnlinePairs().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
@@ -375,7 +375,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM
|
|||||||
_initialized = true;
|
_initialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LoadIninitialPairs()
|
private async Task LoadInitialPairs()
|
||||||
{
|
{
|
||||||
foreach (var userPair in await UserGetPairedClients().ConfigureAwait(false))
|
foreach (var userPair in await UserGetPairedClients().ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
@@ -435,7 +435,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ServerState = ServerState.Connected;
|
ServerState = ServerState.Connected;
|
||||||
await LoadIninitialPairs().ConfigureAwait(false);
|
await LoadInitialPairs().ConfigureAwait(false);
|
||||||
await LoadOnlinePairs().ConfigureAwait(false);
|
await LoadOnlinePairs().ConfigureAwait(false);
|
||||||
Mediator.Publish(new ConnectedMessage(_connectionDto));
|
Mediator.Publish(new ConnectedMessage(_connectionDto));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -172,6 +172,16 @@ public sealed class TokenProvider : IDisposable, IMediatorSubscriber
|
|||||||
return await GetNewToken(jwtIdentifier, ct).ConfigureAwait(false);
|
return await GetNewToken(jwtIdentifier, ct).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<string?> ForceRefreshToken(CancellationToken ct)
|
||||||
|
{
|
||||||
|
JwtIdentifier? jwtIdentifier = await GetIdentifier().ConfigureAwait(false);
|
||||||
|
if (jwtIdentifier == null) return null;
|
||||||
|
|
||||||
|
_tokenCache.TryRemove(jwtIdentifier, out _);
|
||||||
|
_logger.LogTrace("ForceRefresh: Getting new token");
|
||||||
|
return await GetNewToken(jwtIdentifier, ct).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
public string? GetStapledWellKnown(string apiUrl)
|
public string? GetStapledWellKnown(string apiUrl)
|
||||||
{
|
{
|
||||||
_wellKnownCache.TryGetValue(apiUrl, out var wellKnown);
|
_wellKnownCache.TryGetValue(apiUrl, out var wellKnown);
|
||||||
@@ -180,4 +190,4 @@ public sealed class TokenProvider : IDisposable, IMediatorSubscriber
|
|||||||
return null;
|
return null;
|
||||||
return wellKnown;
|
return wellKnown;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user