Compare commits

...

2 Commits

Author SHA1 Message Date
620ebf9195 Update UI & Syncshell Public & MCDF Share 2025-11-01 19:57:54 +01:00
8cc4f34c55 Update UI & Syncshell Public & MCDF Share 2025-11-01 19:55:49 +01:00
23 changed files with 1950 additions and 299 deletions

Submodule MareAPI updated: 0abb078c21...deb911cb0a

View File

@@ -15,6 +15,7 @@ using MareSynchronos.Services;
using MareSynchronos.Services.Events; using MareSynchronos.Services.Events;
using MareSynchronos.Services.Mediator; using MareSynchronos.Services.Mediator;
using MareSynchronos.Services.ServerConfiguration; using MareSynchronos.Services.ServerConfiguration;
using MareSynchronos.Services.Notifications;
using MareSynchronos.UI; using MareSynchronos.UI;
using MareSynchronos.UI.Components; using MareSynchronos.UI.Components;
using MareSynchronos.UI.Components.Popup; using MareSynchronos.UI.Components.Popup;
@@ -102,6 +103,7 @@ public sealed class Plugin : IDalamudPlugin
collection.AddSingleton<MareSynchronos.Services.AutoDetect.NearbyDiscoveryService>(); collection.AddSingleton<MareSynchronos.Services.AutoDetect.NearbyDiscoveryService>();
collection.AddSingleton<MareSynchronos.Services.AutoDetect.NearbyPendingService>(); collection.AddSingleton<MareSynchronos.Services.AutoDetect.NearbyPendingService>();
collection.AddSingleton<MareSynchronos.Services.AutoDetect.AutoDetectSuppressionService>(); collection.AddSingleton<MareSynchronos.Services.AutoDetect.AutoDetectSuppressionService>();
collection.AddSingleton<MareSynchronos.Services.AutoDetect.SyncshellDiscoveryService>();
collection.AddSingleton<MarePlugin>(); collection.AddSingleton<MarePlugin>();
collection.AddSingleton<MareProfileManager>(); collection.AddSingleton<MareProfileManager>();
collection.AddSingleton<GameObjectHandlerFactory>(); collection.AddSingleton<GameObjectHandlerFactory>();
@@ -126,6 +128,7 @@ public sealed class Plugin : IDalamudPlugin
collection.AddSingleton<CharaDataCharacterHandler>(); collection.AddSingleton<CharaDataCharacterHandler>();
collection.AddSingleton<CharaDataNearbyManager>(); collection.AddSingleton<CharaDataNearbyManager>();
collection.AddSingleton<CharaDataGposeTogetherManager>(); collection.AddSingleton<CharaDataGposeTogetherManager>();
collection.AddSingleton<McdfShareManager>();
collection.AddSingleton<VfxSpawnManager>(); collection.AddSingleton<VfxSpawnManager>();
collection.AddSingleton<BlockedCharacterHandler>(); collection.AddSingleton<BlockedCharacterHandler>();
@@ -151,6 +154,7 @@ public sealed class Plugin : IDalamudPlugin
collection.AddSingleton<PartyListTypingService>(); collection.AddSingleton<PartyListTypingService>();
collection.AddSingleton<TypingIndicatorStateService>(); collection.AddSingleton<TypingIndicatorStateService>();
collection.AddSingleton<ChatTwoCompatibilityService>(); collection.AddSingleton<ChatTwoCompatibilityService>();
collection.AddSingleton<NotificationTracker>();
collection.AddSingleton((s) => new MareConfigService(pluginInterface.ConfigDirectory.FullName)); collection.AddSingleton((s) => new MareConfigService(pluginInterface.ConfigDirectory.FullName));
collection.AddSingleton((s) => new ServerConfigService(pluginInterface.ConfigDirectory.FullName)); collection.AddSingleton((s) => new ServerConfigService(pluginInterface.ConfigDirectory.FullName));
@@ -186,6 +190,7 @@ public sealed class Plugin : IDalamudPlugin
collection.AddScoped<CompactUi>(); collection.AddScoped<CompactUi>();
collection.AddScoped<EditProfileUi>(); collection.AddScoped<EditProfileUi>();
collection.AddScoped<DataAnalysisUi>(); collection.AddScoped<DataAnalysisUi>();
collection.AddScoped<CharaDataHubUi>();
collection.AddScoped<AutoDetectUi>(); collection.AddScoped<AutoDetectUi>();
collection.AddScoped<WindowMediatorSubscriberBase>(sp => sp.GetRequiredService<SettingsUi>()); collection.AddScoped<WindowMediatorSubscriberBase>(sp => sp.GetRequiredService<SettingsUi>());
collection.AddScoped<WindowMediatorSubscriberBase>(sp => sp.GetRequiredService<CompactUi>()); collection.AddScoped<WindowMediatorSubscriberBase>(sp => sp.GetRequiredService<CompactUi>());
@@ -227,6 +232,7 @@ public sealed class Plugin : IDalamudPlugin
collection.AddHostedService(p => p.GetRequiredService<MarePlugin>()); collection.AddHostedService(p => p.GetRequiredService<MarePlugin>());
collection.AddHostedService(p => p.GetRequiredService<IpcProvider>()); collection.AddHostedService(p => p.GetRequiredService<IpcProvider>());
collection.AddHostedService(p => p.GetRequiredService<MareSynchronos.Services.AutoDetect.NearbyDiscoveryService>()); collection.AddHostedService(p => p.GetRequiredService<MareSynchronos.Services.AutoDetect.NearbyDiscoveryService>());
collection.AddHostedService(p => p.GetRequiredService<MareSynchronos.Services.AutoDetect.SyncshellDiscoveryService>());
collection.AddHostedService(p => p.GetRequiredService<ChatTwoCompatibilityService>()); collection.AddHostedService(p => p.GetRequiredService<ChatTwoCompatibilityService>());
collection.AddHostedService(p => p.GetRequiredService<MareSynchronos.Services.AutoDetect.AutoDetectSuppressionService>()); collection.AddHostedService(p => p.GetRequiredService<MareSynchronos.Services.AutoDetect.AutoDetectSuppressionService>());
}) })

View File

@@ -3,6 +3,7 @@ using System.Collections.Concurrent;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using MareSynchronos.MareConfiguration.Models; using MareSynchronos.MareConfiguration.Models;
using MareSynchronos.Services.Mediator; using MareSynchronos.Services.Mediator;
using MareSynchronos.Services.Notifications;
using MareSynchronos.WebAPI; using MareSynchronos.WebAPI;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -14,17 +15,19 @@ public sealed class NearbyPendingService : IMediatorSubscriber
private readonly MareMediator _mediator; private readonly MareMediator _mediator;
private readonly ApiController _api; private readonly ApiController _api;
private readonly AutoDetectRequestService _requestService; private readonly AutoDetectRequestService _requestService;
private readonly NotificationTracker _notificationTracker;
private readonly ConcurrentDictionary<string, string> _pending = new(StringComparer.Ordinal); private readonly ConcurrentDictionary<string, string> _pending = new(StringComparer.Ordinal);
private static readonly TimeSpan RegexTimeout = TimeSpan.FromSeconds(1); private static readonly TimeSpan RegexTimeout = TimeSpan.FromSeconds(1);
private static readonly Regex ReqRegex = new(@"^Nearby Request: .+ \[(?<uid>[A-Z0-9]+)\]$", RegexOptions.Compiled | RegexOptions.ExplicitCapture, RegexTimeout); private static readonly Regex ReqRegex = new(@"^Nearby Request: .+ \[(?<uid>[A-Z0-9]+)\]$", RegexOptions.Compiled | RegexOptions.ExplicitCapture, RegexTimeout);
private static readonly Regex AcceptRegex = new(@"^Nearby Accept: .+ \[(?<uid>[A-Z0-9]+)\]$", RegexOptions.Compiled | RegexOptions.ExplicitCapture, RegexTimeout); private static readonly Regex AcceptRegex = new(@"^Nearby Accept: .+ \[(?<uid>[A-Z0-9]+)\]$", RegexOptions.Compiled | RegexOptions.ExplicitCapture, RegexTimeout);
public NearbyPendingService(ILogger<NearbyPendingService> logger, MareMediator mediator, ApiController api, AutoDetectRequestService requestService) public NearbyPendingService(ILogger<NearbyPendingService> logger, MareMediator mediator, ApiController api, AutoDetectRequestService requestService, NotificationTracker notificationTracker)
{ {
_logger = logger; _logger = logger;
_mediator = mediator; _mediator = mediator;
_api = api; _api = api;
_requestService = requestService; _requestService = requestService;
_notificationTracker = notificationTracker;
_mediator.Subscribe<NotificationMessage>(this, OnNotification); _mediator.Subscribe<NotificationMessage>(this, OnNotification);
_mediator.Subscribe<ManualPairInviteMessage>(this, OnManualPairInvite); _mediator.Subscribe<ManualPairInviteMessage>(this, OnManualPairInvite);
} }
@@ -46,6 +49,7 @@ public sealed class NearbyPendingService : IMediatorSubscriber
_ = _api.UserAddPair(new MareSynchronos.API.Dto.User.UserDto(new MareSynchronos.API.Data.UserData(uidA))); _ = _api.UserAddPair(new MareSynchronos.API.Dto.User.UserDto(new MareSynchronos.API.Data.UserData(uidA)));
_pending.TryRemove(uidA, out _); _pending.TryRemove(uidA, out _);
_requestService.RemovePendingRequestByUid(uidA); _requestService.RemovePendingRequestByUid(uidA);
_notificationTracker.Remove(NotificationCategory.AutoDetect, uidA);
_logger.LogInformation("NearbyPending: auto-accepted pairing with {uid}", uidA); _logger.LogInformation("NearbyPending: auto-accepted pairing with {uid}", uidA);
} }
return; return;
@@ -67,6 +71,7 @@ public sealed class NearbyPendingService : IMediatorSubscriber
catch { name = uid; } catch { name = uid; }
_pending[uid] = name; _pending[uid] = name;
_logger.LogInformation("NearbyPending: received request from {uid} ({name})", uid, name); _logger.LogInformation("NearbyPending: received request from {uid} ({name})", uid, name);
_notificationTracker.Upsert(NotificationEntry.AutoDetect(uid, name));
} }
private void OnManualPairInvite(ManualPairInviteMessage msg) private void OnManualPairInvite(ManualPairInviteMessage msg)
@@ -81,12 +86,14 @@ public sealed class NearbyPendingService : IMediatorSubscriber
_pending[msg.SourceUid] = display; _pending[msg.SourceUid] = display;
_logger.LogInformation("NearbyPending: received manual invite from {uid} ({name})", msg.SourceUid, display); _logger.LogInformation("NearbyPending: received manual invite from {uid} ({name})", msg.SourceUid, display);
_mediator.Publish(new NotificationMessage("Nearby request", $"{display} vous a envoyé une invitation de pair.", NotificationType.Info, TimeSpan.FromSeconds(5))); _mediator.Publish(new NotificationMessage("Nearby request", $"{display} vous a envoyé une invitation de pair.", NotificationType.Info, TimeSpan.FromSeconds(5)));
_notificationTracker.Upsert(NotificationEntry.AutoDetect(msg.SourceUid, display));
} }
public void Remove(string uid) public void Remove(string uid)
{ {
_pending.TryRemove(uid, out _); _pending.TryRemove(uid, out _);
_requestService.RemovePendingRequestByUid(uid); _requestService.RemovePendingRequestByUid(uid);
_notificationTracker.Remove(NotificationCategory.AutoDetect, uid);
} }
public async Task<bool> AcceptAsync(string uid) public async Task<bool> AcceptAsync(string uid)
@@ -97,6 +104,7 @@ public sealed class NearbyPendingService : IMediatorSubscriber
_pending.TryRemove(uid, out _); _pending.TryRemove(uid, out _);
_requestService.RemovePendingRequestByUid(uid); _requestService.RemovePendingRequestByUid(uid);
_ = _requestService.SendAcceptNotifyAsync(uid); _ = _requestService.SendAcceptNotifyAsync(uid);
_notificationTracker.Remove(NotificationCategory.AutoDetect, uid);
return true; return true;
} }
catch (Exception ex) catch (Exception ex)

View File

@@ -0,0 +1,146 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MareSynchronos.API.Data;
using MareSynchronos.API.Dto.Group;
using MareSynchronos.Services.Mediator;
using MareSynchronos.WebAPI;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace MareSynchronos.Services.AutoDetect;
public sealed class SyncshellDiscoveryService : IHostedService, IMediatorSubscriber
{
private readonly ILogger<SyncshellDiscoveryService> _logger;
private readonly MareMediator _mediator;
private readonly ApiController _apiController;
private readonly SemaphoreSlim _refreshSemaphore = new(1, 1);
private readonly object _entriesLock = new();
private List<SyncshellDiscoveryEntryDto> _entries = [];
private string? _lastError;
private bool _isRefreshing;
public SyncshellDiscoveryService(ILogger<SyncshellDiscoveryService> logger, MareMediator mediator, ApiController apiController)
{
_logger = logger;
_mediator = mediator;
_apiController = apiController;
}
public MareMediator Mediator => _mediator;
public IReadOnlyList<SyncshellDiscoveryEntryDto> Entries
{
get
{
lock (_entriesLock)
{
return _entries.ToList();
}
}
}
public bool IsRefreshing => _isRefreshing;
public string? LastError => _lastError;
public async Task<bool> JoinAsync(string gid, CancellationToken ct)
{
try
{
return await _apiController.SyncshellDiscoveryJoin(new GroupDto(new GroupData(gid))).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Join syncshell discovery failed for {gid}", gid);
return false;
}
}
public async Task<SyncshellDiscoveryStateDto?> GetStateAsync(string gid, CancellationToken ct)
{
try
{
return await _apiController.SyncshellDiscoveryGetState(new GroupDto(new GroupData(gid))).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to fetch syncshell discovery state for {gid}", gid);
return null;
}
}
public async Task<bool> SetVisibilityAsync(string gid, bool visible, CancellationToken ct)
{
try
{
var request = new SyncshellDiscoveryVisibilityRequestDto
{
GID = gid,
AutoDetectVisible = visible,
};
var success = await _apiController.SyncshellDiscoverySetVisibility(request).ConfigureAwait(false);
if (!success) return false;
var state = await GetStateAsync(gid, ct).ConfigureAwait(false);
if (state != null)
{
_mediator.Publish(new SyncshellAutoDetectStateChanged(state.GID, state.AutoDetectVisible, state.PasswordTemporarilyDisabled));
}
return true;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to set syncshell visibility for {gid}", gid);
return false;
}
}
public async Task RefreshAsync(CancellationToken ct)
{
if (!await _refreshSemaphore.WaitAsync(0, ct).ConfigureAwait(false))
{
return;
}
try
{
_isRefreshing = true;
var discovered = await _apiController.SyncshellDiscoveryList().ConfigureAwait(false);
lock (_entriesLock)
{
_entries = discovered ?? [];
}
_lastError = null;
_mediator.Publish(new SyncshellDiscoveryUpdated(Entries.ToList()));
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to refresh syncshell discovery list");
_lastError = ex.Message;
}
finally
{
_isRefreshing = false;
_refreshSemaphore.Release();
}
}
public Task StartAsync(CancellationToken cancellationToken)
{
_mediator.Subscribe<ConnectedMessage>(this, msg =>
{
_ = RefreshAsync(CancellationToken.None);
});
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
_mediator.UnsubscribeAll(this);
return Task.CompletedTask;
}
}

View File

@@ -11,6 +11,7 @@ using MareSynchronos.Services.CharaData.Models;
using MareSynchronos.Utils; using MareSynchronos.Utils;
using MareSynchronos.WebAPI.Files; using MareSynchronos.WebAPI.Files;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Threading;
namespace MareSynchronos.Services; namespace MareSynchronos.Services;
@@ -295,6 +296,32 @@ public sealed class CharaDataFileHandler : IDisposable
} }
} }
internal async Task<byte[]?> CreateCharaFileBytesAsync(string description, CancellationToken token = default)
{
var tempFilePath = Path.Combine(Path.GetTempPath(), "umbra_mcdfshare_" + Guid.NewGuid().ToString("N") + ".mcdf");
try
{
await SaveCharaFileAsync(description, tempFilePath).ConfigureAwait(false);
if (!File.Exists(tempFilePath)) return null;
token.ThrowIfCancellationRequested();
return await File.ReadAllBytesAsync(tempFilePath, token).ConfigureAwait(false);
}
finally
{
try
{
if (File.Exists(tempFilePath))
{
File.Delete(tempFilePath);
}
}
catch
{
// ignored
}
}
}
internal async Task<List<string>> UploadFiles(List<string> fileList, ValueProgress<string> uploadProgress, CancellationToken token) internal async Task<List<string>> UploadFiles(List<string> fileList, ValueProgress<string> uploadProgress, CancellationToken token)
{ {
return await _fileUploadManager.UploadFiles(fileList, uploadProgress, token).ConfigureAwait(false); return await _fileUploadManager.UploadFiles(fileList, uploadProgress, token).ConfigureAwait(false);

View File

@@ -13,7 +13,9 @@ using MareSynchronos.Utils;
using MareSynchronos.WebAPI; using MareSynchronos.WebAPI;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.IO;
using System.Text; using System.Text;
using System.Threading;
namespace MareSynchronos.Services; namespace MareSynchronos.Services;
@@ -457,6 +459,14 @@ public sealed partial class CharaDataManager : DisposableMediatorSubscriberBase
LoadedMcdfHeader = _fileHandler.LoadCharaFileHeader(filePath); LoadedMcdfHeader = _fileHandler.LoadCharaFileHeader(filePath);
} }
public async Task<string> LoadMcdfFromBytes(byte[] data, CancellationToken token = default)
{
var tempFilePath = Path.Combine(Path.GetTempPath(), "umbra_mcdfshare_" + Guid.NewGuid().ToString("N") + ".mcdf");
await File.WriteAllBytesAsync(tempFilePath, data, token).ConfigureAwait(false);
LoadedMcdfHeader = _fileHandler.LoadCharaFileHeader(tempFilePath);
return tempFilePath;
}
public void McdfApplyToTarget(string charaName) public void McdfApplyToTarget(string charaName)
{ {
if (LoadedMcdfHeader == null || !LoadedMcdfHeader.IsCompletedSuccessfully) return; if (LoadedMcdfHeader == null || !LoadedMcdfHeader.IsCompletedSuccessfully) return;

View File

@@ -0,0 +1,309 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using MareSynchronos.API.Dto.McdfShare;
using MareSynchronos.MareConfiguration.Models;
using MareSynchronos.Services.ServerConfiguration;
using MareSynchronos.WebAPI;
using Microsoft.Extensions.Logging;
namespace MareSynchronos.Services.CharaData;
public sealed class McdfShareManager
{
private readonly ILogger<McdfShareManager> _logger;
private readonly ApiController _apiController;
private readonly CharaDataFileHandler _fileHandler;
private readonly CharaDataManager _charaDataManager;
private readonly ServerConfigurationManager _serverConfigurationManager;
private readonly SemaphoreSlim _operationSemaphore = new(1, 1);
private readonly List<McdfShareEntryDto> _ownShares = new();
private readonly List<McdfShareEntryDto> _sharedWithMe = new();
private Task? _currentTask;
public McdfShareManager(ILogger<McdfShareManager> logger, ApiController apiController,
CharaDataFileHandler fileHandler, CharaDataManager charaDataManager,
ServerConfigurationManager serverConfigurationManager)
{
_logger = logger;
_apiController = apiController;
_fileHandler = fileHandler;
_charaDataManager = charaDataManager;
_serverConfigurationManager = serverConfigurationManager;
}
public IReadOnlyList<McdfShareEntryDto> OwnShares => _ownShares;
public IReadOnlyList<McdfShareEntryDto> SharedShares => _sharedWithMe;
public bool IsBusy => _currentTask is { IsCompleted: false };
public string? LastError { get; private set; }
public string? LastSuccess { get; private set; }
public Task RefreshAsync(CancellationToken token)
{
return RunOperation(() => InternalRefreshAsync(token));
}
public Task CreateShareAsync(string description, IReadOnlyList<string> allowedIndividuals, IReadOnlyList<string> allowedSyncshells, DateTime? expiresAtUtc, CancellationToken token)
{
return RunOperation(async () =>
{
token.ThrowIfCancellationRequested();
var mcdfBytes = await _fileHandler.CreateCharaFileBytesAsync(description, token).ConfigureAwait(false);
if (mcdfBytes == null || mcdfBytes.Length == 0)
{
LastError = "Impossible de préparer les données MCDF.";
return;
}
var secretKey = _serverConfigurationManager.GetSecretKey(out bool hasMultiple);
if (hasMultiple)
{
LastError = "Plusieurs clés secrètes sont configurées pour ce personnage. Corrigez cela dans les paramètres.";
return;
}
if (string.IsNullOrEmpty(secretKey))
{
LastError = "Aucune clé secrète n'est configurée pour ce personnage.";
return;
}
var shareId = Guid.NewGuid();
byte[] salt = RandomNumberGenerator.GetBytes(16);
byte[] nonce = RandomNumberGenerator.GetBytes(12);
byte[] key = DeriveKey(secretKey, shareId, salt);
byte[] cipher = new byte[mcdfBytes.Length];
byte[] tag = new byte[16];
using (var aes = new AesGcm(key, 16))
{
aes.Encrypt(nonce, mcdfBytes, cipher, tag);
}
var uploadDto = new McdfShareUploadRequestDto
{
ShareId = shareId,
Description = description,
CipherData = cipher,
Nonce = nonce,
Salt = salt,
Tag = tag,
ExpiresAtUtc = expiresAtUtc,
AllowedIndividuals = allowedIndividuals.ToList(),
AllowedSyncshells = allowedSyncshells.ToList()
};
await _apiController.McdfShareUpload(uploadDto).ConfigureAwait(false);
await InternalRefreshAsync(token).ConfigureAwait(false);
LastSuccess = "Partage MCDF créé.";
});
}
public Task DeleteShareAsync(Guid shareId)
{
return RunOperation(async () =>
{
var result = await _apiController.McdfShareDelete(shareId).ConfigureAwait(false);
if (!result)
{
LastError = "Le serveur a refusé de supprimer le partage MCDF.";
return;
}
_ownShares.RemoveAll(s => s.Id == shareId);
_sharedWithMe.RemoveAll(s => s.Id == shareId);
await InternalRefreshAsync(CancellationToken.None).ConfigureAwait(false);
LastSuccess = "Partage MCDF supprimé.";
});
}
public Task UpdateShareAsync(McdfShareUpdateRequestDto updateRequest)
{
return RunOperation(async () =>
{
var updated = await _apiController.McdfShareUpdate(updateRequest).ConfigureAwait(false);
if (updated == null)
{
LastError = "Le serveur a refusé de mettre à jour le partage MCDF.";
return;
}
var idx = _ownShares.FindIndex(s => s.Id == updated.Id);
if (idx >= 0)
{
_ownShares[idx] = updated;
}
LastSuccess = "Partage MCDF mis à jour.";
});
}
public Task ApplyShareAsync(Guid shareId, CancellationToken token)
{
return RunOperation(async () =>
{
token.ThrowIfCancellationRequested();
var plainBytes = await DownloadAndDecryptShareAsync(shareId, token).ConfigureAwait(false);
if (plainBytes == null)
{
LastError ??= "Échec du téléchargement du partage MCDF.";
return;
}
var tempPath = await _charaDataManager.LoadMcdfFromBytes(plainBytes, token).ConfigureAwait(false);
try
{
await _charaDataManager.McdfApplyToGposeTarget().ConfigureAwait(false);
}
finally
{
try
{
if (File.Exists(tempPath))
{
File.Delete(tempPath);
}
}
catch
{
// ignored
}
}
LastSuccess = "Partage MCDF appliqué sur la cible GPose.";
});
}
public Task ExportShareAsync(Guid shareId, string filePath, CancellationToken token)
{
return RunOperation(async () =>
{
token.ThrowIfCancellationRequested();
var plainBytes = await DownloadAndDecryptShareAsync(shareId, token).ConfigureAwait(false);
if (plainBytes == null)
{
LastError ??= "Échec du téléchargement du partage MCDF.";
return;
}
var directory = Path.GetDirectoryName(filePath);
if (!string.IsNullOrEmpty(directory))
{
Directory.CreateDirectory(directory);
}
await File.WriteAllBytesAsync(filePath, plainBytes, token).ConfigureAwait(false);
LastSuccess = "Partage MCDF exporté.";
});
}
public Task DownloadShareToFileAsync(McdfShareEntryDto entry, string filePath, CancellationToken token)
{
return ExportShareAsync(entry.Id, filePath, token);
}
private async Task<byte[]?> DownloadAndDecryptShareAsync(Guid shareId, CancellationToken token)
{
var payload = await _apiController.McdfShareDownload(shareId).ConfigureAwait(false);
if (payload == null)
{
LastError = "Partage indisponible.";
return null;
}
var secretKey = _serverConfigurationManager.GetSecretKey(out bool hasMultiple);
if (hasMultiple)
{
LastError = "Plusieurs clés secrètes sont configurées pour ce personnage.";
return null;
}
if (string.IsNullOrEmpty(secretKey))
{
LastError = "Aucune clé secrète n'est configurée pour ce personnage.";
return null;
}
byte[] key = DeriveKey(secretKey, payload.ShareId, payload.Salt);
byte[] plaintext = new byte[payload.CipherData.Length];
try
{
using var aes = new AesGcm(key, 16);
aes.Decrypt(payload.Nonce, payload.CipherData, payload.Tag, plaintext);
}
catch (CryptographicException ex)
{
_logger.LogWarning(ex, "Failed to decrypt MCDF share {ShareId}", shareId);
LastError = "Impossible de déchiffrer le partage MCDF.";
return null;
}
token.ThrowIfCancellationRequested();
return plaintext;
}
private async Task InternalRefreshAsync(CancellationToken token)
{
token.ThrowIfCancellationRequested();
var own = await _apiController.McdfShareGetOwn().ConfigureAwait(false);
token.ThrowIfCancellationRequested();
var shared = await _apiController.McdfShareGetShared().ConfigureAwait(false);
_ownShares.Clear();
_ownShares.AddRange(own);
_sharedWithMe.Clear();
_sharedWithMe.AddRange(shared);
LastSuccess = "Partages MCDF actualisés.";
}
private Task RunOperation(Func<Task> operation)
{
async Task Wrapper()
{
await _operationSemaphore.WaitAsync().ConfigureAwait(false);
try
{
LastError = null;
LastSuccess = null;
await operation().ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error during MCDF share operation");
LastError = ex.Message;
}
finally
{
_operationSemaphore.Release();
}
}
var task = Wrapper();
_currentTask = task;
return task;
}
private static byte[] DeriveKey(string secretKey, Guid shareId, byte[] salt)
{
byte[] secretBytes;
try
{
secretBytes = Convert.FromHexString(secretKey);
}
catch (FormatException)
{
// fallback to UTF8 if not hex
secretBytes = System.Text.Encoding.UTF8.GetBytes(secretKey);
}
byte[] shareBytes = shareId.ToByteArray();
byte[] material = new byte[secretBytes.Length + shareBytes.Length + salt.Length];
Buffer.BlockCopy(secretBytes, 0, material, 0, secretBytes.Length);
Buffer.BlockCopy(shareBytes, 0, material, secretBytes.Length, shareBytes.Length);
Buffer.BlockCopy(salt, 0, material, secretBytes.Length + shareBytes.Length, salt.Length);
return SHA256.HashData(material);
}
}

View File

@@ -109,9 +109,23 @@ public sealed class MareMediator : IHostedService, IDisposable
} }
} }
private bool _disposed;
public void Dispose() public void Dispose()
{
if (_disposed) return;
_disposed = true;
if (!_loopCts.IsCancellationRequested)
{
try
{ {
_loopCts.Cancel(); _loopCts.Cancel();
}
catch (ObjectDisposedException)
{
// already disposed, swallow
}
}
_loopCts.Dispose(); _loopCts.Dispose();
} }

View File

@@ -115,12 +115,15 @@ public record NearbyEntry(string Name, ushort WorldId, float Distance, bool IsMa
public record DiscoveryListUpdated(List<NearbyEntry> Entries) : MessageBase; public record DiscoveryListUpdated(List<NearbyEntry> Entries) : MessageBase;
public record NearbyDetectionToggled(bool Enabled) : MessageBase; public record NearbyDetectionToggled(bool Enabled) : MessageBase;
public record AllowPairRequestsToggled(bool Enabled) : MessageBase; public record AllowPairRequestsToggled(bool Enabled) : MessageBase;
public record SyncshellDiscoveryUpdated(List<SyncshellDiscoveryEntryDto> Entries) : MessageBase;
public record SyncshellAutoDetectStateChanged(string Gid, bool Visible, bool PasswordTemporarilyDisabled) : MessageBase;
public record ManualPairInviteMessage(string SourceUid, string SourceAlias, string TargetUid, string? DisplayName, string InviteId) : MessageBase; public record ManualPairInviteMessage(string SourceUid, string SourceAlias, string TargetUid, string? DisplayName, string InviteId) : MessageBase;
public record ApplyDefaultPairPermissionsMessage(UserPairDto Pair) : MessageBase; public record ApplyDefaultPairPermissionsMessage(UserPairDto Pair) : MessageBase;
public record ApplyDefaultGroupPermissionsMessage(GroupPairFullInfoDto GroupPair) : MessageBase; public record ApplyDefaultGroupPermissionsMessage(GroupPairFullInfoDto GroupPair) : MessageBase;
public record ApplyDefaultsToAllSyncsMessage(string? Context = null, bool? Disabled = null) : MessageBase; public record ApplyDefaultsToAllSyncsMessage(string? Context = null, bool? Disabled = null) : MessageBase;
public record PairSyncOverrideChanged(string Uid, bool? DisableSounds, bool? DisableAnimations, bool? DisableVfx) : MessageBase; public record PairSyncOverrideChanged(string Uid, bool? DisableSounds, bool? DisableAnimations, bool? DisableVfx) : MessageBase;
public record GroupSyncOverrideChanged(string Gid, bool? DisableSounds, bool? DisableAnimations, bool? DisableVfx) : MessageBase; public record GroupSyncOverrideChanged(string Gid, bool? DisableSounds, bool? DisableAnimations, bool? DisableVfx) : MessageBase;
public record NotificationStateChanged(int TotalCount) : MessageBase;
public record PluginChangeMessage(string InternalName, Version Version, bool IsLoaded) : KeyedMessage(InternalName); public record PluginChangeMessage(string InternalName, Version Version, bool IsLoaded) : KeyedMessage(InternalName);
#pragma warning restore S2094 #pragma warning restore S2094

View File

@@ -0,0 +1,73 @@
using System;
using System.Collections.Generic;
using System.Linq;
using MareSynchronos.Services.Mediator;
namespace MareSynchronos.Services.Notifications;
public enum NotificationCategory
{
AutoDetect,
}
public sealed record NotificationEntry(NotificationCategory Category, string Id, string Title, string? Description, DateTime CreatedAt)
{
public static NotificationEntry AutoDetect(string uid, string displayName)
=> new(NotificationCategory.AutoDetect, uid, displayName, "Nouvelle demande d'appairage via AutoDetect.", DateTime.UtcNow);
}
public sealed class NotificationTracker
{
private readonly MareMediator _mediator;
private readonly Dictionary<(NotificationCategory Category, string Id), NotificationEntry> _entries = new();
private readonly object _lock = new();
public NotificationTracker(MareMediator mediator)
{
_mediator = mediator;
}
public void Upsert(NotificationEntry entry)
{
lock (_lock)
{
_entries[(entry.Category, entry.Id)] = entry;
}
PublishState();
}
public void Remove(NotificationCategory category, string id)
{
lock (_lock)
{
_entries.Remove((category, id));
}
PublishState();
}
public IReadOnlyList<NotificationEntry> GetEntries()
{
lock (_lock)
{
return _entries.Values
.OrderBy(e => e.CreatedAt)
.ToList();
}
}
public int Count
{
get
{
lock (_lock)
{
return _entries.Count;
}
}
}
private void PublishState()
{
_mediator.Publish(new NotificationStateChanged(Count));
}
}

View File

@@ -1,5 +1,6 @@
using MareSynchronos.API.Dto.Group; using MareSynchronos.API.Dto.Group;
using MareSynchronos.PlayerData.Pairs; using MareSynchronos.PlayerData.Pairs;
using MareSynchronos.Services.AutoDetect;
using MareSynchronos.Services.Mediator; using MareSynchronos.Services.Mediator;
using MareSynchronos.Services.ServerConfiguration; using MareSynchronos.Services.ServerConfiguration;
using MareSynchronos.UI; using MareSynchronos.UI;
@@ -19,9 +20,10 @@ public class UiFactory
private readonly ServerConfigurationManager _serverConfigManager; private readonly ServerConfigurationManager _serverConfigManager;
private readonly MareProfileManager _mareProfileManager; private readonly MareProfileManager _mareProfileManager;
private readonly PerformanceCollectorService _performanceCollectorService; private readonly PerformanceCollectorService _performanceCollectorService;
private readonly SyncshellDiscoveryService _syncshellDiscoveryService;
public UiFactory(ILoggerFactory loggerFactory, MareMediator mareMediator, ApiController apiController, public UiFactory(ILoggerFactory loggerFactory, MareMediator mareMediator, ApiController apiController,
UiSharedService uiSharedService, PairManager pairManager, ServerConfigurationManager serverConfigManager, UiSharedService uiSharedService, PairManager pairManager, SyncshellDiscoveryService syncshellDiscoveryService, ServerConfigurationManager serverConfigManager,
MareProfileManager mareProfileManager, PerformanceCollectorService performanceCollectorService) MareProfileManager mareProfileManager, PerformanceCollectorService performanceCollectorService)
{ {
_loggerFactory = loggerFactory; _loggerFactory = loggerFactory;
@@ -29,6 +31,7 @@ public class UiFactory
_apiController = apiController; _apiController = apiController;
_uiSharedService = uiSharedService; _uiSharedService = uiSharedService;
_pairManager = pairManager; _pairManager = pairManager;
_syncshellDiscoveryService = syncshellDiscoveryService;
_serverConfigManager = serverConfigManager; _serverConfigManager = serverConfigManager;
_mareProfileManager = mareProfileManager; _mareProfileManager = mareProfileManager;
_performanceCollectorService = performanceCollectorService; _performanceCollectorService = performanceCollectorService;
@@ -37,7 +40,7 @@ public class UiFactory
public SyncshellAdminUI CreateSyncshellAdminUi(GroupFullInfoDto dto) public SyncshellAdminUI CreateSyncshellAdminUi(GroupFullInfoDto dto)
{ {
return new SyncshellAdminUI(_loggerFactory.CreateLogger<SyncshellAdminUI>(), _mareMediator, return new SyncshellAdminUI(_loggerFactory.CreateLogger<SyncshellAdminUI>(), _mareMediator,
_apiController, _uiSharedService, _pairManager, dto, _performanceCollectorService); _apiController, _uiSharedService, _pairManager, _syncshellDiscoveryService, dto, _performanceCollectorService);
} }
public StandaloneProfileUi CreateStandaloneProfileUi(Pair pair) public StandaloneProfileUi CreateStandaloneProfileUi(Pair pair)

View File

@@ -1,11 +1,13 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Dalamud.Bindings.ImGui; using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Colors; using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility; using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Utility.Raii;
using MareSynchronos.API.Dto.Group;
using MareSynchronos.MareConfiguration; using MareSynchronos.MareConfiguration;
using MareSynchronos.PlayerData.Pairs; using MareSynchronos.PlayerData.Pairs;
using MareSynchronos.Services.Mediator; using MareSynchronos.Services.Mediator;
@@ -29,11 +31,16 @@ public class AutoDetectUi : WindowMediatorSubscriberBase
private readonly PairManager _pairManager; private readonly PairManager _pairManager;
private List<Services.Mediator.NearbyEntry> _entries; private List<Services.Mediator.NearbyEntry> _entries;
private readonly HashSet<string> _acceptInFlight = new(StringComparer.Ordinal); private readonly HashSet<string> _acceptInFlight = new(StringComparer.Ordinal);
private readonly SyncshellDiscoveryService _syncshellDiscoveryService;
private List<SyncshellDiscoveryEntryDto> _syncshellEntries = [];
private bool _syncshellInitialized;
private readonly HashSet<string> _syncshellJoinInFlight = new(StringComparer.OrdinalIgnoreCase);
private string? _syncshellLastError;
public AutoDetectUi(ILogger<AutoDetectUi> logger, MareMediator mediator, public AutoDetectUi(ILogger<AutoDetectUi> logger, MareMediator mediator,
MareConfigService configService, DalamudUtilService dalamudUtilService, MareConfigService configService, DalamudUtilService dalamudUtilService,
AutoDetectRequestService requestService, NearbyPendingService pendingService, PairManager pairManager, AutoDetectRequestService requestService, NearbyPendingService pendingService, PairManager pairManager,
NearbyDiscoveryService discoveryService, NearbyDiscoveryService discoveryService, SyncshellDiscoveryService syncshellDiscoveryService,
PerformanceCollectorService performanceCollectorService) PerformanceCollectorService performanceCollectorService)
: base(logger, mediator, "AutoDetect", performanceCollectorService) : base(logger, mediator, "AutoDetect", performanceCollectorService)
{ {
@@ -43,7 +50,9 @@ public class AutoDetectUi : WindowMediatorSubscriberBase
_pendingService = pendingService; _pendingService = pendingService;
_pairManager = pairManager; _pairManager = pairManager;
_discoveryService = discoveryService; _discoveryService = discoveryService;
_syncshellDiscoveryService = syncshellDiscoveryService;
Mediator.Subscribe<Services.Mediator.DiscoveryListUpdated>(this, OnDiscoveryUpdated); Mediator.Subscribe<Services.Mediator.DiscoveryListUpdated>(this, OnDiscoveryUpdated);
Mediator.Subscribe<SyncshellDiscoveryUpdated>(this, OnSyncshellDiscoveryUpdated);
_entries = _discoveryService.SnapshotEntries(); _entries = _discoveryService.SnapshotEntries();
Flags |= ImGuiWindowFlags.NoScrollbar; Flags |= ImGuiWindowFlags.NoScrollbar;
@@ -81,14 +90,7 @@ public class AutoDetectUi : WindowMediatorSubscriberBase
}); });
DrawStyledTab("Proximité", accent, inactiveTab, hoverTab, DrawNearbyTab); DrawStyledTab("Proximité", accent, inactiveTab, hoverTab, DrawNearbyTab);
DrawStyledTab("Syncshell", accent, inactiveTab, hoverTab, DrawSyncshellTab);
using (ImRaii.Disabled(true))
{
DrawStyledTab("Syncshell", accent, inactiveTab, hoverTab, () =>
{
UiSharedService.ColorTextWrapped("Disponible prochainement.", ImGuiColors.DalamudGrey3);
}, true);
}
} }
public void DrawInline() public void DrawInline()
@@ -221,6 +223,7 @@ public class AutoDetectUi : WindowMediatorSubscriberBase
var sourceEntries = _entries.Count > 0 ? _entries : _discoveryService.SnapshotEntries(); var sourceEntries = _entries.Count > 0 ? _entries : _discoveryService.SnapshotEntries();
var orderedEntries = sourceEntries var orderedEntries = sourceEntries
.Where(e => e.IsMatch)
.OrderBy(e => float.IsNaN(e.Distance) ? float.MaxValue : e.Distance) .OrderBy(e => float.IsNaN(e.Distance) ? float.MaxValue : e.Distance)
.ToList(); .ToList();
@@ -245,10 +248,9 @@ public class AutoDetectUi : WindowMediatorSubscriberBase
for (int i = 0; i < orderedEntries.Count; i++) for (int i = 0; i < orderedEntries.Count; i++)
{ {
var entry = orderedEntries[i]; var entry = orderedEntries[i];
bool isMatch = entry.IsMatch;
bool alreadyPaired = IsAlreadyPairedByUidOrAlias(entry); bool alreadyPaired = IsAlreadyPairedByUidOrAlias(entry);
bool overDistance = !float.IsNaN(entry.Distance) && entry.Distance > maxDist; bool overDistance = !float.IsNaN(entry.Distance) && entry.Distance > maxDist;
bool canRequest = isMatch && entry.AcceptPairRequests && !string.IsNullOrEmpty(entry.Token) && !alreadyPaired; bool canRequest = entry.AcceptPairRequests && !string.IsNullOrEmpty(entry.Token) && !alreadyPaired;
string displayName = entry.DisplayName ?? entry.Name; string displayName = entry.DisplayName ?? entry.Name;
string worldName = entry.WorldId == 0 string worldName = entry.WorldId == 0
@@ -260,8 +262,6 @@ public class AutoDetectUi : WindowMediatorSubscriberBase
? "Déjà appairé" ? "Déjà appairé"
: overDistance : overDistance
? $"Hors portée (> {maxDist} m)" ? $"Hors portée (> {maxDist} m)"
: !isMatch
? "Umbra non activé"
: !entry.AcceptPairRequests : !entry.AcceptPairRequests
? "Invitations refusées" ? "Invitations refusées"
: string.IsNullOrEmpty(entry.Token) : string.IsNullOrEmpty(entry.Token)
@@ -297,8 +297,6 @@ public class AutoDetectUi : WindowMediatorSubscriberBase
? "Vous êtes déjà appairé avec ce joueur." ? "Vous êtes déjà appairé avec ce joueur."
: overDistance : overDistance
? $"Ce joueur est au-delà de la distance maximale configurée ({maxDist} m)." ? $"Ce joueur est au-delà de la distance maximale configurée ({maxDist} m)."
: !isMatch
? "Ce joueur n'utilise pas UmbraSync ou ne s'est pas rendu détectable."
: !entry.AcceptPairRequests : !entry.AcceptPairRequests
? "Ce joueur a désactivé la réception automatique des invitations." ? "Ce joueur a désactivé la réception automatique des invitations."
: string.IsNullOrEmpty(entry.Token) : string.IsNullOrEmpty(entry.Token)
@@ -317,11 +315,147 @@ public class AutoDetectUi : WindowMediatorSubscriberBase
ImGui.EndTable(); ImGui.EndTable();
} }
private async Task JoinSyncshellAsync(SyncshellDiscoveryEntryDto entry)
{
if (!_syncshellJoinInFlight.Add(entry.GID))
{
return;
}
try
{
var joined = await _syncshellDiscoveryService.JoinAsync(entry.GID, CancellationToken.None).ConfigureAwait(false);
if (joined)
{
Mediator.Publish(new NotificationMessage("AutoDetect Syncshell", $"Rejoint {entry.Alias ?? entry.GID}.", NotificationType.Info, TimeSpan.FromSeconds(5)));
await _syncshellDiscoveryService.RefreshAsync(CancellationToken.None).ConfigureAwait(false);
}
else
{
_syncshellLastError = $"Impossible de rejoindre {entry.Alias ?? entry.GID}.";
Mediator.Publish(new NotificationMessage("AutoDetect Syncshell", _syncshellLastError, NotificationType.Warning, TimeSpan.FromSeconds(5)));
}
}
catch (Exception ex)
{
_syncshellLastError = $"Erreur lors de l'adhésion : {ex.Message}";
Mediator.Publish(new NotificationMessage("AutoDetect Syncshell", _syncshellLastError, NotificationType.Error, TimeSpan.FromSeconds(5)));
}
finally
{
_syncshellJoinInFlight.Remove(entry.GID);
}
}
private void DrawSyncshellTab()
{
if (!_syncshellInitialized)
{
_syncshellInitialized = true;
_ = _syncshellDiscoveryService.RefreshAsync(CancellationToken.None);
}
bool isRefreshing = _syncshellDiscoveryService.IsRefreshing;
var serviceError = _syncshellDiscoveryService.LastError;
if (ImGui.Button("Actualiser la liste"))
{
_ = _syncshellDiscoveryService.RefreshAsync(CancellationToken.None);
}
UiSharedService.AttachToolTip("Met à jour la liste des Syncshells ayant activé l'AutoDetect.");
if (isRefreshing)
{
ImGui.SameLine();
ImGui.TextDisabled("Actualisation...");
}
ImGuiHelpers.ScaledDummy(4);
UiSharedService.TextWrapped("Les Syncshells affichées ont temporairement désactivé leur mot de passe pour permettre un accès direct via AutoDetect. Rejoignez-les uniquement si vous faites confiance aux administrateurs.");
if (!string.IsNullOrEmpty(serviceError))
{
UiSharedService.ColorTextWrapped(serviceError, ImGuiColors.DalamudRed);
}
else if (!string.IsNullOrEmpty(_syncshellLastError))
{
UiSharedService.ColorTextWrapped(_syncshellLastError!, ImGuiColors.DalamudOrange);
}
var entries = _syncshellEntries.Count > 0 ? _syncshellEntries : _syncshellDiscoveryService.Entries.ToList();
if (entries.Count == 0)
{
ImGuiHelpers.ScaledDummy(4);
UiSharedService.ColorTextWrapped("Aucune Syncshell n'est actuellement visible dans AutoDetect.", ImGuiColors.DalamudGrey3);
return;
}
if (!ImGui.BeginTable("autodetect-syncshells", 5, ImGuiTableFlags.SizingStretchProp | ImGuiTableFlags.RowBg))
{
return;
}
ImGui.TableSetupColumn("Nom");
ImGui.TableSetupColumn("Propriétaire");
ImGui.TableSetupColumn("Membres");
ImGui.TableSetupColumn("Invitations");
ImGui.TableSetupColumn("Action");
ImGui.TableHeadersRow();
foreach (var entry in entries.OrderBy(e => e.Alias ?? e.GID, StringComparer.OrdinalIgnoreCase))
{
bool alreadyMember = _pairManager.Groups.Keys.Any(g => string.Equals(g.GID, entry.GID, StringComparison.OrdinalIgnoreCase));
bool joining = _syncshellJoinInFlight.Contains(entry.GID);
ImGui.TableNextColumn();
ImGui.TextUnformatted(string.IsNullOrEmpty(entry.Alias) ? entry.GID : $"{entry.Alias} ({entry.GID})");
ImGui.TableNextColumn();
ImGui.TextUnformatted(string.IsNullOrEmpty(entry.OwnerAlias) ? entry.OwnerUID : $"{entry.OwnerAlias} ({entry.OwnerUID})");
ImGui.TableNextColumn();
ImGui.TextUnformatted(entry.MemberCount.ToString(CultureInfo.InvariantCulture));
ImGui.TableNextColumn();
string inviteMode = entry.AutoAcceptPairs ? "Auto" : "Manuel";
ImGui.TextUnformatted(inviteMode);
if (!entry.AutoAcceptPairs)
{
UiSharedService.AttachToolTip("L'administrateur doit approuver manuellement les nouveaux membres.");
}
ImGui.TableNextColumn();
using (ImRaii.Disabled(alreadyMember || joining))
{
if (alreadyMember)
{
ImGui.TextDisabled("Déjà membre");
}
else if (joining)
{
ImGui.TextDisabled("Connexion...");
}
else if (ImGui.Button("Rejoindre"))
{
_syncshellLastError = null;
_ = JoinSyncshellAsync(entry);
}
}
}
ImGui.EndTable();
}
private void OnDiscoveryUpdated(Services.Mediator.DiscoveryListUpdated msg) private void OnDiscoveryUpdated(Services.Mediator.DiscoveryListUpdated msg)
{ {
_entries = msg.Entries; _entries = msg.Entries;
} }
private void OnSyncshellDiscoveryUpdated(SyncshellDiscoveryUpdated msg)
{
_syncshellEntries = msg.Entries;
}
private bool IsAlreadyPairedByUidOrAlias(Services.Mediator.NearbyEntry e) private bool IsAlreadyPairedByUidOrAlias(Services.Mediator.NearbyEntry e)
{ {
try try

View File

@@ -6,7 +6,7 @@ using System.Text;
namespace MareSynchronos.UI; namespace MareSynchronos.UI;
internal sealed partial class CharaDataHubUi public sealed partial class CharaDataHubUi
{ {
private static string GetAccessTypeString(AccessTypeDto dto) => dto switch private static string GetAccessTypeString(AccessTypeDto dto) => dto switch
{ {

View File

@@ -7,7 +7,7 @@ using MareSynchronos.Services.CharaData.Models;
namespace MareSynchronos.UI; namespace MareSynchronos.UI;
internal sealed partial class CharaDataHubUi public sealed partial class CharaDataHubUi
{ {
private string _joinLobbyId = string.Empty; private string _joinLobbyId = string.Empty;
private void DrawGposeTogether() private void DrawGposeTogether()
@@ -90,7 +90,7 @@ internal sealed partial class CharaDataHubUi
if (!_uiSharedService.IsInGpose) if (!_uiSharedService.IsInGpose)
{ {
ImGuiHelpers.ScaledDummy(5); ImGuiHelpers.ScaledDummy(5);
UiSharedService.DrawGroupedCenteredColorText("Assigning users to characters is only available in GPose.", ImGuiColors.DalamudYellow, 300); UiSharedService.DrawGroupedCenteredColorText("Assigning users to characters is only available in GPose.", UiSharedService.AccentColor, 300);
} }
UiSharedService.DistanceSeparator(); UiSharedService.DistanceSeparator();
ImGui.TextUnformatted("Users In Lobby"); ImGui.TextUnformatted("Users In Lobby");
@@ -104,7 +104,7 @@ internal sealed partial class CharaDataHubUi
if (!_charaDataGposeTogetherManager.UsersInLobby.Any() && !string.IsNullOrEmpty(_charaDataGposeTogetherManager.CurrentGPoseLobbyId)) if (!_charaDataGposeTogetherManager.UsersInLobby.Any() && !string.IsNullOrEmpty(_charaDataGposeTogetherManager.CurrentGPoseLobbyId))
{ {
UiSharedService.DrawGroupedCenteredColorText("No other users in current GPose lobby", ImGuiColors.DalamudYellow); UiSharedService.DrawGroupedCenteredColorText("No other users in current GPose lobby", UiSharedService.AccentColor);
} }
else else
{ {

View File

@@ -9,7 +9,7 @@ using System.Numerics;
namespace MareSynchronos.UI; namespace MareSynchronos.UI;
internal sealed partial class CharaDataHubUi public sealed partial class CharaDataHubUi
{ {
private void DrawEditCharaData(CharaDataFullExtendedDto? dataDto) private void DrawEditCharaData(CharaDataFullExtendedDto? dataDto)
{ {
@@ -18,7 +18,7 @@ internal sealed partial class CharaDataHubUi
if (dataDto == null) if (dataDto == null)
{ {
ImGuiHelpers.ScaledDummy(5); ImGuiHelpers.ScaledDummy(5);
UiSharedService.DrawGroupedCenteredColorText("Select an entry above to edit its data.", ImGuiColors.DalamudYellow); UiSharedService.DrawGroupedCenteredColorText("Select an entry above to edit its data.", UiSharedService.AccentColor);
return; return;
} }
@@ -26,7 +26,7 @@ internal sealed partial class CharaDataHubUi
if (updateDto == null) if (updateDto == null)
{ {
UiSharedService.DrawGroupedCenteredColorText("Something went awfully wrong and there's no update DTO. Try updating Character Data via the button above.", ImGuiColors.DalamudYellow); UiSharedService.DrawGroupedCenteredColorText("Something went awfully wrong and there's no update DTO. Try updating Character Data via the button above.", UiSharedService.AccentColor);
return; return;
} }
@@ -61,7 +61,7 @@ internal sealed partial class CharaDataHubUi
} }
if (_charaDataManager.CharaUpdateTask != null && !_charaDataManager.CharaUpdateTask.IsCompleted) if (_charaDataManager.CharaUpdateTask != null && !_charaDataManager.CharaUpdateTask.IsCompleted)
{ {
UiSharedService.ColorTextWrapped("Updating data on server, please wait.", ImGuiColors.DalamudYellow); UiSharedService.ColorTextWrapped("Updating data on server, please wait.", UiSharedService.AccentColor);
} }
} }
@@ -71,7 +71,7 @@ internal sealed partial class CharaDataHubUi
{ {
if (_charaDataManager.UploadProgress != null) if (_charaDataManager.UploadProgress != null)
{ {
UiSharedService.ColorTextWrapped(_charaDataManager.UploadProgress.Value ?? string.Empty, ImGuiColors.DalamudYellow); UiSharedService.ColorTextWrapped(_charaDataManager.UploadProgress.Value ?? string.Empty, UiSharedService.AccentColor);
} }
if ((!_charaDataManager.UploadTask?.IsCompleted ?? false) && _uiSharedService.IconTextButton(FontAwesomeIcon.Ban, "Cancel Upload")) if ((!_charaDataManager.UploadTask?.IsCompleted ?? false) && _uiSharedService.IconTextButton(FontAwesomeIcon.Ban, "Cancel Upload"))
{ {
@@ -230,7 +230,7 @@ internal sealed partial class CharaDataHubUi
ImGui.SameLine(); ImGui.SameLine();
ImGuiHelpers.ScaledDummy(20, 1); ImGuiHelpers.ScaledDummy(20, 1);
ImGui.SameLine(); ImGui.SameLine();
UiSharedService.ColorTextWrapped("New data was set. It may contain files that require to be uploaded (will happen on Saving to server)", ImGuiColors.DalamudYellow); UiSharedService.ColorTextWrapped("New data was set. It may contain files that require to be uploaded (will happen on Saving to server)", UiSharedService.AccentColor);
} }
ImGui.TextUnformatted("Contains Manipulation Data"); ImGui.TextUnformatted("Contains Manipulation Data");
@@ -385,7 +385,7 @@ internal sealed partial class CharaDataHubUi
} }
} }
ImGui.SameLine(); ImGui.SameLine();
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudYellow, poseCount == maxPoses)) using (ImRaii.PushColor(ImGuiCol.Text, UiSharedService.AccentColor, poseCount == maxPoses))
ImGui.TextUnformatted($"{poseCount}/{maxPoses} poses attached"); ImGui.TextUnformatted($"{poseCount}/{maxPoses} poses attached");
ImGuiHelpers.ScaledDummy(5); ImGuiHelpers.ScaledDummy(5);
@@ -395,7 +395,7 @@ internal sealed partial class CharaDataHubUi
if (!_uiSharedService.IsInGpose && _charaDataManager.BrioAvailable) if (!_uiSharedService.IsInGpose && _charaDataManager.BrioAvailable)
{ {
ImGuiHelpers.ScaledDummy(5); ImGuiHelpers.ScaledDummy(5);
UiSharedService.DrawGroupedCenteredColorText("To attach pose and world data you need to be in GPose.", ImGuiColors.DalamudYellow); UiSharedService.DrawGroupedCenteredColorText("To attach pose and world data you need to be in GPose.", UiSharedService.AccentColor);
ImGuiHelpers.ScaledDummy(5); ImGuiHelpers.ScaledDummy(5);
} }
else if (!_charaDataManager.BrioAvailable) else if (!_charaDataManager.BrioAvailable)
@@ -414,7 +414,7 @@ internal sealed partial class CharaDataHubUi
if (pose.Id == null) if (pose.Id == null)
{ {
ImGui.SameLine(50); ImGui.SameLine(50);
_uiSharedService.IconText(FontAwesomeIcon.Plus, ImGuiColors.DalamudYellow); _uiSharedService.IconText(FontAwesomeIcon.Plus, UiSharedService.AccentColor);
UiSharedService.AttachToolTip("This pose has not been added to the server yet. Save changes to upload this Pose data."); UiSharedService.AttachToolTip("This pose has not been added to the server yet. Save changes to upload this Pose data.");
} }
@@ -422,14 +422,14 @@ internal sealed partial class CharaDataHubUi
if (poseHasChanges) if (poseHasChanges)
{ {
ImGui.SameLine(50); ImGui.SameLine(50);
_uiSharedService.IconText(FontAwesomeIcon.ExclamationTriangle, ImGuiColors.DalamudYellow); _uiSharedService.IconText(FontAwesomeIcon.ExclamationTriangle, UiSharedService.AccentColor);
UiSharedService.AttachToolTip("This pose has changes that have not been saved to the server yet."); UiSharedService.AttachToolTip("This pose has changes that have not been saved to the server yet.");
} }
ImGui.SameLine(75); ImGui.SameLine(75);
if (pose.Description == null && pose.WorldData == null && pose.PoseData == null) if (pose.Description == null && pose.WorldData == null && pose.PoseData == null)
{ {
UiSharedService.ColorText("Pose scheduled for deletion", ImGuiColors.DalamudYellow); UiSharedService.ColorText("Pose scheduled for deletion", UiSharedService.AccentColor);
} }
else else
{ {
@@ -586,7 +586,7 @@ internal sealed partial class CharaDataHubUi
var idText = entry.FullId; var idText = entry.FullId;
if (uDto?.HasChanges ?? false) if (uDto?.HasChanges ?? false)
{ {
UiSharedService.ColorText(idText, ImGuiColors.DalamudYellow); UiSharedService.ColorText(idText, UiSharedService.AccentColor);
UiSharedService.AttachToolTip("This entry has unsaved changes"); UiSharedService.AttachToolTip("This entry has unsaved changes");
} }
else else
@@ -641,7 +641,7 @@ internal sealed partial class CharaDataHubUi
FontAwesomeIcon eIcon = FontAwesomeIcon.None; FontAwesomeIcon eIcon = FontAwesomeIcon.None;
if (!Equals(DateTime.MaxValue, entry.ExpiryDate)) if (!Equals(DateTime.MaxValue, entry.ExpiryDate))
eIcon = FontAwesomeIcon.Clock; eIcon = FontAwesomeIcon.Clock;
_uiSharedService.IconText(eIcon, ImGuiColors.DalamudYellow); _uiSharedService.IconText(eIcon, UiSharedService.AccentColor);
if (ImGui.IsItemClicked()) SelectedDtoId = entry.Id; if (ImGui.IsItemClicked()) SelectedDtoId = entry.Id;
if (eIcon != FontAwesomeIcon.None) if (eIcon != FontAwesomeIcon.None)
{ {
@@ -677,13 +677,13 @@ internal sealed partial class CharaDataHubUi
if (_charaDataManager.OwnCharaData.Count == _charaDataManager.MaxCreatableCharaData) if (_charaDataManager.OwnCharaData.Count == _charaDataManager.MaxCreatableCharaData)
{ {
ImGui.AlignTextToFramePadding(); ImGui.AlignTextToFramePadding();
UiSharedService.ColorTextWrapped("You have reached the maximum Character Data entries and cannot create more.", ImGuiColors.DalamudYellow); UiSharedService.ColorTextWrapped("You have reached the maximum Character Data entries and cannot create more.", UiSharedService.AccentColor);
} }
} }
if (_charaDataManager.DataCreationTask != null && !_charaDataManager.DataCreationTask.IsCompleted) if (_charaDataManager.DataCreationTask != null && !_charaDataManager.DataCreationTask.IsCompleted)
{ {
UiSharedService.ColorTextWrapped("Creating new character data entry on server...", ImGuiColors.DalamudYellow); UiSharedService.ColorTextWrapped("Creating new character data entry on server...", UiSharedService.AccentColor);
} }
else if (_charaDataManager.DataCreationTask != null && _charaDataManager.DataCreationTask.IsCompleted) else if (_charaDataManager.DataCreationTask != null && _charaDataManager.DataCreationTask.IsCompleted)
{ {

View File

@@ -7,7 +7,7 @@ using System.Numerics;
namespace MareSynchronos.UI; namespace MareSynchronos.UI;
internal partial class CharaDataHubUi public sealed partial class CharaDataHubUi
{ {
private void DrawNearbyPoses() private void DrawNearbyPoses()
{ {
@@ -86,7 +86,7 @@ internal partial class CharaDataHubUi
if (!_uiSharedService.IsInGpose) if (!_uiSharedService.IsInGpose)
{ {
ImGuiHelpers.ScaledDummy(5); ImGuiHelpers.ScaledDummy(5);
UiSharedService.DrawGroupedCenteredColorText("Spawning and applying pose data is only available in GPose.", ImGuiColors.DalamudYellow); UiSharedService.DrawGroupedCenteredColorText("Spawning and applying pose data is only available in GPose.", UiSharedService.AccentColor);
ImGuiHelpers.ScaledDummy(5); ImGuiHelpers.ScaledDummy(5);
} }
@@ -101,7 +101,7 @@ internal partial class CharaDataHubUi
using var indent = ImRaii.PushIndent(5f); using var indent = ImRaii.PushIndent(5f);
if (_charaDataNearbyManager.NearbyData.Count == 0) if (_charaDataNearbyManager.NearbyData.Count == 0)
{ {
UiSharedService.DrawGroupedCenteredColorText("No Shared World Poses found nearby.", ImGuiColors.DalamudYellow); UiSharedService.DrawGroupedCenteredColorText("No Shared World Poses found nearby.", UiSharedService.AccentColor);
} }
bool wasAnythingHovered = false; bool wasAnythingHovered = false;

View File

@@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using Dalamud.Bindings.ImGui; using Dalamud.Bindings.ImGui;
using Dalamud.Interface; using Dalamud.Interface;
using Dalamud.Interface.Colors; using Dalamud.Interface.Colors;
@@ -16,10 +17,13 @@ using MareSynchronos.Services.Mediator;
using MareSynchronos.Services.ServerConfiguration; using MareSynchronos.Services.ServerConfiguration;
using MareSynchronos.Utils; using MareSynchronos.Utils;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.IO;
using System.Linq;
using System.Threading;
namespace MareSynchronos.UI; namespace MareSynchronos.UI;
internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase public sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
{ {
private const int maxPoses = 10; private const int maxPoses = 10;
private readonly CharaDataManager _charaDataManager; private readonly CharaDataManager _charaDataManager;
@@ -31,6 +35,7 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
private readonly CharaDataGposeTogetherManager _charaDataGposeTogetherManager; private readonly CharaDataGposeTogetherManager _charaDataGposeTogetherManager;
private readonly ServerConfigurationManager _serverConfigurationManager; private readonly ServerConfigurationManager _serverConfigurationManager;
private readonly UiSharedService _uiSharedService; private readonly UiSharedService _uiSharedService;
private readonly McdfShareManager _mcdfShareManager;
private CancellationTokenSource? _closalCts = new(); private CancellationTokenSource? _closalCts = new();
private bool _disableUI = false; private bool _disableUI = false;
private CancellationTokenSource? _disposalCts = new(); private CancellationTokenSource? _disposalCts = new();
@@ -63,6 +68,15 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
} }
} }
private static string SanitizeFileName(string? candidate, string fallback)
{
var invalidChars = Path.GetInvalidFileNameChars();
if (string.IsNullOrWhiteSpace(candidate)) return fallback;
var sanitized = new string(candidate.Select(ch => invalidChars.Contains(ch) ? '_' : ch).ToArray()).Trim('_');
return string.IsNullOrWhiteSpace(sanitized) ? fallback : sanitized;
}
private string _selectedSpecificUserIndividual = string.Empty; private string _selectedSpecificUserIndividual = string.Empty;
private string _selectedSpecificGroupIndividual = string.Empty; private string _selectedSpecificGroupIndividual = string.Empty;
private string _sharedWithYouDescriptionFilter = string.Empty; private string _sharedWithYouDescriptionFilter = string.Empty;
@@ -74,12 +88,21 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
private string? _openComboHybridId = null; private string? _openComboHybridId = null;
private (string Id, string? Alias, string AliasOrId, string? Note)[]? _openComboHybridEntries = null; private (string Id, string? Alias, string AliasOrId, string? Note)[]? _openComboHybridEntries = null;
private bool _comboHybridUsedLastFrame = false; private bool _comboHybridUsedLastFrame = false;
private bool _mcdfShareInitialized;
private string _mcdfShareDescription = string.Empty;
private readonly List<string> _mcdfShareAllowedIndividuals = new();
private readonly List<string> _mcdfShareAllowedSyncshells = new();
private string _mcdfShareIndividualDropdownSelection = string.Empty;
private string _mcdfShareIndividualInput = string.Empty;
private string _mcdfShareSyncshellDropdownSelection = string.Empty;
private string _mcdfShareSyncshellInput = string.Empty;
private int _mcdfShareExpireDays;
public CharaDataHubUi(ILogger<CharaDataHubUi> logger, MareMediator mediator, PerformanceCollectorService performanceCollectorService, public CharaDataHubUi(ILogger<CharaDataHubUi> logger, MareMediator mediator, PerformanceCollectorService performanceCollectorService,
CharaDataManager charaDataManager, CharaDataNearbyManager charaDataNearbyManager, CharaDataConfigService configService, CharaDataManager charaDataManager, CharaDataNearbyManager charaDataNearbyManager, CharaDataConfigService configService,
UiSharedService uiSharedService, ServerConfigurationManager serverConfigurationManager, UiSharedService uiSharedService, ServerConfigurationManager serverConfigurationManager,
DalamudUtilService dalamudUtilService, FileDialogManager fileDialogManager, PairManager pairManager, DalamudUtilService dalamudUtilService, FileDialogManager fileDialogManager, PairManager pairManager,
CharaDataGposeTogetherManager charaDataGposeTogetherManager) CharaDataGposeTogetherManager charaDataGposeTogetherManager, McdfShareManager mcdfShareManager)
: base(logger, mediator, "Umbra Character Data Hub###UmbraCharaDataUI", performanceCollectorService) : base(logger, mediator, "Umbra Character Data Hub###UmbraCharaDataUI", performanceCollectorService)
{ {
SetWindowSizeConstraints(); SetWindowSizeConstraints();
@@ -93,6 +116,7 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
_fileDialogManager = fileDialogManager; _fileDialogManager = fileDialogManager;
_pairManager = pairManager; _pairManager = pairManager;
_charaDataGposeTogetherManager = charaDataGposeTogetherManager; _charaDataGposeTogetherManager = charaDataGposeTogetherManager;
_mcdfShareManager = mcdfShareManager;
Mediator.Subscribe<GposeStartMessage>(this, (_) => IsOpen |= _configService.Current.OpenMareHubOnGposeStart); Mediator.Subscribe<GposeStartMessage>(this, (_) => IsOpen |= _configService.Current.OpenMareHubOnGposeStart);
Mediator.Subscribe<OpenCharaDataHubWithFilterMessage>(this, (msg) => Mediator.Subscribe<OpenCharaDataHubWithFilterMessage>(this, (msg) =>
{ {
@@ -158,6 +182,19 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
} }
protected override void DrawInternal() protected override void DrawInternal()
{
DrawHubContent();
}
public void DrawInline()
{
using (ImRaii.PushId("CharaDataHubInline"))
{
DrawHubContent();
}
}
private void DrawHubContent()
{ {
if (!_comboHybridUsedLastFrame) if (!_comboHybridUsedLastFrame)
{ {
@@ -198,7 +235,7 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
} }
if (!string.IsNullOrEmpty(_charaDataManager.DataApplicationProgress)) if (!string.IsNullOrEmpty(_charaDataManager.DataApplicationProgress))
{ {
UiSharedService.ColorTextWrapped(_charaDataManager.DataApplicationProgress, ImGuiColors.DalamudYellow); UiSharedService.ColorTextWrapped(_charaDataManager.DataApplicationProgress, UiSharedService.AccentColor);
} }
if (_charaDataManager.DataApplicationTask != null) if (_charaDataManager.DataApplicationTask != null)
{ {
@@ -208,8 +245,12 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
} }
}); });
using var tabs = ImRaii.TabBar("TabsTopLevel");
bool smallUi = false; bool smallUi = false;
using (var topTabColor = ImRaii.PushColor(ImGuiCol.Tab, UiSharedService.AccentColor))
using (var topTabHoverColor = ImRaii.PushColor(ImGuiCol.TabHovered, UiSharedService.AccentHoverColor))
using (var topTabActiveColor = ImRaii.PushColor(ImGuiCol.TabActive, UiSharedService.AccentActiveColor))
{
using var tabs = ImRaii.TabBar("TabsTopLevel");
_isHandlingSelf = _charaDataManager.HandledCharaData.Any(c => c.Value.IsSelf); _isHandlingSelf = _charaDataManager.HandledCharaData.Any(c => c.Value.IsSelf);
if (_isHandlingSelf) _openMcdOnlineOnNextRun = false; if (_isHandlingSelf) _openMcdOnlineOnNextRun = false;
@@ -229,6 +270,10 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
if (applicationTabItem) if (applicationTabItem)
{ {
smallUi = true; smallUi = true;
using (var appTabColor = ImRaii.PushColor(ImGuiCol.Tab, UiSharedService.AccentColor))
using (var appTabHoverColor = ImRaii.PushColor(ImGuiCol.TabHovered, UiSharedService.AccentHoverColor))
using (var appTabActiveColor = ImRaii.PushColor(ImGuiCol.TabActive, UiSharedService.AccentActiveColor))
{
using var appTabs = ImRaii.TabBar("TabsApplicationLevel"); using var appTabs = ImRaii.TabBar("TabsApplicationLevel");
using (ImRaii.Disabled(!_uiSharedService.IsInGpose)) using (ImRaii.Disabled(!_uiSharedService.IsInGpose))
@@ -270,6 +315,7 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
} }
} }
} }
}
else else
{ {
_charaDataNearbyManager.ComputeNearbyData = false; _charaDataNearbyManager.ComputeNearbyData = false;
@@ -288,6 +334,10 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
using (var creationTabItem = ImRaii.TabItem("Data Creation", flagsTopLevel)) using (var creationTabItem = ImRaii.TabItem("Data Creation", flagsTopLevel))
{ {
if (creationTabItem) if (creationTabItem)
{
using (var creationTabColor = ImRaii.PushColor(ImGuiCol.Tab, UiSharedService.AccentColor))
using (var creationTabHoverColor = ImRaii.PushColor(ImGuiCol.TabHovered, UiSharedService.AccentHoverColor))
using (var creationTabActiveColor = ImRaii.PushColor(ImGuiCol.TabActive, UiSharedService.AccentActiveColor))
{ {
using var creationTabs = ImRaii.TabBar("TabsCreationLevel"); using var creationTabs = ImRaii.TabBar("TabsCreationLevel");
@@ -314,9 +364,21 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
DrawMcdfExport(); DrawMcdfExport();
} }
} }
using (var mcdfShareTabItem = ImRaii.TabItem("Partage MCDF"))
{
if (mcdfShareTabItem)
{
using var id = ImRaii.PushId("mcdfShare");
DrawMcdfShare();
} }
} }
} }
}
}
}
}
if (_isHandlingSelf) if (_isHandlingSelf)
{ {
UiSharedService.AttachToolTip("Cannot use creation tools while having Character Data applied to self."); UiSharedService.AttachToolTip("Cannot use creation tools while having Character Data applied to self.");
@@ -444,11 +506,15 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
if (!_hasValidGposeTarget) if (!_hasValidGposeTarget)
{ {
ImGuiHelpers.ScaledDummy(3); ImGuiHelpers.ScaledDummy(3);
UiSharedService.DrawGroupedCenteredColorText("Applying data is only available in GPose with a valid selected GPose target.", ImGuiColors.DalamudYellow, 350); UiSharedService.DrawGroupedCenteredColorText("Applying data is only available in GPose with a valid selected GPose target.", UiSharedService.AccentColor, 350);
} }
ImGuiHelpers.ScaledDummy(10); ImGuiHelpers.ScaledDummy(10);
using (var applyTabColor = ImRaii.PushColor(ImGuiCol.Tab, UiSharedService.AccentColor))
using (var applyTabHoverColor = ImRaii.PushColor(ImGuiCol.TabHovered, UiSharedService.AccentHoverColor))
using (var applyTabActiveColor = ImRaii.PushColor(ImGuiCol.TabActive, UiSharedService.AccentActiveColor))
{
using var tabs = ImRaii.TabBar("Tabs"); using var tabs = ImRaii.TabBar("Tabs");
using (var byFavoriteTabItem = ImRaii.TabItem("Favorites")) using (var byFavoriteTabItem = ImRaii.TabItem("Favorites"))
@@ -603,7 +669,7 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
if (_configService.Current.FavoriteCodes.Count == 0) if (_configService.Current.FavoriteCodes.Count == 0)
{ {
UiSharedService.ColorTextWrapped("You have no favorites added. Add Favorites through the other tabs before you can use this tab.", ImGuiColors.DalamudYellow); UiSharedService.ColorTextWrapped("You have no favorites added. Add Favorites through the other tabs before you can use this tab.", UiSharedService.AccentColor);
} }
} }
} }
@@ -652,7 +718,7 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
ImGui.NewLine(); ImGui.NewLine();
if (!_charaDataManager.DownloadMetaInfoTask?.IsCompleted ?? false) if (!_charaDataManager.DownloadMetaInfoTask?.IsCompleted ?? false)
{ {
UiSharedService.ColorTextWrapped("Downloading meta info. Please wait.", ImGuiColors.DalamudYellow); UiSharedService.ColorTextWrapped("Downloading meta info. Please wait.", UiSharedService.AccentColor);
} }
if ((_charaDataManager.DownloadMetaInfoTask?.IsCompleted ?? false) && !_charaDataManager.DownloadMetaInfoTask.Result.Success) if ((_charaDataManager.DownloadMetaInfoTask?.IsCompleted ?? false) && !_charaDataManager.DownloadMetaInfoTask.Result.Success)
{ {
@@ -859,12 +925,13 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
UiSharedService.ColorTextWrapped("Failure to read MCDF file. MCDF file is possibly corrupt. Re-export the MCDF file and try again.", UiSharedService.ColorTextWrapped("Failure to read MCDF file. MCDF file is possibly corrupt. Re-export the MCDF file and try again.",
UiSharedService.AccentColor); UiSharedService.AccentColor);
UiSharedService.ColorTextWrapped("Note: if this is your MCDF, try redrawing yourself, wait and re-export the file. " + UiSharedService.ColorTextWrapped("Note: if this is your MCDF, try redrawing yourself, wait and re-export the file. " +
"If you received it from someone else have them do the same.", ImGuiColors.DalamudYellow); "If you received it from someone else have them do the same.", UiSharedService.AccentColor);
} }
} }
else else
{ {
UiSharedService.ColorTextWrapped("Loading Character...", ImGuiColors.DalamudYellow); UiSharedService.ColorTextWrapped("Loading Character...", UiSharedService.AccentColor);
}
} }
} }
} }
@@ -892,7 +959,7 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
{ {
string defaultFileName = string.IsNullOrEmpty(_exportDescription) string defaultFileName = string.IsNullOrEmpty(_exportDescription)
? "export.mcdf" ? "export.mcdf"
: string.Join('_', $"{_exportDescription}.mcdf".Split(Path.GetInvalidFileNameChars())); : SanitizeFileName(_exportDescription, "export") + ".mcdf";
_uiSharedService.FileDialogManager.SaveFileDialog("Export Character to file", ".mcdf", defaultFileName, ".mcdf", (success, path) => _uiSharedService.FileDialogManager.SaveFileDialog("Export Character to file", ".mcdf", defaultFileName, ".mcdf", (success, path) =>
{ {
if (!success) return; if (!success) return;
@@ -905,12 +972,418 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
}, Directory.Exists(_configService.Current.LastSavedCharaDataLocation) ? _configService.Current.LastSavedCharaDataLocation : null); }, Directory.Exists(_configService.Current.LastSavedCharaDataLocation) ? _configService.Current.LastSavedCharaDataLocation : null);
} }
UiSharedService.ColorTextWrapped("Note: For best results make sure you have everything you want to be shared as well as the correct character appearance" + UiSharedService.ColorTextWrapped("Note: For best results make sure you have everything you want to be shared as well as the correct character appearance" +
" equipped and redraw your character before exporting.", ImGuiColors.DalamudYellow); " equipped and redraw your character before exporting.", UiSharedService.AccentColor);
ImGui.Unindent(); ImGui.Unindent();
} }
} }
private void DrawMcdfShare()
{
if (!_mcdfShareInitialized && !_mcdfShareManager.IsBusy)
{
_mcdfShareInitialized = true;
_ = _mcdfShareManager.RefreshAsync(CancellationToken.None);
}
if (_mcdfShareManager.IsBusy)
{
UiSharedService.ColorTextWrapped("Traitement en cours...", ImGuiColors.DalamudYellow);
}
if (!string.IsNullOrEmpty(_mcdfShareManager.LastError))
{
UiSharedService.ColorTextWrapped(_mcdfShareManager.LastError!, ImGuiColors.DalamudRed);
}
else if (!string.IsNullOrEmpty(_mcdfShareManager.LastSuccess))
{
UiSharedService.ColorTextWrapped(_mcdfShareManager.LastSuccess!, ImGuiColors.HealerGreen);
}
if (ImGui.Button("Actualiser les partages"))
{
_ = _mcdfShareManager.RefreshAsync(CancellationToken.None);
}
ImGui.Separator();
_uiSharedService.BigText("Créer un partage MCDF");
ImGui.InputTextWithHint("##mcdfShareDescription", "Description", ref _mcdfShareDescription, 128);
ImGui.InputInt("Expiration (jours, 0 = jamais)", ref _mcdfShareExpireDays);
DrawMcdfShareIndividualDropdown();
ImGui.SameLine();
ImGui.SetNextItemWidth(220f);
if (ImGui.InputTextWithHint("##mcdfShareUidInput", "UID ou vanity", ref _mcdfShareIndividualInput, 32))
{
_mcdfShareIndividualDropdownSelection = string.Empty;
}
ImGui.SameLine();
var normalizedUid = NormalizeUidCandidate(_mcdfShareIndividualInput);
using (ImRaii.Disabled(string.IsNullOrEmpty(normalizedUid)
|| _mcdfShareAllowedIndividuals.Any(p => string.Equals(p, normalizedUid, StringComparison.OrdinalIgnoreCase))))
{
if (ImGui.SmallButton("Ajouter"))
{
_mcdfShareAllowedIndividuals.Add(normalizedUid);
_mcdfShareIndividualInput = string.Empty;
_mcdfShareIndividualDropdownSelection = string.Empty;
}
}
ImGui.SameLine();
ImGui.TextUnformatted("UID synchronisé à ajouter");
_uiSharedService.DrawHelpText("Choisissez un pair synchronisé dans la liste ou saisissez un UID. Les utilisateurs listés pourront récupérer ce partage MCDF.");
foreach (var uid in _mcdfShareAllowedIndividuals.ToArray())
{
using (ImRaii.PushId("mcdfShareUid" + uid))
{
ImGui.BulletText(FormatPairLabel(uid));
ImGui.SameLine();
if (ImGui.SmallButton("Retirer"))
{
_mcdfShareAllowedIndividuals.Remove(uid);
}
}
}
DrawMcdfShareSyncshellDropdown();
ImGui.SameLine();
ImGui.SetNextItemWidth(220f);
if (ImGui.InputTextWithHint("##mcdfShareSyncshellInput", "GID ou alias", ref _mcdfShareSyncshellInput, 32))
{
_mcdfShareSyncshellDropdownSelection = string.Empty;
}
ImGui.SameLine();
var normalizedSyncshell = NormalizeSyncshellCandidate(_mcdfShareSyncshellInput);
using (ImRaii.Disabled(string.IsNullOrEmpty(normalizedSyncshell)
|| _mcdfShareAllowedSyncshells.Any(p => string.Equals(p, normalizedSyncshell, StringComparison.OrdinalIgnoreCase))))
{
if (ImGui.SmallButton("Ajouter"))
{
_mcdfShareAllowedSyncshells.Add(normalizedSyncshell);
_mcdfShareSyncshellInput = string.Empty;
_mcdfShareSyncshellDropdownSelection = string.Empty;
}
}
ImGui.SameLine();
ImGui.TextUnformatted("Syncshell à ajouter");
_uiSharedService.DrawHelpText("Sélectionnez une syncshell synchronisée ou saisissez un identifiant. Les syncshells listées auront accès au partage.");
foreach (var shell in _mcdfShareAllowedSyncshells.ToArray())
{
using (ImRaii.PushId("mcdfShareShell" + shell))
{
ImGui.BulletText(FormatSyncshellLabel(shell));
ImGui.SameLine();
if (ImGui.SmallButton("Retirer"))
{
_mcdfShareAllowedSyncshells.Remove(shell);
}
}
}
using (ImRaii.Disabled(_mcdfShareManager.IsBusy))
{
if (ImGui.Button("Créer"))
{
DateTime? expiresAt = _mcdfShareExpireDays <= 0 ? null : DateTime.UtcNow.AddDays(_mcdfShareExpireDays);
_ = _mcdfShareManager.CreateShareAsync(_mcdfShareDescription, _mcdfShareAllowedIndividuals.ToList(), _mcdfShareAllowedSyncshells.ToList(), expiresAt, CancellationToken.None);
_mcdfShareDescription = string.Empty;
_mcdfShareAllowedIndividuals.Clear();
_mcdfShareAllowedSyncshells.Clear();
_mcdfShareIndividualInput = string.Empty;
_mcdfShareIndividualDropdownSelection = string.Empty;
_mcdfShareSyncshellInput = string.Empty;
_mcdfShareSyncshellDropdownSelection = string.Empty;
_mcdfShareExpireDays = 0;
}
}
ImGui.Separator();
_uiSharedService.BigText("Mes partages : ");
if (_mcdfShareManager.OwnShares.Count == 0)
{
ImGui.TextDisabled("Aucun partage MCDF créé.");
}
else if (ImGui.BeginTable("mcdf-own-shares", 6, ImGuiTableFlags.SizingStretchProp | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersOuter))
{
ImGui.TableSetupColumn("Description");
ImGui.TableSetupColumn("Créé le");
ImGui.TableSetupColumn("Expire");
ImGui.TableSetupColumn("Téléchargements");
ImGui.TableSetupColumn("Accès");
ImGui.TableSetupColumn("Actions", ImGuiTableColumnFlags.WidthFixed, 220f);
ImGui.TableHeadersRow();
foreach (var entry in _mcdfShareManager.OwnShares)
{
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.TextUnformatted(string.IsNullOrEmpty(entry.Description) ? entry.Id.ToString() : entry.Description);
ImGui.TableNextColumn();
ImGui.TextUnformatted(entry.CreatedUtc.ToLocalTime().ToString("g"));
ImGui.TableNextColumn();
ImGui.TextUnformatted(entry.ExpiresAtUtc.HasValue ? entry.ExpiresAtUtc.Value.ToLocalTime().ToString("g") : "Jamais");
ImGui.TableNextColumn();
ImGui.TextUnformatted(entry.DownloadCount.ToString());
ImGui.TableNextColumn();
ImGui.TextUnformatted($"UID : {entry.AllowedIndividuals.Count}, Syncshells : {entry.AllowedSyncshells.Count}");
ImGui.TableNextColumn();
using (ImRaii.PushId("ownShare" + entry.Id))
{
if (ImGui.SmallButton("Appliquer en GPose"))
{
_ = _mcdfShareManager.ApplyShareAsync(entry.Id, CancellationToken.None);
}
ImGui.SameLine();
if (ImGui.SmallButton("Enregistrer"))
{
var baseName = SanitizeFileName(entry.Description, entry.Id.ToString());
var defaultName = baseName + ".mcdf";
_fileDialogManager.SaveFileDialog("Enregistrer le partage MCDF", ".mcdf", defaultName, ".mcdf", async (success, path) =>
{
if (!success || string.IsNullOrEmpty(path)) return;
await _mcdfShareManager.ExportShareAsync(entry.Id, path, CancellationToken.None).ConfigureAwait(false);
});
}
ImGui.SameLine();
if (ImGui.SmallButton("Supprimer"))
{
_ = _mcdfShareManager.DeleteShareAsync(entry.Id);
}
}
}
ImGui.EndTable();
}
ImGui.Separator();
_uiSharedService.BigText("Partagés avec moi : ");
if (_mcdfShareManager.SharedShares.Count == 0)
{
ImGui.TextDisabled("Aucun partage MCDF reçu.");
}
else if (ImGui.BeginTable("mcdf-shared-shares", 5, ImGuiTableFlags.SizingStretchProp | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersOuter))
{
ImGui.TableSetupColumn("Description");
ImGui.TableSetupColumn("Propriétaire");
ImGui.TableSetupColumn("Expire");
ImGui.TableSetupColumn("Téléchargements");
ImGui.TableSetupColumn("Actions", ImGuiTableColumnFlags.WidthFixed, 180f);
ImGui.TableHeadersRow();
foreach (var entry in _mcdfShareManager.SharedShares)
{
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.TextUnformatted(string.IsNullOrEmpty(entry.Description) ? entry.Id.ToString() : entry.Description);
ImGui.TableNextColumn();
ImGui.TextUnformatted(string.IsNullOrEmpty(entry.OwnerAlias) ? entry.OwnerUid : entry.OwnerAlias);
ImGui.TableNextColumn();
ImGui.TextUnformatted(entry.ExpiresAtUtc.HasValue ? entry.ExpiresAtUtc.Value.ToLocalTime().ToString("g") : "Jamais");
ImGui.TableNextColumn();
ImGui.TextUnformatted(entry.DownloadCount.ToString());
ImGui.TableNextColumn();
using (ImRaii.PushId("sharedShare" + entry.Id))
{
if (ImGui.SmallButton("Appliquer"))
{
_ = _mcdfShareManager.ApplyShareAsync(entry.Id, CancellationToken.None);
}
ImGui.SameLine();
if (ImGui.SmallButton("Enregistrer"))
{
var baseName = SanitizeFileName(entry.Description, entry.Id.ToString());
var defaultName = baseName + ".mcdf";
_fileDialogManager.SaveFileDialog("Enregistrer le partage MCDF", ".mcdf", defaultName, ".mcdf", async (success, path) =>
{
if (!success || string.IsNullOrEmpty(path)) return;
await _mcdfShareManager.ExportShareAsync(entry.Id, path, CancellationToken.None).ConfigureAwait(false);
});
}
}
}
ImGui.EndTable();
}
}
private void DrawMcdfShareIndividualDropdown()
{
ImGui.SetNextItemWidth(220f);
var previewSource = string.IsNullOrEmpty(_mcdfShareIndividualDropdownSelection)
? _mcdfShareIndividualInput
: _mcdfShareIndividualDropdownSelection;
var previewLabel = string.IsNullOrEmpty(previewSource)
? "Sélectionner un pair synchronisé..."
: FormatPairLabel(previewSource);
using var combo = ImRaii.Combo("##mcdfShareUidDropdown", previewLabel, ImGuiComboFlags.None);
if (!combo)
{
return;
}
foreach (var pair in _pairManager.DirectPairs
.OrderBy(p => p.GetNoteOrName() ?? p.UserData.AliasOrUID, StringComparer.OrdinalIgnoreCase))
{
var normalized = pair.UserData.UID;
var display = FormatPairLabel(normalized);
bool selected = string.Equals(normalized, _mcdfShareIndividualDropdownSelection, StringComparison.OrdinalIgnoreCase);
if (ImGui.Selectable(display, selected))
{
_mcdfShareIndividualDropdownSelection = normalized;
_mcdfShareIndividualInput = normalized;
}
}
}
private void DrawMcdfShareSyncshellDropdown()
{
ImGui.SetNextItemWidth(220f);
var previewSource = string.IsNullOrEmpty(_mcdfShareSyncshellDropdownSelection)
? _mcdfShareSyncshellInput
: _mcdfShareSyncshellDropdownSelection;
var previewLabel = string.IsNullOrEmpty(previewSource)
? "Sélectionner une syncshell..."
: FormatSyncshellLabel(previewSource);
using var combo = ImRaii.Combo("##mcdfShareSyncshellDropdown", previewLabel, ImGuiComboFlags.None);
if (!combo)
{
return;
}
foreach (var group in _pairManager.Groups.Values
.OrderBy(g => _serverConfigurationManager.GetNoteForGid(g.GID) ?? g.GroupAliasOrGID, StringComparer.OrdinalIgnoreCase))
{
var gid = group.GID;
var display = FormatSyncshellLabel(gid);
bool selected = string.Equals(gid, _mcdfShareSyncshellDropdownSelection, StringComparison.OrdinalIgnoreCase);
if (ImGui.Selectable(display, selected))
{
_mcdfShareSyncshellDropdownSelection = gid;
_mcdfShareSyncshellInput = gid;
}
}
}
private string NormalizeUidCandidate(string candidate)
{
if (string.IsNullOrWhiteSpace(candidate))
{
return string.Empty;
}
var trimmed = candidate.Trim();
foreach (var pair in _pairManager.DirectPairs)
{
var alias = pair.UserData.Alias;
var aliasOrUid = pair.UserData.AliasOrUID;
var note = pair.GetNoteOrName();
if (string.Equals(pair.UserData.UID, trimmed, StringComparison.OrdinalIgnoreCase)
|| (!string.IsNullOrEmpty(alias) && string.Equals(alias, trimmed, StringComparison.OrdinalIgnoreCase))
|| string.Equals(aliasOrUid, trimmed, StringComparison.OrdinalIgnoreCase)
|| (!string.IsNullOrEmpty(note) && string.Equals(note, trimmed, StringComparison.OrdinalIgnoreCase)))
{
return pair.UserData.UID;
}
}
return trimmed.ToUpperInvariant();
}
private string NormalizeSyncshellCandidate(string candidate)
{
if (string.IsNullOrWhiteSpace(candidate))
{
return string.Empty;
}
var trimmed = candidate.Trim();
foreach (var group in _pairManager.Groups.Values)
{
var alias = group.GroupAlias;
var aliasOrGid = group.GroupAliasOrGID;
var note = _serverConfigurationManager.GetNoteForGid(group.GID);
if (string.Equals(group.GID, trimmed, StringComparison.OrdinalIgnoreCase)
|| (!string.IsNullOrEmpty(alias) && string.Equals(alias, trimmed, StringComparison.OrdinalIgnoreCase))
|| string.Equals(aliasOrGid, trimmed, StringComparison.OrdinalIgnoreCase)
|| (!string.IsNullOrEmpty(note) && string.Equals(note, trimmed, StringComparison.OrdinalIgnoreCase)))
{
return group.GID;
}
}
return trimmed.ToUpperInvariant();
}
private string FormatPairLabel(string candidate)
{
if (string.IsNullOrEmpty(candidate))
{
return string.Empty;
}
foreach (var pair in _pairManager.DirectPairs)
{
var alias = pair.UserData.Alias;
var aliasOrUid = pair.UserData.AliasOrUID;
var note = pair.GetNoteOrName();
if (string.Equals(pair.UserData.UID, candidate, StringComparison.OrdinalIgnoreCase)
|| (!string.IsNullOrEmpty(alias) && string.Equals(alias, candidate, StringComparison.OrdinalIgnoreCase))
|| string.Equals(aliasOrUid, candidate, StringComparison.OrdinalIgnoreCase)
|| (!string.IsNullOrEmpty(note) && string.Equals(note, candidate, StringComparison.OrdinalIgnoreCase)))
{
return string.IsNullOrEmpty(note) ? aliasOrUid : $"{note} ({aliasOrUid})";
}
}
return candidate;
}
private string FormatSyncshellLabel(string candidate)
{
if (string.IsNullOrEmpty(candidate))
{
return string.Empty;
}
foreach (var group in _pairManager.Groups.Values)
{
var alias = group.GroupAlias;
var aliasOrGid = group.GroupAliasOrGID;
var note = _serverConfigurationManager.GetNoteForGid(group.GID);
if (string.Equals(group.GID, candidate, StringComparison.OrdinalIgnoreCase)
|| (!string.IsNullOrEmpty(alias) && string.Equals(alias, candidate, StringComparison.OrdinalIgnoreCase))
|| string.Equals(aliasOrGid, candidate, StringComparison.OrdinalIgnoreCase)
|| (!string.IsNullOrEmpty(note) && string.Equals(note, candidate, StringComparison.OrdinalIgnoreCase)))
{
return string.IsNullOrEmpty(note) ? aliasOrGid : $"{note} ({aliasOrGid})";
}
}
return candidate;
}
private void DrawMetaInfoData(string selectedGposeActor, bool hasValidGposeTarget, CharaDataMetaInfoExtendedDto data, bool canOpen = false) private void DrawMetaInfoData(string selectedGposeActor, bool hasValidGposeTarget, CharaDataMetaInfoExtendedDto data, bool canOpen = false)
{ {
ImGuiHelpers.ScaledDummy(5); ImGuiHelpers.ScaledDummy(5);

View File

@@ -14,6 +14,7 @@ using MareSynchronos.Services;
using MareSynchronos.Services.Mediator; using MareSynchronos.Services.Mediator;
using MareSynchronos.Services.ServerConfiguration; using MareSynchronos.Services.ServerConfiguration;
using MareSynchronos.Services.AutoDetect; using MareSynchronos.Services.AutoDetect;
using MareSynchronos.Services.Notifications;
using MareSynchronos.UI.Components; using MareSynchronos.UI.Components;
using MareSynchronos.UI.Handlers; using MareSynchronos.UI.Handlers;
using MareSynchronos.WebAPI; using MareSynchronos.WebAPI;
@@ -28,6 +29,7 @@ using System.Diagnostics;
using System.Globalization; using System.Globalization;
using System.Numerics; using System.Numerics;
using System.Reflection; using System.Reflection;
using System.Threading.Tasks;
using System.Linq; using System.Linq;
namespace MareSynchronos.UI; namespace MareSynchronos.UI;
@@ -57,6 +59,8 @@ public class CompactUi : WindowMediatorSubscriberBase
private readonly SettingsUi _settingsUi; private readonly SettingsUi _settingsUi;
private readonly AutoDetectUi _autoDetectUi; private readonly AutoDetectUi _autoDetectUi;
private readonly DataAnalysisUi _dataAnalysisUi; private readonly DataAnalysisUi _dataAnalysisUi;
private readonly CharaDataHubUi _charaDataHubUi;
private readonly NotificationTracker _notificationTracker;
private bool _buttonState; private bool _buttonState;
private string _characterOrCommentFilter = string.Empty; private string _characterOrCommentFilter = string.Empty;
private Pair? _lastAddedUser; private Pair? _lastAddedUser;
@@ -71,6 +75,7 @@ public class CompactUi : WindowMediatorSubscriberBase
private bool _visibleOpen = true; private bool _visibleOpen = true;
private bool _selfAnalysisOpen = false; private bool _selfAnalysisOpen = false;
private List<Services.Mediator.NearbyEntry> _nearbyEntries = new(); private List<Services.Mediator.NearbyEntry> _nearbyEntries = new();
private int _notificationCount;
private const long SelfAnalysisSizeWarningThreshold = 300L * 1024 * 1024; private const long SelfAnalysisSizeWarningThreshold = 300L * 1024 * 1024;
private const long SelfAnalysisTriangleWarningThreshold = 150_000; private const long SelfAnalysisTriangleWarningThreshold = 150_000;
private CompactUiSection _activeSection = CompactUiSection.VisiblePairs; private CompactUiSection _activeSection = CompactUiSection.VisiblePairs;
@@ -84,6 +89,7 @@ public class CompactUi : WindowMediatorSubscriberBase
private enum CompactUiSection private enum CompactUiSection
{ {
VisiblePairs, VisiblePairs,
Notifications,
IndividualPairs, IndividualPairs,
Syncshells, Syncshells,
AutoDetect, AutoDetect,
@@ -108,7 +114,9 @@ public class CompactUi : WindowMediatorSubscriberBase
EditProfileUi editProfileUi, EditProfileUi editProfileUi,
SettingsUi settingsUi, SettingsUi settingsUi,
AutoDetectUi autoDetectUi, AutoDetectUi autoDetectUi,
DataAnalysisUi dataAnalysisUi) DataAnalysisUi dataAnalysisUi,
CharaDataHubUi charaDataHubUi,
NotificationTracker notificationTracker)
: base(logger, mediator, "###UmbraSyncMainUI", performanceCollectorService) : base(logger, mediator, "###UmbraSyncMainUI", performanceCollectorService)
{ {
_uiSharedService = uiShared; _uiSharedService = uiShared;
@@ -126,6 +134,8 @@ public class CompactUi : WindowMediatorSubscriberBase
_settingsUi = settingsUi; _settingsUi = settingsUi;
_autoDetectUi = autoDetectUi; _autoDetectUi = autoDetectUi;
_dataAnalysisUi = dataAnalysisUi; _dataAnalysisUi = dataAnalysisUi;
_charaDataHubUi = charaDataHubUi;
_notificationTracker = notificationTracker;
var tagHandler = new TagHandler(_serverManager); var tagHandler = new TagHandler(_serverManager);
_groupPanel = new(this, uiShared, _pairManager, chatService, uidDisplayHandler, _serverManager, _charaDataManager, _autoDetectRequestService); _groupPanel = new(this, uiShared, _pairManager, chatService, uidDisplayHandler, _serverManager, _charaDataManager, _autoDetectRequestService);
@@ -162,6 +172,8 @@ public class CompactUi : WindowMediatorSubscriberBase
} }
} }
}); });
Mediator.Subscribe<NotificationStateChanged>(this, msg => _notificationCount = msg.TotalCount);
_notificationCount = _notificationTracker.Count;
Flags |= ImGuiWindowFlags.NoDocking; Flags |= ImGuiWindowFlags.NoDocking;
@@ -706,7 +718,7 @@ if (showNearby && pendingInvites > 0)
if (!showVisibleCard && !showNearbyCard) if (!showVisibleCard && !showNearbyCard)
{ {
const string calmMessage = "C'est bien trop calme ici... Il n'y a rien pour le moment."; const string calmMessage = "C'est bien trop calme ici... Il n'y a personne pour le moment.";
using (_uiSharedService.UidFont.Push()) using (_uiSharedService.UidFont.Push())
{ {
var regionMin = ImGui.GetWindowContentRegionMin(); var regionMin = ImGui.GetWindowContentRegionMin();
@@ -898,6 +910,8 @@ if (showNearby && pendingInvites > 0)
ImGuiHelpers.ScaledDummy(6f); ImGuiHelpers.ScaledDummy(6f);
DrawConnectionIcon(); DrawConnectionIcon();
ImGuiHelpers.ScaledDummy(12f); ImGuiHelpers.ScaledDummy(12f);
DrawSidebarButton(FontAwesomeIcon.Bell, "Notifications", CompactUiSection.Notifications, true, _notificationCount > 0, _notificationCount, null, ImGuiColors.DalamudOrange);
ImGuiHelpers.ScaledDummy(3f);
DrawSidebarButton(FontAwesomeIcon.Eye, "Visible pairs", CompactUiSection.VisiblePairs, isConnected); DrawSidebarButton(FontAwesomeIcon.Eye, "Visible pairs", CompactUiSection.VisiblePairs, isConnected);
ImGuiHelpers.ScaledDummy(3f); ImGuiHelpers.ScaledDummy(3f);
@@ -912,15 +926,9 @@ if (showNearby && pendingInvites > 0)
: "AutoDetect"; : "AutoDetect";
DrawSidebarButton(FontAwesomeIcon.BroadcastTower, autoDetectTooltip, CompactUiSection.AutoDetect, isConnected, highlightAutoDetect, pendingInvites); DrawSidebarButton(FontAwesomeIcon.BroadcastTower, autoDetectTooltip, CompactUiSection.AutoDetect, isConnected, highlightAutoDetect, pendingInvites);
ImGuiHelpers.ScaledDummy(3f); ImGuiHelpers.ScaledDummy(3f);
DrawSidebarButton(FontAwesomeIcon.PersonCircleQuestion, "Character Analysis", CompactUiSection.CharacterAnalysis, isConnected, _dataAnalysisUi.IsOpen, 0, () => DrawSidebarButton(FontAwesomeIcon.PersonCircleQuestion, "Character Analysis", CompactUiSection.CharacterAnalysis, isConnected);
{
Mediator.Publish(new UiToggleMessage(typeof(DataAnalysisUi)));
});
ImGuiHelpers.ScaledDummy(3f); ImGuiHelpers.ScaledDummy(3f);
DrawSidebarButton(FontAwesomeIcon.Running, "Character Data Hub", CompactUiSection.CharacterDataHub, isConnected, false, 0, () => DrawSidebarButton(FontAwesomeIcon.Running, "Character Data Hub", CompactUiSection.CharacterDataHub, isConnected);
{
Mediator.Publish(new UiToggleMessage(typeof(CharaDataHubUi)));
});
ImGuiHelpers.ScaledDummy(12f); ImGuiHelpers.ScaledDummy(12f);
DrawSidebarButton(FontAwesomeIcon.UserCircle, "Edit Profile", CompactUiSection.EditProfile, isConnected); DrawSidebarButton(FontAwesomeIcon.UserCircle, "Edit Profile", CompactUiSection.EditProfile, isConnected);
ImGuiHelpers.ScaledDummy(3f); ImGuiHelpers.ScaledDummy(3f);
@@ -930,7 +938,7 @@ if (showNearby && pendingInvites > 0)
}); });
} }
private void DrawSidebarButton(FontAwesomeIcon icon, string tooltip, CompactUiSection section, bool enabled = true, bool highlight = false, int badgeCount = 0, Action? onClick = null) private void DrawSidebarButton(FontAwesomeIcon icon, string tooltip, CompactUiSection section, bool enabled = true, bool highlight = false, int badgeCount = 0, Action? onClick = null, Vector4? highlightColor = null)
{ {
using var id = ImRaii.PushId((int)section); using var id = ImRaii.PushId((int)section);
float regionWidth = ImGui.GetContentRegionAvail().X; float regionWidth = ImGui.GetContentRegionAvail().X;
@@ -940,7 +948,7 @@ if (showNearby && pendingInvites > 0)
bool isActive = _activeSection == section; bool isActive = _activeSection == section;
if (DrawSidebarSquareButton(icon, isActive, highlight, enabled, badgeCount)) if (DrawSidebarSquareButton(icon, isActive, highlight, enabled, badgeCount, highlightColor))
{ {
if (onClick != null) if (onClick != null)
{ {
@@ -970,7 +978,7 @@ if (showNearby && pendingInvites > 0)
bool isTogglingDisabled = !hasServer || state is ServerState.Reconnecting or ServerState.Disconnecting; bool isTogglingDisabled = !hasServer || state is ServerState.Reconnecting or ServerState.Disconnecting;
if (DrawSidebarSquareButton(icon, isLinked, false, !isTogglingDisabled, 0) && !isTogglingDisabled) if (DrawSidebarSquareButton(icon, isLinked, false, !isTogglingDisabled, 0, null) && !isTogglingDisabled)
{ {
ToggleConnection(); ToggleConnection();
} }
@@ -988,7 +996,7 @@ if (showNearby && pendingInvites > 0)
} }
} }
private bool DrawSidebarSquareButton(FontAwesomeIcon icon, bool isActive, bool highlight, bool enabled, int badgeCount) private bool DrawSidebarSquareButton(FontAwesomeIcon icon, bool isActive, bool highlight, bool enabled, int badgeCount, Vector4? highlightColor)
{ {
float size = SidebarIconSize * ImGuiHelpers.GlobalScale; float size = SidebarIconSize * ImGuiHelpers.GlobalScale;
@@ -1021,9 +1029,14 @@ if (showNearby && pendingInvites > 0)
start.Y + (size - iconSize.Y) / 2f); start.Y + (size - iconSize.Y) / 2f);
uint iconColor = ImGui.ColorConvertFloat4ToU32(new Vector4(0.85f, 0.85f, 0.9f, 1f)); uint iconColor = ImGui.ColorConvertFloat4ToU32(new Vector4(0.85f, 0.85f, 0.9f, 1f));
if (highlight) if (highlight)
iconColor = ImGui.ColorConvertFloat4ToU32(new Vector4(0.45f, 0.85f, 0.45f, 1f)); {
var color = highlightColor ?? new Vector4(0.45f, 0.85f, 0.45f, 1f);
iconColor = ImGui.ColorConvertFloat4ToU32(color);
}
else if (isActive) else if (isActive)
{
iconColor = ImGui.GetColorU32(ImGuiCol.Text); iconColor = ImGui.GetColorU32(ImGuiCol.Text);
}
ImGui.GetWindowDrawList().AddText(textPos, iconColor, iconText); ImGui.GetWindowDrawList().AddText(textPos, iconColor, iconText);
} }
@@ -1093,6 +1106,9 @@ if (showNearby && pendingInvites > 0)
case CompactUiSection.VisiblePairs: case CompactUiSection.VisiblePairs:
DrawPairSection(PairContentMode.VisibleOnly); DrawPairSection(PairContentMode.VisibleOnly);
break; break;
case CompactUiSection.Notifications:
DrawNotificationsSection();
break;
case CompactUiSection.IndividualPairs: case CompactUiSection.IndividualPairs:
DrawPairSection(PairContentMode.All); DrawPairSection(PairContentMode.All);
break; break;
@@ -1102,6 +1118,14 @@ if (showNearby && pendingInvites > 0)
case CompactUiSection.AutoDetect: case CompactUiSection.AutoDetect:
DrawAutoDetectSection(); DrawAutoDetectSection();
break; break;
case CompactUiSection.CharacterAnalysis:
if (_dataAnalysisUi.IsOpen) _dataAnalysisUi.IsOpen = false;
_dataAnalysisUi.DrawInline();
break;
case CompactUiSection.CharacterDataHub:
if (_charaDataHubUi.IsOpen) _charaDataHubUi.IsOpen = false;
_charaDataHubUi.DrawInline();
break;
} }
DrawNewUserNoteModal(); DrawNewUserNoteModal();
@@ -1133,6 +1157,98 @@ if (showNearby && pendingInvites > 0)
using (ImRaii.PushId("autodetect-inline")) _autoDetectUi.DrawInline(); using (ImRaii.PushId("autodetect-inline")) _autoDetectUi.DrawInline();
} }
private void DrawNotificationsSection()
{
var notifications = _notificationTracker.GetEntries();
if (notifications.Count == 0)
{
UiSharedService.ColorTextWrapped("Aucune notification en attente.", ImGuiColors.DalamudGrey3);
return;
}
foreach (var notification in notifications.OrderByDescending(n => n.CreatedAt))
{
switch (notification.Category)
{
case NotificationCategory.AutoDetect:
DrawAutoDetectNotification(notification);
break;
default:
UiSharedService.DrawCard($"notification-{notification.Category}-{notification.Id}", () =>
{
ImGui.TextUnformatted(notification.Title);
if (!string.IsNullOrEmpty(notification.Description))
{
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey3);
ImGui.TextUnformatted(notification.Description);
ImGui.PopStyleColor();
}
}, stretchWidth: true);
break;
}
ImGuiHelpers.ScaledDummy(4f);
}
}
private void DrawAutoDetectNotification(NotificationEntry notification)
{
UiSharedService.DrawCard($"notification-autodetect-{notification.Id}", () =>
{
var label = _nearbyPending.Pending.TryGetValue(notification.Id, out var displayName)
? displayName
: notification.Title;
ImGui.TextUnformatted(label);
if (!string.IsNullOrEmpty(notification.Description))
{
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey3);
ImGui.TextWrapped(notification.Description);
ImGui.PopStyleColor();
}
ImGuiHelpers.ScaledDummy(3f);
bool hasPending = _nearbyPending.Pending.ContainsKey(notification.Id);
using (ImRaii.PushId(notification.Id))
{
using (ImRaii.Disabled(!hasPending))
{
if (ImGui.Button("Accepter"))
{
TriggerAcceptAutoDetectNotification(notification.Id);
}
ImGui.SameLine();
if (ImGui.Button("Refuser"))
{
_nearbyPending.Remove(notification.Id);
}
}
if (!hasPending)
{
ImGui.SameLine();
if (ImGui.Button("Effacer"))
{
_notificationTracker.Remove(NotificationCategory.AutoDetect, notification.Id);
}
}
}
}, stretchWidth: true);
}
private void TriggerAcceptAutoDetectNotification(string uid)
{
_ = Task.Run(async () =>
{
bool accepted = await _nearbyPending.AcceptAsync(uid).ConfigureAwait(false);
if (!accepted)
{
Mediator.Publish(new NotificationMessage("AutoDetect", $"Impossible d'accepter l'invitation {uid}.", NotificationType.Warning, TimeSpan.FromSeconds(5)));
}
});
}
private void DrawNewUserNoteModal() private void DrawNewUserNoteModal()
{ {
if (_configService.Current.OpenPopupOnAdd && _pairManager.LastAddedUser != null) if (_configService.Current.OpenPopupOnAdd && _pairManager.LastAddedUser != null)
@@ -1171,9 +1287,12 @@ if (showNearby && pendingInvites > 0)
private static bool RequiresServerConnection(CompactUiSection section) private static bool RequiresServerConnection(CompactUiSection section)
{ {
return section is CompactUiSection.VisiblePairs return section is CompactUiSection.VisiblePairs
or CompactUiSection.Notifications
or CompactUiSection.IndividualPairs or CompactUiSection.IndividualPairs
or CompactUiSection.Syncshells or CompactUiSection.Syncshells
or CompactUiSection.AutoDetect; or CompactUiSection.AutoDetect
or CompactUiSection.CharacterAnalysis
or CompactUiSection.CharacterDataHub;
} }
private bool IsAlreadyPairedQuickMenu(Services.Mediator.NearbyEntry entry) private bool IsAlreadyPairedQuickMenu(Services.Mediator.NearbyEntry entry)
@@ -1288,37 +1407,61 @@ if (showNearby && pendingInvites > 0)
var originalPos = ImGui.GetCursorPos(); var originalPos = ImGui.GetCursorPos();
UiSharedService.SetFontScale(1.5f); UiSharedService.SetFontScale(1.5f);
Vector2 buttonSize = Vector2.Zero;
float spacingX = ImGui.GetStyle().ItemSpacing.X; float spacingX = ImGui.GetStyle().ItemSpacing.X;
if (_apiController.ServerState is ServerState.Connected)
{
buttonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Copy);
ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth() - buttonSize.X);
ImGui.SetCursorPosY(originalPos.Y + uidTextSize.Y / 2f - buttonSize.Y / 2f);
if (_uiSharedService.IconButton(FontAwesomeIcon.Copy))
{
ImGui.SetClipboardText(_apiController.DisplayName);
}
UiSharedService.AttachToolTip("Copy your UID to clipboard");
ImGui.SameLine();
}
ImGui.SetCursorPos(originalPos);
UiSharedService.SetFontScale(1f);
float referenceHeight = buttonSize.Y > 0f ? buttonSize.Y : ImGui.GetFrameHeight();
ImGui.SetCursorPosY(originalPos.Y + referenceHeight / 2f - uidTextSize.Y / 2f - spacingX / 2f);
float contentMin = ImGui.GetWindowContentRegionMin().X; float contentMin = ImGui.GetWindowContentRegionMin().X;
float contentMax = ImGui.GetWindowContentRegionMax().X; float contentMax = ImGui.GetWindowContentRegionMax().X;
float availableWidth = contentMax - contentMin; float availableWidth = contentMax - contentMin;
float center = contentMin + availableWidth / 2f; float center = contentMin + availableWidth / 2f;
ImGui.SetCursorPosX(center - uidTextSize.X / 2f);
bool isConnected = _apiController.ServerState is ServerState.Connected;
float buttonSize = 18f * ImGuiHelpers.GlobalScale;
float textPosY = originalPos.Y + MathF.Max(buttonSize, uidTextSize.Y) / 2f - uidTextSize.Y / 2f;
float textPosX = center - uidTextSize.X / 2f;
if (isConnected)
{
float buttonX = textPosX - spacingX - buttonSize;
float buttonVerticalOffset = 7f * ImGuiHelpers.GlobalScale;
float buttonY = textPosY + uidTextSize.Y - buttonSize + buttonVerticalOffset;
ImGui.SetCursorPos(new Vector2(buttonX, buttonY));
if (ImGui.Button("##copy", new Vector2(buttonSize, buttonSize)))
{
ImGui.SetClipboardText(_apiController.DisplayName);
}
var buttonMin = ImGui.GetItemRectMin();
var drawList = ImGui.GetWindowDrawList();
using (_uiSharedService.IconFont.Push())
{
string iconText = FontAwesomeIcon.Copy.ToIconString();
var baseSize = ImGui.CalcTextSize(iconText);
float maxDimension = MathF.Max(MathF.Max(baseSize.X, baseSize.Y), 1f);
float available = buttonSize - 4f;
float scale = MathF.Min(1f, available / maxDimension);
float iconWidth = baseSize.X * scale;
float iconHeight = baseSize.Y * scale;
var iconPos = new Vector2(
buttonMin.X + (buttonSize - iconWidth) / 2f,
buttonMin.Y + (buttonSize - iconHeight) / 2f);
var font = ImGui.GetFont();
float fontSize = ImGui.GetFontSize() * scale;
drawList.AddText(font, fontSize, iconPos, ImGui.GetColorU32(ImGuiCol.Text), iconText);
}
UiSharedService.AttachToolTip("Copy your UID to clipboard");
ImGui.SameLine(0f, spacingX);
}
else
{
ImGui.SetCursorPos(originalPos);
}
ImGui.SetCursorPos(new Vector2(textPosX, textPosY));
using (_uiSharedService.UidFont.Push()) using (_uiSharedService.UidFont.Push())
ImGui.TextColored(GetUidColor(), uidText); ImGui.TextColored(GetUidColor(), uidText);
if (_apiController.ServerState is not ServerState.Connected) UiSharedService.SetFontScale(1f);
if (!isConnected)
UiSharedService.ColorTextWrapped(GetServerError(), GetUidColor()); UiSharedService.ColorTextWrapped(GetServerError(), GetUidColor());
{ {
if (_apiController.ServerState is ServerState.NoSecretKey) if (_apiController.ServerState is ServerState.NoSecretKey)

View File

@@ -65,6 +65,19 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
} }
protected override void DrawInternal() protected override void DrawInternal()
{
DrawAnalysisContent();
}
public void DrawInline()
{
using (ImRaii.PushId("CharacterAnalysisInline"))
{
DrawAnalysisContent();
}
}
private void DrawAnalysisContent()
{ {
if (_conversionTask != null && !_conversionTask.IsCompleted) if (_conversionTask != null && !_conversionTask.IsCompleted)
{ {
@@ -116,7 +129,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
if (isAnalyzing) if (isAnalyzing)
{ {
UiSharedService.ColorTextWrapped($"Analyzing {_characterAnalyzer.CurrentFile}/{_characterAnalyzer.TotalFiles}", UiSharedService.ColorTextWrapped($"Analyzing {_characterAnalyzer.CurrentFile}/{_characterAnalyzer.TotalFiles}",
ImGuiColors.DalamudYellow); UiSharedService.AccentColor);
if (_uiSharedService.IconTextButton(FontAwesomeIcon.StopCircle, "Cancel analysis")) if (_uiSharedService.IconTextButton(FontAwesomeIcon.StopCircle, "Cancel analysis"))
{ {
_characterAnalyzer.CancelAnalyze(); _characterAnalyzer.CancelAnalyze();
@@ -127,7 +140,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
if (needAnalysis) if (needAnalysis)
{ {
UiSharedService.ColorTextWrapped("Some entries in the analysis have file size not determined yet, press the button below to analyze your current data", UiSharedService.ColorTextWrapped("Some entries in the analysis have file size not determined yet, press the button below to analyze your current data",
ImGuiColors.DalamudYellow); UiSharedService.AccentColor);
if (_uiSharedService.IconTextButton(FontAwesomeIcon.PlayCircle, "Start analysis (missing entries)")) if (_uiSharedService.IconTextButton(FontAwesomeIcon.PlayCircle, "Start analysis (missing entries)"))
{ {
_ = _characterAnalyzer.ComputeAnalysis(print: false); _ = _characterAnalyzer.ComputeAnalysis(print: false);
@@ -166,7 +179,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
ImGui.TextUnformatted(UiSharedService.ByteToString(_cachedAnalysis!.Sum(c => c.Value.Sum(c => c.Value.OriginalSize)))); ImGui.TextUnformatted(UiSharedService.ByteToString(_cachedAnalysis!.Sum(c => c.Value.Sum(c => c.Value.OriginalSize))));
ImGui.TextUnformatted("Total size (download size):"); ImGui.TextUnformatted("Total size (download size):");
ImGui.SameLine(); ImGui.SameLine();
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudYellow, needAnalysis)) using (ImRaii.PushColor(ImGuiCol.Text, UiSharedService.AccentColor, needAnalysis))
{ {
ImGui.TextUnformatted(UiSharedService.ByteToString(_cachedAnalysis!.Sum(c => c.Value.Sum(c => c.Value.CompressedSize)))); ImGui.TextUnformatted(UiSharedService.ByteToString(_cachedAnalysis!.Sum(c => c.Value.Sum(c => c.Value.CompressedSize))));
if (needAnalysis && !isAnalyzing) if (needAnalysis && !isAnalyzing)
@@ -180,6 +193,10 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
ImGui.TextUnformatted($"Total modded model triangles: {UiSharedService.TrisToString(_cachedAnalysis.Sum(c => c.Value.Sum(f => f.Value.Triangles)))}"); ImGui.TextUnformatted($"Total modded model triangles: {UiSharedService.TrisToString(_cachedAnalysis.Sum(c => c.Value.Sum(f => f.Value.Triangles)))}");
ImGui.Separator(); ImGui.Separator();
{
using var objectTabColor = ImRaii.PushColor(ImGuiCol.Tab, UiSharedService.AccentColor);
using var objectTabHoverColor = ImRaii.PushColor(ImGuiCol.TabHovered, UiSharedService.AccentHoverColor);
using var objectTabActiveColor = ImRaii.PushColor(ImGuiCol.TabActive, UiSharedService.AccentActiveColor);
using var tabbar = ImRaii.TabBar("objectSelection"); using var tabbar = ImRaii.TabBar("objectSelection");
foreach (var kvp in _cachedAnalysis) foreach (var kvp in _cachedAnalysis)
{ {
@@ -213,7 +230,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
ImGui.TextUnformatted(UiSharedService.ByteToString(kvp.Value.Sum(c => c.Value.OriginalSize))); ImGui.TextUnformatted(UiSharedService.ByteToString(kvp.Value.Sum(c => c.Value.OriginalSize)));
ImGui.TextUnformatted($"{kvp.Key} size (download size):"); ImGui.TextUnformatted($"{kvp.Key} size (download size):");
ImGui.SameLine(); ImGui.SameLine();
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudYellow, needAnalysis)) using (ImRaii.PushColor(ImGuiCol.Text, UiSharedService.AccentColor, needAnalysis))
{ {
ImGui.TextUnformatted(UiSharedService.ByteToString(kvp.Value.Sum(c => c.Value.CompressedSize))); ImGui.TextUnformatted(UiSharedService.ByteToString(kvp.Value.Sum(c => c.Value.CompressedSize)));
if (needAnalysis && !isAnalyzing) if (needAnalysis && !isAnalyzing)
@@ -243,15 +260,17 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
_texturesToConvert.Clear(); _texturesToConvert.Clear();
} }
using var fileTabColor = ImRaii.PushColor(ImGuiCol.Tab, UiSharedService.AccentColor);
using var fileTabHoverColor = ImRaii.PushColor(ImGuiCol.TabHovered, UiSharedService.AccentHoverColor);
using var fileTabActiveColor = ImRaii.PushColor(ImGuiCol.TabActive, UiSharedService.AccentActiveColor);
using var fileTabBar = ImRaii.TabBar("fileTabs"); using var fileTabBar = ImRaii.TabBar("fileTabs");
foreach (IGrouping<string, CharacterAnalyzer.FileDataEntry>? fileGroup in groupedfiles) foreach (IGrouping<string, CharacterAnalyzer.FileDataEntry>? fileGroup in groupedfiles)
{ {
string fileGroupText = fileGroup.Key + " [" + fileGroup.Count() + "]"; string fileGroupText = fileGroup.Key + " [" + fileGroup.Count() + "]";
var requiresCompute = fileGroup.Any(k => !k.IsComputed); var requiresCompute = fileGroup.Any(k => !k.IsComputed);
using var tabcol = ImRaii.PushColor(ImGuiCol.Tab, UiSharedService.Color(ImGuiColors.DalamudYellow), requiresCompute);
ImRaii.IEndObject fileTab; ImRaii.IEndObject fileTab;
using (var textcol = ImRaii.PushColor(ImGuiCol.Text, UiSharedService.Color(new(0, 0, 0, 1)), using (var textcol = ImRaii.PushColor(ImGuiCol.Text, UiSharedService.Color(Vector4.One),
requiresCompute && !string.Equals(_selectedFileTypeTab, fileGroup.Key, StringComparison.Ordinal))) requiresCompute && !string.Equals(_selectedFileTypeTab, fileGroup.Key, StringComparison.Ordinal)))
{ {
fileTab = ImRaii.TabItem(fileGroupText + "###" + fileGroup.Key); fileTab = ImRaii.TabItem(fileGroupText + "###" + fileGroup.Key);
@@ -284,7 +303,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
ImGui.Checkbox("Enable BC7 Conversion Mode", ref _enableBc7ConversionMode); ImGui.Checkbox("Enable BC7 Conversion Mode", ref _enableBc7ConversionMode);
if (_enableBc7ConversionMode) if (_enableBc7ConversionMode)
{ {
UiSharedService.ColorText("WARNING BC7 CONVERSION:", ImGuiColors.DalamudYellow); UiSharedService.ColorText("WARNING BC7 CONVERSION:", UiSharedService.AccentColor);
ImGui.SameLine(); ImGui.SameLine();
UiSharedService.ColorText("Converting textures to BC7 is irreversible!", UiSharedService.AccentColor); UiSharedService.ColorText("Converting textures to BC7 is irreversible!", UiSharedService.AccentColor);
UiSharedService.ColorTextWrapped("- Converting textures to BC7 will reduce their size (compressed and uncompressed) drastically. It is recommended to be used for large (4k+) textures." + UiSharedService.ColorTextWrapped("- Converting textures to BC7 will reduce their size (compressed and uncompressed) drastically. It is recommended to be used for large (4k+) textures." +
@@ -292,7 +311,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
Environment.NewLine + "- Before converting textures, make sure to have the original files of the mod you are converting so you can reimport it in case of issues." + Environment.NewLine + "- Before converting textures, make sure to have the original files of the mod you are converting so you can reimport it in case of issues." +
Environment.NewLine + "- Conversion will convert all found texture duplicates (entries with more than 1 file path) automatically." + Environment.NewLine + "- Conversion will convert all found texture duplicates (entries with more than 1 file path) automatically." +
Environment.NewLine + "- Converting textures to BC7 is a very expensive operation and, depending on the amount of textures to convert, will take a while to complete." Environment.NewLine + "- Converting textures to BC7 is a very expensive operation and, depending on the amount of textures to convert, will take a while to complete."
, ImGuiColors.DalamudYellow); , UiSharedService.AccentColor);
if (_texturesToConvert.Count > 0 && _uiSharedService.IconTextButton(FontAwesomeIcon.PlayCircle, "Start conversion of " + _texturesToConvert.Count + " texture(s)")) if (_texturesToConvert.Count > 0 && _uiSharedService.IconTextButton(FontAwesomeIcon.PlayCircle, "Start conversion of " + _texturesToConvert.Count + " texture(s)"))
{ {
var conversionCts = EnsureFreshCts(ref _conversionCancellationTokenSource); var conversionCts = EnsureFreshCts(ref _conversionCancellationTokenSource);
@@ -306,14 +325,17 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
fileTab.Dispose(); fileTab.Dispose();
} }
} }
} }
}
ImGui.Separator(); ImGui.Separator();
ImGui.TextUnformatted("Selected file:"); ImGui.TextUnformatted("Selected file:");
ImGui.SameLine(); ImGui.SameLine();
UiSharedService.ColorText(_selectedHash, ImGuiColors.DalamudYellow); UiSharedService.ColorText(_selectedHash, UiSharedService.AccentColor);
if (_cachedAnalysis[_selectedObjectTab].TryGetValue(_selectedHash, out CharacterAnalyzer.FileDataEntry? item)) if (_cachedAnalysis[_selectedObjectTab].TryGetValue(_selectedHash, out CharacterAnalyzer.FileDataEntry? item))
{ {
@@ -440,8 +462,8 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
ImGui.TableNextColumn(); ImGui.TableNextColumn();
if (string.Equals(_selectedHash, item.Hash, StringComparison.Ordinal)) if (string.Equals(_selectedHash, item.Hash, StringComparison.Ordinal))
{ {
ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg1, UiSharedService.Color(ImGuiColors.DalamudYellow)); ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg1, UiSharedService.Color(UiSharedService.AccentColor));
ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg0, UiSharedService.Color(ImGuiColors.DalamudYellow)); ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg0, UiSharedService.Color(UiSharedService.AccentColor));
} }
ImGui.TextUnformatted(item.Hash); ImGui.TextUnformatted(item.Hash);
if (ImGui.IsItemClicked()) _selectedHash = item.Hash; if (ImGui.IsItemClicked()) _selectedHash = item.Hash;
@@ -455,7 +477,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
ImGui.TextUnformatted(UiSharedService.ByteToString(item.OriginalSize)); ImGui.TextUnformatted(UiSharedService.ByteToString(item.OriginalSize));
if (ImGui.IsItemClicked()) _selectedHash = item.Hash; if (ImGui.IsItemClicked()) _selectedHash = item.Hash;
ImGui.TableNextColumn(); ImGui.TableNextColumn();
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudYellow, !item.IsComputed)) using (ImRaii.PushColor(ImGuiCol.Text, UiSharedService.AccentColor, !item.IsComputed))
ImGui.TextUnformatted(UiSharedService.ByteToString(item.CompressedSize)); ImGui.TextUnformatted(UiSharedService.ByteToString(item.CompressedSize));
if (ImGui.IsItemClicked()) _selectedHash = item.Hash; if (ImGui.IsItemClicked()) _selectedHash = item.Hash;
if (string.Equals(fileGroup.Key, "tex", StringComparison.Ordinal)) if (string.Equals(fileGroup.Key, "tex", StringComparison.Ordinal))

View File

@@ -3,15 +3,19 @@ using Dalamud.Interface;
using Dalamud.Interface.Colors; using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility; using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Utility.Raii;
using System;
using MareSynchronos.API.Data.Enum; using MareSynchronos.API.Data.Enum;
using MareSynchronos.API.Data.Extensions; using MareSynchronos.API.Data.Extensions;
using MareSynchronos.API.Dto.Group; using MareSynchronos.API.Dto.Group;
using MareSynchronos.PlayerData.Pairs; using MareSynchronos.PlayerData.Pairs;
using MareSynchronos.Services; using MareSynchronos.Services;
using MareSynchronos.Services.AutoDetect;
using MareSynchronos.Services.Mediator; using MareSynchronos.Services.Mediator;
using MareSynchronos.WebAPI; using MareSynchronos.WebAPI;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Globalization; using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
namespace MareSynchronos.UI.Components.Popup; namespace MareSynchronos.UI.Components.Popup;
@@ -23,6 +27,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
private readonly List<string> _oneTimeInvites = []; private readonly List<string> _oneTimeInvites = [];
private readonly PairManager _pairManager; private readonly PairManager _pairManager;
private readonly UiSharedService _uiSharedService; private readonly UiSharedService _uiSharedService;
private readonly SyncshellDiscoveryService _syncshellDiscoveryService;
private List<BannedGroupUserDto> _bannedUsers = []; private List<BannedGroupUserDto> _bannedUsers = [];
private int _multiInvites; private int _multiInvites;
private string _newPassword; private string _newPassword;
@@ -30,20 +35,31 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
private Task<int>? _pruneTestTask; private Task<int>? _pruneTestTask;
private Task<int>? _pruneTask; private Task<int>? _pruneTask;
private int _pruneDays = 14; private int _pruneDays = 14;
private bool _autoDetectStateInitialized;
private bool _autoDetectStateLoading;
private bool _autoDetectToggleInFlight;
private bool _autoDetectVisible;
private bool _autoDetectPasswordDisabled;
private string? _autoDetectMessage;
public SyncshellAdminUI(ILogger<SyncshellAdminUI> logger, MareMediator mediator, ApiController apiController, public SyncshellAdminUI(ILogger<SyncshellAdminUI> logger, MareMediator mediator, ApiController apiController,
UiSharedService uiSharedService, PairManager pairManager, GroupFullInfoDto groupFullInfo, PerformanceCollectorService performanceCollectorService) UiSharedService uiSharedService, PairManager pairManager, SyncshellDiscoveryService syncshellDiscoveryService,
GroupFullInfoDto groupFullInfo, PerformanceCollectorService performanceCollectorService)
: base(logger, mediator, "Syncshell Admin Panel (" + groupFullInfo.GroupAliasOrGID + ")", performanceCollectorService) : base(logger, mediator, "Syncshell Admin Panel (" + groupFullInfo.GroupAliasOrGID + ")", performanceCollectorService)
{ {
GroupFullInfo = groupFullInfo; GroupFullInfo = groupFullInfo;
_apiController = apiController; _apiController = apiController;
_uiSharedService = uiSharedService; _uiSharedService = uiSharedService;
_pairManager = pairManager; _pairManager = pairManager;
_syncshellDiscoveryService = syncshellDiscoveryService;
_isOwner = string.Equals(GroupFullInfo.OwnerUID, _apiController.UID, StringComparison.Ordinal); _isOwner = string.Equals(GroupFullInfo.OwnerUID, _apiController.UID, StringComparison.Ordinal);
_isModerator = GroupFullInfo.GroupUserInfo.IsModerator(); _isModerator = GroupFullInfo.GroupUserInfo.IsModerator();
_newPassword = string.Empty; _newPassword = string.Empty;
_multiInvites = 30; _multiInvites = 30;
_pwChangeSuccess = true; _pwChangeSuccess = true;
_autoDetectVisible = groupFullInfo.AutoDetectVisible;
_autoDetectPasswordDisabled = groupFullInfo.PasswordTemporarilyDisabled;
Mediator.Subscribe<SyncshellAutoDetectStateChanged>(this, OnSyncshellAutoDetectStateChanged);
IsOpen = true; IsOpen = true;
SizeConstraints = new WindowSizeConstraints() SizeConstraints = new WindowSizeConstraints()
{ {
@@ -59,6 +75,11 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
if (!_isModerator && !_isOwner) return; if (!_isModerator && !_isOwner) return;
GroupFullInfo = _pairManager.Groups[GroupFullInfo.Group]; GroupFullInfo = _pairManager.Groups[GroupFullInfo.Group];
if (!_autoDetectToggleInFlight && !_autoDetectStateLoading)
{
_autoDetectVisible = GroupFullInfo.AutoDetectVisible;
_autoDetectPasswordDisabled = GroupFullInfo.PasswordTemporarilyDisabled;
}
using var id = ImRaii.PushId("syncshell_admin_" + GroupFullInfo.GID); using var id = ImRaii.PushId("syncshell_admin_" + GroupFullInfo.GID);
@@ -363,6 +384,13 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
} }
mgmtTab.Dispose(); mgmtTab.Dispose();
var discoveryTab = ImRaii.TabItem("AutoDetect");
if (discoveryTab)
{
DrawAutoDetectTab();
}
discoveryTab.Dispose();
var permissionTab = ImRaii.TabItem("Permissions"); var permissionTab = ImRaii.TabItem("Permissions");
if (permissionTab) if (permissionTab)
{ {
@@ -448,6 +476,128 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
} }
} }
private void DrawAutoDetectTab()
{
if (!_autoDetectStateInitialized && !_autoDetectStateLoading)
{
_autoDetectStateInitialized = true;
_autoDetectStateLoading = true;
_ = EnsureAutoDetectStateAsync();
}
UiSharedService.TextWrapped("Activer l'affichage AutoDetect rend la Syncshell visible dans l'onglet AutoDetect et désactive temporairement le mot de passe.");
ImGuiHelpers.ScaledDummy(4);
if (_autoDetectStateLoading)
{
ImGui.TextDisabled("Chargement de l'état en cours...");
}
if (!string.IsNullOrEmpty(_autoDetectMessage))
{
UiSharedService.ColorTextWrapped(_autoDetectMessage!, ImGuiColors.DalamudYellow);
}
bool desiredVisibility = _autoDetectVisible;
using (ImRaii.Disabled(_autoDetectToggleInFlight || _autoDetectStateLoading))
{
if (ImGui.Checkbox("Afficher cette Syncshell dans l'AutoDetect", ref desiredVisibility))
{
_ = ToggleAutoDetectAsync(desiredVisibility);
}
}
_uiSharedService.DrawHelpText("Quand cette option est activée, le mot de passe devient inactif tant que la visibilité est maintenue.");
if (_autoDetectPasswordDisabled && _autoDetectVisible)
{
UiSharedService.ColorTextWrapped("Le mot de passe est actuellement désactivé pendant la visibilité AutoDetect.", ImGuiColors.DalamudYellow);
}
ImGuiHelpers.ScaledDummy(6);
if (ImGui.Button("Recharger l'état"))
{
_autoDetectStateLoading = true;
_ = EnsureAutoDetectStateAsync(true);
}
}
private async Task EnsureAutoDetectStateAsync(bool force = false)
{
try
{
var state = await _syncshellDiscoveryService.GetStateAsync(GroupFullInfo.GID, CancellationToken.None).ConfigureAwait(false);
if (state != null)
{
ApplyAutoDetectState(state.AutoDetectVisible, state.PasswordTemporarilyDisabled, true);
_autoDetectMessage = null;
}
else if (force)
{
_autoDetectMessage = "Impossible de récupérer l'état AutoDetect.";
}
}
catch (Exception ex)
{
_autoDetectMessage = force ? $"Erreur lors du rafraîchissement : {ex.Message}" : _autoDetectMessage;
}
finally
{
_autoDetectStateLoading = false;
}
}
private async Task ToggleAutoDetectAsync(bool desiredVisibility)
{
if (_autoDetectToggleInFlight)
{
return;
}
_autoDetectToggleInFlight = true;
_autoDetectMessage = null;
try
{
var success = await _syncshellDiscoveryService.SetVisibilityAsync(GroupFullInfo.GID, desiredVisibility, CancellationToken.None).ConfigureAwait(false);
if (!success)
{
_autoDetectMessage = "Impossible de mettre à jour la visibilité AutoDetect.";
return;
}
await EnsureAutoDetectStateAsync(true).ConfigureAwait(false);
_autoDetectMessage = desiredVisibility
? "La Syncshell est désormais visible dans AutoDetect."
: "La Syncshell n'est plus visible dans AutoDetect.";
}
catch (Exception ex)
{
_autoDetectMessage = $"Erreur lors de la mise à jour AutoDetect : {ex.Message}";
}
finally
{
_autoDetectToggleInFlight = false;
}
}
private void ApplyAutoDetectState(bool visible, bool passwordDisabled, bool fromServer)
{
_autoDetectVisible = visible;
_autoDetectPasswordDisabled = passwordDisabled;
if (fromServer)
{
GroupFullInfo.AutoDetectVisible = visible;
GroupFullInfo.PasswordTemporarilyDisabled = passwordDisabled;
}
}
private void OnSyncshellAutoDetectStateChanged(SyncshellAutoDetectStateChanged msg)
{
if (!string.Equals(msg.Gid, GroupFullInfo.GID, StringComparison.OrdinalIgnoreCase)) return;
ApplyAutoDetectState(msg.Visible, msg.PasswordTemporarilyDisabled, true);
_autoDetectMessage = null;
}
public override void OnClose() public override void OnClose()
{ {
Mediator.Publish(new RemoveWindowMessage(this)); Mediator.Publish(new RemoveWindowMessage(this));

View File

@@ -124,7 +124,11 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
e.OnPreBuild(tk => tk.AddDalamudAssetFont(Dalamud.DalamudAsset.NotoSansJpMedium, new() e.OnPreBuild(tk => tk.AddDalamudAssetFont(Dalamud.DalamudAsset.NotoSansJpMedium, new()
{ {
SizePx = 27, SizePx = 27,
GlyphRanges = [0x20, 0x7E, 0] GlyphRanges = [
0x0020, 0x007E,
0x00A0, 0x017F,
0
]
})); }));
}); });
GameFont = _pluginInterface.UiBuilder.FontAtlas.NewGameFontHandle(new(GameFontFamilyAndSize.Axis12)); GameFont = _pluginInterface.UiBuilder.FontAtlas.NewGameFontHandle(new(GameFontFamilyAndSize.Axis12));

View File

@@ -0,0 +1,94 @@
using System;
using System.Collections.Generic;
using MareSynchronos.API.Dto.McdfShare;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.Logging;
namespace MareSynchronos.WebAPI;
public sealed partial class ApiController
{
public async Task<List<McdfShareEntryDto>> McdfShareGetOwn()
{
if (!IsConnected) return [];
try
{
return await _mareHub!.InvokeAsync<List<McdfShareEntryDto>>(nameof(McdfShareGetOwn)).ConfigureAwait(false);
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Error during {method}", nameof(McdfShareGetOwn));
return [];
}
}
public async Task<List<McdfShareEntryDto>> McdfShareGetShared()
{
if (!IsConnected) return [];
try
{
return await _mareHub!.InvokeAsync<List<McdfShareEntryDto>>(nameof(McdfShareGetShared)).ConfigureAwait(false);
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Error during {method}", nameof(McdfShareGetShared));
return [];
}
}
public async Task McdfShareUpload(McdfShareUploadRequestDto requestDto)
{
if (!IsConnected) return;
try
{
await _mareHub!.InvokeAsync(nameof(McdfShareUpload), requestDto).ConfigureAwait(false);
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Error during {method}", nameof(McdfShareUpload));
throw;
}
}
public async Task<McdfSharePayloadDto?> McdfShareDownload(Guid shareId)
{
if (!IsConnected) return null;
try
{
return await _mareHub!.InvokeAsync<McdfSharePayloadDto?>(nameof(McdfShareDownload), shareId).ConfigureAwait(false);
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Error during {method}", nameof(McdfShareDownload));
throw;
}
}
public async Task<bool> McdfShareDelete(Guid shareId)
{
if (!IsConnected) return false;
try
{
return await _mareHub!.InvokeAsync<bool>(nameof(McdfShareDelete), shareId).ConfigureAwait(false);
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Error during {method}", nameof(McdfShareDelete));
throw;
}
}
public async Task<McdfShareEntryDto?> McdfShareUpdate(McdfShareUpdateRequestDto requestDto)
{
if (!IsConnected) return null;
try
{
return await _mareHub!.InvokeAsync<McdfShareEntryDto?>(nameof(McdfShareUpdate), requestDto).ConfigureAwait(false);
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Error during {method}", nameof(McdfShareUpdate));
throw;
}
}
}

View File

@@ -0,0 +1,32 @@
using System.Collections.Generic;
using MareSynchronos.API.Dto.Group;
using Microsoft.AspNetCore.SignalR.Client;
namespace MareSynchronos.WebAPI;
public partial class ApiController
{
public async Task<List<SyncshellDiscoveryEntryDto>> SyncshellDiscoveryList()
{
CheckConnection();
return await _mareHub!.InvokeAsync<List<SyncshellDiscoveryEntryDto>>(nameof(SyncshellDiscoveryList)).ConfigureAwait(false);
}
public async Task<SyncshellDiscoveryStateDto?> SyncshellDiscoveryGetState(GroupDto group)
{
CheckConnection();
return await _mareHub!.InvokeAsync<SyncshellDiscoveryStateDto?>(nameof(SyncshellDiscoveryGetState), group).ConfigureAwait(false);
}
public async Task<bool> SyncshellDiscoverySetVisibility(SyncshellDiscoveryVisibilityRequestDto request)
{
CheckConnection();
return await _mareHub!.InvokeAsync<bool>(nameof(SyncshellDiscoverySetVisibility), request).ConfigureAwait(false);
}
public async Task<bool> SyncshellDiscoveryJoin(GroupDto group)
{
CheckConnection();
return await _mareHub!.InvokeAsync<bool>(nameof(SyncshellDiscoveryJoin), group).ConfigureAwait(false);
}
}