Compare commits
17 Commits
fr-transla
...
620ebf9195
| Author | SHA1 | Date | |
|---|---|---|---|
|
620ebf9195
|
|||
|
8cc4f34c55
|
|||
|
513845b811
|
|||
|
84586cac3d
|
|||
|
b4108c7803
|
|||
|
d891dceb28
|
|||
|
89fa1a999f
|
|||
|
1f6e86ec2d
|
|||
|
d225a3844a
|
|||
|
d4a46910f9
|
|||
|
b59a579f56
|
|||
|
7706ef1fa7
|
|||
|
fca730557e
|
|||
|
6572fdcc27
|
|||
|
bf770f19d9
|
|||
|
78089a9fc7
|
|||
|
3c81e1f243
|
Submodule Glamourer.Api updated: 54c1944dc7...59a7ab5fa9
2
MareAPI
2
MareAPI
Submodule MareAPI updated: 7a48ca9823...deb911cb0a
@@ -1,4 +1,5 @@
|
|||||||
using MareSynchronos.Interop.Ipc;
|
using System;
|
||||||
|
using MareSynchronos.Interop.Ipc;
|
||||||
using MareSynchronos.MareConfiguration;
|
using MareSynchronos.MareConfiguration;
|
||||||
using MareSynchronos.Services;
|
using MareSynchronos.Services;
|
||||||
using MareSynchronos.Services.Mediator;
|
using MareSynchronos.Services.Mediator;
|
||||||
@@ -606,14 +607,35 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
|||||||
protected override void Dispose(bool disposing)
|
protected override void Dispose(bool disposing)
|
||||||
{
|
{
|
||||||
base.Dispose(disposing);
|
base.Dispose(disposing);
|
||||||
_scanCancellationTokenSource?.Cancel();
|
try
|
||||||
|
{
|
||||||
|
_scanCancellationTokenSource.Cancel();
|
||||||
|
}
|
||||||
|
catch (ObjectDisposedException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
_scanCancellationTokenSource.Dispose();
|
||||||
PenumbraWatcher?.Dispose();
|
PenumbraWatcher?.Dispose();
|
||||||
MareWatcher?.Dispose();
|
MareWatcher?.Dispose();
|
||||||
SubstWatcher?.Dispose();
|
SubstWatcher?.Dispose();
|
||||||
_penumbraFswCts?.CancelDispose();
|
TryCancelAndDispose(_penumbraFswCts);
|
||||||
_mareFswCts?.CancelDispose();
|
TryCancelAndDispose(_mareFswCts);
|
||||||
_substFswCts?.CancelDispose();
|
TryCancelAndDispose(_substFswCts);
|
||||||
_periodicCalculationTokenSource?.CancelDispose();
|
TryCancelAndDispose(_periodicCalculationTokenSource);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TryCancelAndDispose(CancellationTokenSource? cts)
|
||||||
|
{
|
||||||
|
if (cts == null) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
cts.Cancel();
|
||||||
|
}
|
||||||
|
catch (ObjectDisposedException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
cts.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void FullFileScan(CancellationToken ct)
|
private void FullFileScan(CancellationToken ct)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ public sealed class FileCompactor
|
|||||||
|
|
||||||
private readonly Dictionary<string, int> _clusterSizes;
|
private readonly Dictionary<string, int> _clusterSizes;
|
||||||
|
|
||||||
private readonly WOF_FILE_COMPRESSION_INFO_V1 _efInfo;
|
private readonly WofFileCompressionInfoV1 _efInfo;
|
||||||
private readonly ILogger<FileCompactor> _logger;
|
private readonly ILogger<FileCompactor> _logger;
|
||||||
|
|
||||||
private readonly MareConfigService _mareConfigService;
|
private readonly MareConfigService _mareConfigService;
|
||||||
@@ -24,7 +24,7 @@ public sealed class FileCompactor
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
_mareConfigService = mareConfigService;
|
_mareConfigService = mareConfigService;
|
||||||
_dalamudUtilService = dalamudUtilService;
|
_dalamudUtilService = dalamudUtilService;
|
||||||
_efInfo = new WOF_FILE_COMPRESSION_INFO_V1
|
_efInfo = new WofFileCompressionInfoV1
|
||||||
{
|
{
|
||||||
Algorithm = CompressionAlgorithm.XPRESS8K,
|
Algorithm = CompressionAlgorithm.XPRESS8K,
|
||||||
Flags = 0
|
Flags = 0
|
||||||
@@ -123,7 +123,7 @@ public sealed class FileCompactor
|
|||||||
out uint lpTotalNumberOfClusters);
|
out uint lpTotalNumberOfClusters);
|
||||||
|
|
||||||
[DllImport("WoFUtil.dll")]
|
[DllImport("WoFUtil.dll")]
|
||||||
private static extern int WofIsExternalFile([MarshalAs(UnmanagedType.LPWStr)] string Filepath, out int IsExternalFile, out uint Provider, out WOF_FILE_COMPRESSION_INFO_V1 Info, ref uint BufferLength);
|
private static extern int WofIsExternalFile([MarshalAs(UnmanagedType.LPWStr)] string Filepath, out int IsExternalFile, out uint Provider, out WofFileCompressionInfoV1 Info, ref uint BufferLength);
|
||||||
|
|
||||||
[DllImport("WofUtil.dll")]
|
[DllImport("WofUtil.dll")]
|
||||||
private static extern int WofSetFileDataLocation(IntPtr FileHandle, ulong Provider, IntPtr ExternalFileInfo, ulong Length);
|
private static extern int WofSetFileDataLocation(IntPtr FileHandle, ulong Provider, IntPtr ExternalFileInfo, ulong Length);
|
||||||
@@ -242,7 +242,7 @@ public sealed class FileCompactor
|
|||||||
}
|
}
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Sequential)]
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
private struct WOF_FILE_COMPRESSION_INFO_V1
|
private struct WofFileCompressionInfoV1
|
||||||
{
|
{
|
||||||
public CompressionAlgorithm Algorithm;
|
public CompressionAlgorithm Algorithm;
|
||||||
public ulong Flags;
|
public ulong Flags;
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ namespace MareSynchronos.Interop;
|
|||||||
|
|
||||||
public record ChatChannelOverride
|
public record ChatChannelOverride
|
||||||
{
|
{
|
||||||
public string ChannelName = string.Empty;
|
public string ChannelName { get; set; } = string.Empty;
|
||||||
public Action<byte[]>? ChatMessageHandler;
|
public Action<byte[]>? ChatMessageHandler { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public unsafe sealed class GameChatHooks : IDisposable
|
public unsafe sealed class GameChatHooks : IDisposable
|
||||||
|
|||||||
@@ -31,11 +31,11 @@ public class MdlFile
|
|||||||
public ushort Unknown9;
|
public ushort Unknown9;
|
||||||
|
|
||||||
// Offsets are stored relative to RuntimeSize instead of file start.
|
// Offsets are stored relative to RuntimeSize instead of file start.
|
||||||
public uint[] VertexOffset = [0, 0, 0];
|
public uint[] VertexOffset;
|
||||||
public uint[] IndexOffset = [0, 0, 0];
|
public uint[] IndexOffset;
|
||||||
|
|
||||||
public uint[] VertexBufferSize = [0, 0, 0];
|
public uint[] VertexBufferSize;
|
||||||
public uint[] IndexBufferSize = [0, 0, 0];
|
public uint[] IndexBufferSize;
|
||||||
public byte LodCount;
|
public byte LodCount;
|
||||||
public bool EnableIndexBufferStreaming;
|
public bool EnableIndexBufferStreaming;
|
||||||
public bool EnableEdgeGeometry;
|
public bool EnableEdgeGeometry;
|
||||||
@@ -43,15 +43,26 @@ public class MdlFile
|
|||||||
public ModelFlags1 Flags1;
|
public ModelFlags1 Flags1;
|
||||||
public ModelFlags2 Flags2;
|
public ModelFlags2 Flags2;
|
||||||
|
|
||||||
public VertexDeclarationStruct[] VertexDeclarations = [];
|
public VertexDeclarationStruct[] VertexDeclarations;
|
||||||
public ElementIdStruct[] ElementIds = [];
|
public ElementIdStruct[] ElementIds;
|
||||||
public MeshStruct[] Meshes = [];
|
public MeshStruct[] Meshes;
|
||||||
public BoundingBoxStruct[] BoneBoundingBoxes = [];
|
public BoundingBoxStruct[] BoneBoundingBoxes;
|
||||||
public LodStruct[] Lods = [];
|
public LodStruct[] Lods;
|
||||||
public ExtraLodStruct[] ExtraLods = [];
|
public ExtraLodStruct[] ExtraLods;
|
||||||
|
|
||||||
public MdlFile(string filePath)
|
public MdlFile(string filePath)
|
||||||
{
|
{
|
||||||
|
VertexOffset = Array.Empty<uint>();
|
||||||
|
IndexOffset = Array.Empty<uint>();
|
||||||
|
VertexBufferSize = Array.Empty<uint>();
|
||||||
|
IndexBufferSize = Array.Empty<uint>();
|
||||||
|
VertexDeclarations = Array.Empty<VertexDeclarationStruct>();
|
||||||
|
ElementIds = Array.Empty<ElementIdStruct>();
|
||||||
|
Meshes = Array.Empty<MeshStruct>();
|
||||||
|
BoneBoundingBoxes = Array.Empty<BoundingBoxStruct>();
|
||||||
|
Lods = Array.Empty<LodStruct>();
|
||||||
|
ExtraLods = Array.Empty<ExtraLodStruct>();
|
||||||
|
|
||||||
using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
|
using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
|
||||||
using var r = new LuminaBinaryReader(stream);
|
using var r = new LuminaBinaryReader(stream);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Dalamud.Game.ClientState.Objects.Types;
|
using Dalamud.Game.ClientState.Objects.Types;
|
||||||
|
using System;
|
||||||
using Dalamud.Plugin;
|
using Dalamud.Plugin;
|
||||||
using Dalamud.Plugin.Ipc;
|
using Dalamud.Plugin.Ipc;
|
||||||
using MareSynchronos.MareConfiguration;
|
using MareSynchronos.MareConfiguration;
|
||||||
@@ -29,7 +30,7 @@ public class IpcProvider : IHostedService, IMediatorSubscriber
|
|||||||
private bool _marePluginEnabled = false;
|
private bool _marePluginEnabled = false;
|
||||||
private bool _impersonating = false;
|
private bool _impersonating = false;
|
||||||
private DateTime _unregisterTime = DateTime.UtcNow;
|
private DateTime _unregisterTime = DateTime.UtcNow;
|
||||||
private CancellationTokenSource _registerDelayCts = new();
|
private CancellationTokenSource? _registerDelayCts = new();
|
||||||
|
|
||||||
public bool MarePluginEnabled => _marePluginEnabled;
|
public bool MarePluginEnabled => _marePluginEnabled;
|
||||||
public bool ImpersonationActive => _impersonating;
|
public bool ImpersonationActive => _impersonating;
|
||||||
@@ -100,7 +101,7 @@ public class IpcProvider : IHostedService, IMediatorSubscriber
|
|||||||
{
|
{
|
||||||
if (_mareConfig.Current.MareAPI)
|
if (_mareConfig.Current.MareAPI)
|
||||||
{
|
{
|
||||||
var cancelToken = _registerDelayCts.Token;
|
var cancelToken = EnsureFreshCts(ref _registerDelayCts).Token;
|
||||||
Task.Run(async () =>
|
Task.Run(async () =>
|
||||||
{
|
{
|
||||||
// Wait before registering to reduce the chance of a race condition
|
// Wait before registering to reduce the chance of a race condition
|
||||||
@@ -125,7 +126,7 @@ public class IpcProvider : IHostedService, IMediatorSubscriber
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_registerDelayCts = _registerDelayCts.CancelRecreate();
|
EnsureFreshCts(ref _registerDelayCts);
|
||||||
if (_impersonating)
|
if (_impersonating)
|
||||||
{
|
{
|
||||||
_loadFileProviderMare?.UnregisterFunc();
|
_loadFileProviderMare?.UnregisterFunc();
|
||||||
@@ -146,7 +147,7 @@ public class IpcProvider : IHostedService, IMediatorSubscriber
|
|||||||
_loadFileAsyncProvider?.UnregisterFunc();
|
_loadFileAsyncProvider?.UnregisterFunc();
|
||||||
_handledGameAddresses?.UnregisterFunc();
|
_handledGameAddresses?.UnregisterFunc();
|
||||||
|
|
||||||
_registerDelayCts.Cancel();
|
TryCancel(_registerDelayCts);
|
||||||
if (_impersonating)
|
if (_impersonating)
|
||||||
{
|
{
|
||||||
_loadFileProviderMare?.UnregisterFunc();
|
_loadFileProviderMare?.UnregisterFunc();
|
||||||
@@ -155,6 +156,7 @@ public class IpcProvider : IHostedService, IMediatorSubscriber
|
|||||||
}
|
}
|
||||||
|
|
||||||
Mediator.UnsubscribeAll(this);
|
Mediator.UnsubscribeAll(this);
|
||||||
|
CancelAndDispose(ref _registerDelayCts);
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,4 +195,31 @@ public class IpcProvider : IHostedService, IMediatorSubscriber
|
|||||||
|
|
||||||
return _activeGameObjectHandlers.Where(g => g.Address != nint.Zero).Select(g => g.Address).Distinct().ToList();
|
return _activeGameObjectHandlers.Where(g => g.Address != nint.Zero).Select(g => g.Address).Distinct().ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static CancellationTokenSource EnsureFreshCts(ref CancellationTokenSource? cts)
|
||||||
|
{
|
||||||
|
CancelAndDispose(ref cts);
|
||||||
|
cts = new CancellationTokenSource();
|
||||||
|
return cts;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CancelAndDispose(ref CancellationTokenSource? cts)
|
||||||
|
{
|
||||||
|
if (cts == null) return;
|
||||||
|
TryCancel(cts);
|
||||||
|
cts.Dispose();
|
||||||
|
cts = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TryCancel(CancellationTokenSource? cts)
|
||||||
|
{
|
||||||
|
if (cts == null) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
cts.Cancel();
|
||||||
|
}
|
||||||
|
catch (ObjectDisposedException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,20 @@
|
|||||||
using Dalamud.Game.ClientState.Objects.Types;
|
using Dalamud.Game.ClientState.Objects.Types;
|
||||||
|
using System;
|
||||||
using MareSynchronos.PlayerData.Handlers;
|
using MareSynchronos.PlayerData.Handlers;
|
||||||
using MareSynchronos.Services;
|
using MareSynchronos.Services;
|
||||||
using MareSynchronos.Services.Mediator;
|
using MareSynchronos.Services.Mediator;
|
||||||
using MareSynchronos.Utils;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
namespace MareSynchronos.Interop.Ipc;
|
namespace MareSynchronos.Interop.Ipc;
|
||||||
|
|
||||||
public class RedrawManager
|
public class RedrawManager : IDisposable
|
||||||
{
|
{
|
||||||
private readonly MareMediator _mareMediator;
|
private readonly MareMediator _mareMediator;
|
||||||
private readonly DalamudUtilService _dalamudUtil;
|
private readonly DalamudUtilService _dalamudUtil;
|
||||||
private readonly ConcurrentDictionary<nint, bool> _penumbraRedrawRequests = [];
|
private readonly ConcurrentDictionary<nint, bool> _penumbraRedrawRequests = [];
|
||||||
private CancellationTokenSource _disposalCts = new();
|
private CancellationTokenSource? _disposalCts = new();
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
public SemaphoreSlim RedrawSemaphore { get; init; } = new(2, 2);
|
public SemaphoreSlim RedrawSemaphore { get; init; } = new(2, 2);
|
||||||
|
|
||||||
@@ -32,12 +33,12 @@ public class RedrawManager
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
using CancellationTokenSource cancelToken = new CancellationTokenSource();
|
using CancellationTokenSource cancelToken = new CancellationTokenSource();
|
||||||
using CancellationTokenSource combinedCts = CancellationTokenSource.CreateLinkedTokenSource(cancelToken.Token, token, _disposalCts.Token);
|
using CancellationTokenSource combinedCts = CancellationTokenSource.CreateLinkedTokenSource(cancelToken.Token, token, EnsureFreshCts(ref _disposalCts).Token);
|
||||||
var combinedToken = combinedCts.Token;
|
var combinedToken = combinedCts.Token;
|
||||||
cancelToken.CancelAfter(TimeSpan.FromSeconds(15));
|
cancelToken.CancelAfter(TimeSpan.FromSeconds(15));
|
||||||
await handler.ActOnFrameworkAfterEnsureNoDrawAsync(action, combinedToken).ConfigureAwait(false);
|
await handler.ActOnFrameworkAfterEnsureNoDrawAsync(action, combinedToken).ConfigureAwait(false);
|
||||||
|
|
||||||
if (!_disposalCts.Token.IsCancellationRequested)
|
if (!_disposalCts!.Token.IsCancellationRequested)
|
||||||
await _dalamudUtil.WaitWhileCharacterIsDrawing(logger, handler, applicationId, 30000, combinedToken).ConfigureAwait(false);
|
await _dalamudUtil.WaitWhileCharacterIsDrawing(logger, handler, applicationId, 30000, combinedToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -49,6 +50,45 @@ public class RedrawManager
|
|||||||
|
|
||||||
internal void Cancel()
|
internal void Cancel()
|
||||||
{
|
{
|
||||||
_disposalCts = _disposalCts.CancelRecreate();
|
EnsureFreshCts(ref _disposalCts);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Dispose(true);
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (_disposed) return;
|
||||||
|
if (disposing)
|
||||||
|
{
|
||||||
|
CancelAndDispose(ref _disposalCts);
|
||||||
|
}
|
||||||
|
|
||||||
|
_disposed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CancellationTokenSource EnsureFreshCts(ref CancellationTokenSource? cts)
|
||||||
|
{
|
||||||
|
CancelAndDispose(ref cts);
|
||||||
|
cts = new CancellationTokenSource();
|
||||||
|
return cts;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CancelAndDispose(ref CancellationTokenSource? cts)
|
||||||
|
{
|
||||||
|
if (cts == null) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
cts.Cancel();
|
||||||
|
}
|
||||||
|
catch (ObjectDisposedException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
cts.Dispose();
|
||||||
|
cts = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ public class CharaDataConfig : IMareConfiguration
|
|||||||
public bool NearbyOwnServerOnly { get; set; } = false;
|
public bool NearbyOwnServerOnly { get; set; } = false;
|
||||||
public bool NearbyIgnoreHousingLimitations { get; set; } = false;
|
public bool NearbyIgnoreHousingLimitations { get; set; } = false;
|
||||||
public bool NearbyDrawWisps { get; set; } = true;
|
public bool NearbyDrawWisps { get; set; } = true;
|
||||||
|
public int NearbyMaxWisps { get; set; } = 20;
|
||||||
public int NearbyDistanceFilter { get; set; } = 100;
|
public int NearbyDistanceFilter { get; set; } = 100;
|
||||||
public bool NearbyShowOwnData { get; set; } = false;
|
public bool NearbyShowOwnData { get; set; } = false;
|
||||||
public bool ShowHelpTexts { get; set; } = true;
|
public bool ShowHelpTexts { get; set; } = true;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using MareSynchronos.MareConfiguration.Models;
|
using System.Collections.Generic;
|
||||||
|
using MareSynchronos.MareConfiguration.Models;
|
||||||
using MareSynchronos.UI;
|
using MareSynchronos.UI;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
@@ -19,7 +20,7 @@ public class MareConfig : IMareConfiguration
|
|||||||
public bool UseColorsInDtr { get; set; } = true;
|
public bool UseColorsInDtr { get; set; } = true;
|
||||||
public DtrEntry.Colors DtrColorsDefault { get; set; } = default;
|
public DtrEntry.Colors DtrColorsDefault { get; set; } = default;
|
||||||
public DtrEntry.Colors DtrColorsNotConnected { get; set; } = new(Glow: 0x0428FFu);
|
public DtrEntry.Colors DtrColorsNotConnected { get; set; } = new(Glow: 0x0428FFu);
|
||||||
public DtrEntry.Colors DtrColorsPairsInRange { get; set; } = new(Glow: 0xFFBA47u);
|
public DtrEntry.Colors DtrColorsPairsInRange { get; set; } = new(Glow: 0x8D37C0u);
|
||||||
public bool UseNameColors { get; set; } = false;
|
public bool UseNameColors { get; set; } = false;
|
||||||
public DtrEntry.Colors NameColors { get; set; } = new(Foreground: 0x67EBF5u, Glow: 0x00303Cu);
|
public DtrEntry.Colors NameColors { get; set; } = new(Foreground: 0x67EBF5u, Glow: 0x00303Cu);
|
||||||
public DtrEntry.Colors BlockedNameColors { get; set; } = new(Foreground: 0x8AADC7, Glow: 0x000080u);
|
public DtrEntry.Colors BlockedNameColors { get; set; } = new(Foreground: 0x8AADC7, Glow: 0x000080u);
|
||||||
@@ -37,6 +38,7 @@ public class MareConfig : IMareConfiguration
|
|||||||
public bool OpenGposeImportOnGposeStart { get; set; } = false;
|
public bool OpenGposeImportOnGposeStart { get; set; } = false;
|
||||||
public bool OpenPopupOnAdd { get; set; } = true;
|
public bool OpenPopupOnAdd { get; set; } = true;
|
||||||
public int ParallelDownloads { get; set; } = 10;
|
public int ParallelDownloads { get; set; } = 10;
|
||||||
|
public bool EnableDownloadQueue { get; set; } = false;
|
||||||
public int DownloadSpeedLimitInBytes { get; set; } = 0;
|
public int DownloadSpeedLimitInBytes { get; set; } = 0;
|
||||||
public DownloadSpeeds DownloadSpeedType { get; set; } = DownloadSpeeds.MBps;
|
public DownloadSpeeds DownloadSpeedType { get; set; } = DownloadSpeeds.MBps;
|
||||||
[Obsolete] public bool PreferNotesOverNamesForVisible { get; set; } = false;
|
[Obsolete] public bool PreferNotesOverNamesForVisible { get; set; } = false;
|
||||||
@@ -59,8 +61,14 @@ public class MareConfig : IMareConfiguration
|
|||||||
public bool ShowUploading { get; set; } = true;
|
public bool ShowUploading { get; set; } = true;
|
||||||
public bool ShowUploadingBigText { get; set; } = true;
|
public bool ShowUploadingBigText { get; set; } = true;
|
||||||
public bool ShowVisibleUsersSeparately { get; set; } = true;
|
public bool ShowVisibleUsersSeparately { get; set; } = true;
|
||||||
public bool EnableAutoDetectDiscovery { get; set; } = false;
|
public string LastChangelogVersionSeen { get; set; } = string.Empty;
|
||||||
public bool AllowAutoDetectPairRequests { get; set; } = false;
|
public bool DefaultDisableSounds { get; set; } = false;
|
||||||
|
public bool DefaultDisableAnimations { get; set; } = false;
|
||||||
|
public bool DefaultDisableVfx { get; set; } = false;
|
||||||
|
public Dictionary<string, SyncOverrideEntry> PairSyncOverrides { get; set; } = new(StringComparer.Ordinal);
|
||||||
|
public Dictionary<string, SyncOverrideEntry> GroupSyncOverrides { get; set; } = new(StringComparer.Ordinal);
|
||||||
|
public bool EnableAutoDetectDiscovery { get; set; } = true;
|
||||||
|
public bool AllowAutoDetectPairRequests { get; set; } = true;
|
||||||
public int AutoDetectMaxDistanceMeters { get; set; } = 40;
|
public int AutoDetectMaxDistanceMeters { get; set; } = 40;
|
||||||
public int AutoDetectMuteMinutes { get; set; } = 5;
|
public int AutoDetectMuteMinutes { get; set; } = 5;
|
||||||
public int TimeSpanBetweenScansInSeconds { get; set; } = 30;
|
public int TimeSpanBetweenScansInSeconds { get; set; } = 30;
|
||||||
@@ -77,6 +85,9 @@ public class MareConfig : IMareConfiguration
|
|||||||
public int ChatLogKind { get; set; } = 1; // XivChatType.Debug
|
public int ChatLogKind { get; set; } = 1; // XivChatType.Debug
|
||||||
public bool ExtraChatAPI { get; set; } = false;
|
public bool ExtraChatAPI { get; set; } = false;
|
||||||
public bool ExtraChatTags { get; set; } = false;
|
public bool ExtraChatTags { get; set; } = false;
|
||||||
|
public bool TypingIndicatorShowOnNameplates { get; set; } = true;
|
||||||
|
public bool TypingIndicatorShowOnPartyList { get; set; } = true;
|
||||||
|
public TypingIndicatorBubbleSize TypingIndicatorBubbleSize { get; set; } = TypingIndicatorBubbleSize.Large;
|
||||||
|
|
||||||
public bool MareAPI { get; set; } = true;
|
public bool MareAPI { get; set; } = true;
|
||||||
}
|
}
|
||||||
@@ -8,6 +8,7 @@ public class PlayerPerformanceConfig : IMareConfiguration
|
|||||||
public bool AutoPausePlayersExceedingThresholds { get; set; } = true;
|
public bool AutoPausePlayersExceedingThresholds { get; set; } = true;
|
||||||
public bool NotifyAutoPauseDirectPairs { get; set; } = true;
|
public bool NotifyAutoPauseDirectPairs { get; set; } = true;
|
||||||
public bool NotifyAutoPauseGroupPairs { get; set; } = true;
|
public bool NotifyAutoPauseGroupPairs { get; set; } = true;
|
||||||
|
public bool ShowSelfAnalysisWarnings { get; set; } = true;
|
||||||
public int VRAMSizeAutoPauseThresholdMiB { get; set; } = 500;
|
public int VRAMSizeAutoPauseThresholdMiB { get; set; } = 500;
|
||||||
public int TrisAutoPauseThresholdThousands { get; set; } = 400;
|
public int TrisAutoPauseThresholdThousands { get; set; } = 400;
|
||||||
public bool IgnoreDirectPairs { get; set; } = true;
|
public bool IgnoreDirectPairs { get; set; } = true;
|
||||||
|
|||||||
13
MareSynchronos/MareConfiguration/Models/SyncOverrideEntry.cs
Normal file
13
MareSynchronos/MareConfiguration/Models/SyncOverrideEntry.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace MareSynchronos.MareConfiguration.Models;
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public class SyncOverrideEntry
|
||||||
|
{
|
||||||
|
public bool? DisableSounds { get; set; }
|
||||||
|
public bool? DisableAnimations { get; set; }
|
||||||
|
public bool? DisableVfx { get; set; }
|
||||||
|
|
||||||
|
public bool IsEmpty => DisableSounds is null && DisableAnimations is null && DisableVfx is null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace MareSynchronos.MareConfiguration.Models;
|
||||||
|
|
||||||
|
public enum TypingIndicatorBubbleSize
|
||||||
|
{
|
||||||
|
Small,
|
||||||
|
Medium,
|
||||||
|
Large
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ using MareSynchronos.PlayerData.Services;
|
|||||||
using MareSynchronos.Services;
|
using MareSynchronos.Services;
|
||||||
using MareSynchronos.Services.Mediator;
|
using MareSynchronos.Services.Mediator;
|
||||||
using MareSynchronos.Services.ServerConfiguration;
|
using MareSynchronos.Services.ServerConfiguration;
|
||||||
|
using MareSynchronos.Interop;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -150,8 +151,12 @@ public class MarePlugin : MediatorSubscriberBase, IHostedService
|
|||||||
_runtimeServiceScope.ServiceProvider.GetRequiredService<TransientResourceManager>();
|
_runtimeServiceScope.ServiceProvider.GetRequiredService<TransientResourceManager>();
|
||||||
_runtimeServiceScope.ServiceProvider.GetRequiredService<OnlinePlayerManager>();
|
_runtimeServiceScope.ServiceProvider.GetRequiredService<OnlinePlayerManager>();
|
||||||
_runtimeServiceScope.ServiceProvider.GetRequiredService<NotificationService>();
|
_runtimeServiceScope.ServiceProvider.GetRequiredService<NotificationService>();
|
||||||
|
_runtimeServiceScope.ServiceProvider.GetRequiredService<SyncDefaultsService>();
|
||||||
_runtimeServiceScope.ServiceProvider.GetRequiredService<ChatService>();
|
_runtimeServiceScope.ServiceProvider.GetRequiredService<ChatService>();
|
||||||
|
_runtimeServiceScope.ServiceProvider.GetRequiredService<ChatTypingDetectionService>();
|
||||||
_runtimeServiceScope.ServiceProvider.GetRequiredService<GuiHookService>();
|
_runtimeServiceScope.ServiceProvider.GetRequiredService<GuiHookService>();
|
||||||
|
var characterAnalyzer = _runtimeServiceScope.ServiceProvider.GetRequiredService<CharacterAnalyzer>();
|
||||||
|
_ = characterAnalyzer.ComputeAnalysis(print: false);
|
||||||
|
|
||||||
#if !DEBUG
|
#if !DEBUG
|
||||||
if (_mareConfigService.Current.LogLevel != LogLevel.Information)
|
if (_mareConfigService.Current.LogLevel != LogLevel.Information)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<AssemblyName>UmbraSync</AssemblyName>
|
<AssemblyName>UmbraSync</AssemblyName>
|
||||||
<RootNamespace>UmbraSync</RootNamespace>
|
<RootNamespace>UmbraSync</RootNamespace>
|
||||||
<Version>0.1.7.0</Version>
|
<Version>0.1.9.9</Version>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using MareSynchronos.FileCache;
|
using MareSynchronos.FileCache;
|
||||||
|
using MareSynchronos.MareConfiguration;
|
||||||
using MareSynchronos.Services.Mediator;
|
using MareSynchronos.Services.Mediator;
|
||||||
using MareSynchronos.WebAPI.Files;
|
using MareSynchronos.WebAPI.Files;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -10,21 +11,23 @@ public class FileDownloadManagerFactory
|
|||||||
private readonly FileCacheManager _fileCacheManager;
|
private readonly FileCacheManager _fileCacheManager;
|
||||||
private readonly FileCompactor _fileCompactor;
|
private readonly FileCompactor _fileCompactor;
|
||||||
private readonly FileTransferOrchestrator _fileTransferOrchestrator;
|
private readonly FileTransferOrchestrator _fileTransferOrchestrator;
|
||||||
|
private readonly MareConfigService _mareConfigService;
|
||||||
private readonly ILoggerFactory _loggerFactory;
|
private readonly ILoggerFactory _loggerFactory;
|
||||||
private readonly MareMediator _mareMediator;
|
private readonly MareMediator _mareMediator;
|
||||||
|
|
||||||
public FileDownloadManagerFactory(ILoggerFactory loggerFactory, MareMediator mareMediator, FileTransferOrchestrator fileTransferOrchestrator,
|
public FileDownloadManagerFactory(ILoggerFactory loggerFactory, MareMediator mareMediator, FileTransferOrchestrator fileTransferOrchestrator,
|
||||||
FileCacheManager fileCacheManager, FileCompactor fileCompactor)
|
FileCacheManager fileCacheManager, FileCompactor fileCompactor, MareConfigService mareConfigService)
|
||||||
{
|
{
|
||||||
_loggerFactory = loggerFactory;
|
_loggerFactory = loggerFactory;
|
||||||
_mareMediator = mareMediator;
|
_mareMediator = mareMediator;
|
||||||
_fileTransferOrchestrator = fileTransferOrchestrator;
|
_fileTransferOrchestrator = fileTransferOrchestrator;
|
||||||
_fileCacheManager = fileCacheManager;
|
_fileCacheManager = fileCacheManager;
|
||||||
_fileCompactor = fileCompactor;
|
_fileCompactor = fileCompactor;
|
||||||
|
_mareConfigService = mareConfigService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public FileDownloadManager Create()
|
public FileDownloadManager Create()
|
||||||
{
|
{
|
||||||
return new FileDownloadManager(_loggerFactory.CreateLogger<FileDownloadManager>(), _mareMediator, _fileTransferOrchestrator, _fileCacheManager, _fileCompactor);
|
return new FileDownloadManager(_loggerFactory.CreateLogger<FileDownloadManager>(), _mareMediator, _fileTransferOrchestrator, _fileCacheManager, _fileCompactor, _mareConfigService);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,7 +5,6 @@ using MareSynchronos.PlayerData.Handlers;
|
|||||||
using MareSynchronos.PlayerData.Pairs;
|
using MareSynchronos.PlayerData.Pairs;
|
||||||
using MareSynchronos.Services;
|
using MareSynchronos.Services;
|
||||||
using MareSynchronos.Services.Mediator;
|
using MareSynchronos.Services.Mediator;
|
||||||
using MareSynchronos.Services.ServerConfiguration;
|
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
@@ -23,7 +22,6 @@ public class PairHandlerFactory
|
|||||||
private readonly ILoggerFactory _loggerFactory;
|
private readonly ILoggerFactory _loggerFactory;
|
||||||
private readonly MareMediator _mareMediator;
|
private readonly MareMediator _mareMediator;
|
||||||
private readonly PlayerPerformanceService _playerPerformanceService;
|
private readonly PlayerPerformanceService _playerPerformanceService;
|
||||||
private readonly ServerConfigurationManager _serverConfigManager;
|
|
||||||
private readonly PluginWarningNotificationService _pluginWarningNotificationManager;
|
private readonly PluginWarningNotificationService _pluginWarningNotificationManager;
|
||||||
private readonly PairAnalyzerFactory _pairAnalyzerFactory;
|
private readonly PairAnalyzerFactory _pairAnalyzerFactory;
|
||||||
private readonly VisibilityService _visibilityService;
|
private readonly VisibilityService _visibilityService;
|
||||||
@@ -32,7 +30,7 @@ public class PairHandlerFactory
|
|||||||
FileDownloadManagerFactory fileDownloadManagerFactory, DalamudUtilService dalamudUtilService,
|
FileDownloadManagerFactory fileDownloadManagerFactory, DalamudUtilService dalamudUtilService,
|
||||||
PluginWarningNotificationService pluginWarningNotificationManager, IHostApplicationLifetime hostApplicationLifetime,
|
PluginWarningNotificationService pluginWarningNotificationManager, IHostApplicationLifetime hostApplicationLifetime,
|
||||||
FileCacheManager fileCacheManager, MareMediator mareMediator, PlayerPerformanceService playerPerformanceService,
|
FileCacheManager fileCacheManager, MareMediator mareMediator, PlayerPerformanceService playerPerformanceService,
|
||||||
ServerConfigurationManager serverConfigManager, PairAnalyzerFactory pairAnalyzerFactory,
|
PairAnalyzerFactory pairAnalyzerFactory,
|
||||||
MareConfigService configService, VisibilityService visibilityService)
|
MareConfigService configService, VisibilityService visibilityService)
|
||||||
{
|
{
|
||||||
_loggerFactory = loggerFactory;
|
_loggerFactory = loggerFactory;
|
||||||
@@ -45,7 +43,6 @@ public class PairHandlerFactory
|
|||||||
_fileCacheManager = fileCacheManager;
|
_fileCacheManager = fileCacheManager;
|
||||||
_mareMediator = mareMediator;
|
_mareMediator = mareMediator;
|
||||||
_playerPerformanceService = playerPerformanceService;
|
_playerPerformanceService = playerPerformanceService;
|
||||||
_serverConfigManager = serverConfigManager;
|
|
||||||
_pairAnalyzerFactory = pairAnalyzerFactory;
|
_pairAnalyzerFactory = pairAnalyzerFactory;
|
||||||
_configService = configService;
|
_configService = configService;
|
||||||
_visibilityService = visibilityService;
|
_visibilityService = visibilityService;
|
||||||
@@ -55,6 +52,6 @@ public class PairHandlerFactory
|
|||||||
{
|
{
|
||||||
return new PairHandler(_loggerFactory.CreateLogger<PairHandler>(), pair, _pairAnalyzerFactory.Create(pair), _gameObjectHandlerFactory,
|
return new PairHandler(_loggerFactory.CreateLogger<PairHandler>(), pair, _pairAnalyzerFactory.Create(pair), _gameObjectHandlerFactory,
|
||||||
_ipcManager, _fileDownloadManagerFactory.Create(), _pluginWarningNotificationManager, _dalamudUtilService, _hostApplicationLifetime,
|
_ipcManager, _fileDownloadManagerFactory.Create(), _pluginWarningNotificationManager, _dalamudUtilService, _hostApplicationLifetime,
|
||||||
_fileCacheManager, _mareMediator, _playerPerformanceService, _serverConfigManager, _configService, _visibilityService);
|
_fileCacheManager, _mareMediator, _playerPerformanceService, _configService, _visibilityService);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7,7 +7,6 @@ using MareSynchronos.PlayerData.Pairs;
|
|||||||
using MareSynchronos.Services;
|
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.Utils;
|
using MareSynchronos.Utils;
|
||||||
using MareSynchronos.WebAPI.Files;
|
using MareSynchronos.WebAPI.Files;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
@@ -29,7 +28,6 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
|
|||||||
private readonly GameObjectHandlerFactory _gameObjectHandlerFactory;
|
private readonly GameObjectHandlerFactory _gameObjectHandlerFactory;
|
||||||
private readonly IpcManager _ipcManager;
|
private readonly IpcManager _ipcManager;
|
||||||
private readonly PlayerPerformanceService _playerPerformanceService;
|
private readonly PlayerPerformanceService _playerPerformanceService;
|
||||||
private readonly ServerConfigurationManager _serverConfigManager;
|
|
||||||
private readonly PluginWarningNotificationService _pluginWarningNotificationManager;
|
private readonly PluginWarningNotificationService _pluginWarningNotificationManager;
|
||||||
private readonly VisibilityService _visibilityService;
|
private readonly VisibilityService _visibilityService;
|
||||||
private CancellationTokenSource? _applicationCancellationTokenSource = new();
|
private CancellationTokenSource? _applicationCancellationTokenSource = new();
|
||||||
@@ -53,7 +51,6 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
|
|||||||
DalamudUtilService dalamudUtil, IHostApplicationLifetime lifetime,
|
DalamudUtilService dalamudUtil, IHostApplicationLifetime lifetime,
|
||||||
FileCacheManager fileDbManager, MareMediator mediator,
|
FileCacheManager fileDbManager, MareMediator mediator,
|
||||||
PlayerPerformanceService playerPerformanceService,
|
PlayerPerformanceService playerPerformanceService,
|
||||||
ServerConfigurationManager serverConfigManager,
|
|
||||||
MareConfigService configService, VisibilityService visibilityService) : base(logger, mediator)
|
MareConfigService configService, VisibilityService visibilityService) : base(logger, mediator)
|
||||||
{
|
{
|
||||||
Pair = pair;
|
Pair = pair;
|
||||||
@@ -65,7 +62,6 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
|
|||||||
_dalamudUtil = dalamudUtil;
|
_dalamudUtil = dalamudUtil;
|
||||||
_fileDbManager = fileDbManager;
|
_fileDbManager = fileDbManager;
|
||||||
_playerPerformanceService = playerPerformanceService;
|
_playerPerformanceService = playerPerformanceService;
|
||||||
_serverConfigManager = serverConfigManager;
|
|
||||||
_configService = configService;
|
_configService = configService;
|
||||||
_visibilityService = visibilityService;
|
_visibilityService = visibilityService;
|
||||||
|
|
||||||
|
|||||||
@@ -270,6 +270,20 @@ public class Pair : DisposableMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
base.Dispose(disposing);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_applicationCts.Cancel();
|
||||||
|
}
|
||||||
|
catch (ObjectDisposedException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
_applicationCts.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
public void SetNote(string note)
|
public void SetNote(string note)
|
||||||
{
|
{
|
||||||
_serverConfigurationManager.SetNoteForUid(UserData.UID, note);
|
_serverConfigurationManager.SetNoteForUid(UserData.UID, note);
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ public sealed class PairManager : DisposableMediatorSubscriberBase
|
|||||||
RecreateLazy();
|
RecreateLazy();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddGroupPair(GroupPairFullInfoDto dto)
|
public void AddGroupPair(GroupPairFullInfoDto dto, bool isInitialLoad = false)
|
||||||
{
|
{
|
||||||
if (!_allClientPairs.ContainsKey(dto.User))
|
if (!_allClientPairs.ContainsKey(dto.User))
|
||||||
_allClientPairs[dto.User] = _pairFactory.Create(dto.User);
|
_allClientPairs[dto.User] = _pairFactory.Create(dto.User);
|
||||||
@@ -59,6 +59,11 @@ public sealed class PairManager : DisposableMediatorSubscriberBase
|
|||||||
var group = _allGroups[dto.Group];
|
var group = _allGroups[dto.Group];
|
||||||
_allClientPairs[dto.User].GroupPair[group] = dto;
|
_allClientPairs[dto.User].GroupPair[group] = dto;
|
||||||
RecreateLazy();
|
RecreateLazy();
|
||||||
|
|
||||||
|
if (!isInitialLoad)
|
||||||
|
{
|
||||||
|
Mediator.Publish(new ApplyDefaultGroupPermissionsMessage(dto));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Pair? GetPairByUID(string uid)
|
public Pair? GetPairByUID(string uid)
|
||||||
@@ -88,6 +93,11 @@ public sealed class PairManager : DisposableMediatorSubscriberBase
|
|||||||
LastAddedUser = _allClientPairs[dto.User];
|
LastAddedUser = _allClientPairs[dto.User];
|
||||||
_allClientPairs[dto.User].ApplyLastReceivedData();
|
_allClientPairs[dto.User].ApplyLastReceivedData();
|
||||||
RecreateLazy();
|
RecreateLazy();
|
||||||
|
|
||||||
|
if (addToLastAddedUser)
|
||||||
|
{
|
||||||
|
Mediator.Publish(new ApplyDefaultPairPermissionsMessage(dto));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ClearPairs()
|
public void ClearPairs()
|
||||||
@@ -210,9 +220,16 @@ public sealed class PairManager : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
public void SetGroupInfo(GroupInfoDto dto)
|
public void SetGroupInfo(GroupInfoDto dto)
|
||||||
{
|
{
|
||||||
_allGroups[dto.Group].Group = dto.Group;
|
if (!_allGroups.TryGetValue(dto.Group, out var groupInfo))
|
||||||
_allGroups[dto.Group].Owner = dto.Owner;
|
{
|
||||||
_allGroups[dto.Group].GroupPermissions = dto.GroupPermissions;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
groupInfo.Group = dto.Group;
|
||||||
|
groupInfo.Owner = dto.Owner;
|
||||||
|
groupInfo.GroupPermissions = dto.GroupPermissions;
|
||||||
|
groupInfo.IsTemporary = dto.IsTemporary;
|
||||||
|
groupInfo.ExpiresAt = dto.ExpiresAt;
|
||||||
|
|
||||||
RecreateLazy();
|
RecreateLazy();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -101,6 +102,8 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
collection.AddSingleton<MareSynchronos.Services.AutoDetect.AutoDetectRequestService>();
|
collection.AddSingleton<MareSynchronos.Services.AutoDetect.AutoDetectRequestService>();
|
||||||
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.SyncshellDiscoveryService>();
|
||||||
collection.AddSingleton<MarePlugin>();
|
collection.AddSingleton<MarePlugin>();
|
||||||
collection.AddSingleton<MareProfileManager>();
|
collection.AddSingleton<MareProfileManager>();
|
||||||
collection.AddSingleton<GameObjectHandlerFactory>();
|
collection.AddSingleton<GameObjectHandlerFactory>();
|
||||||
@@ -115,6 +118,7 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
collection.AddSingleton<PluginWarningNotificationService>();
|
collection.AddSingleton<PluginWarningNotificationService>();
|
||||||
collection.AddSingleton<FileCompactor>();
|
collection.AddSingleton<FileCompactor>();
|
||||||
collection.AddSingleton<TagHandler>();
|
collection.AddSingleton<TagHandler>();
|
||||||
|
collection.AddSingleton<SyncDefaultsService>();
|
||||||
collection.AddSingleton<UidDisplayHandler>();
|
collection.AddSingleton<UidDisplayHandler>();
|
||||||
collection.AddSingleton<PluginWatcherService>();
|
collection.AddSingleton<PluginWatcherService>();
|
||||||
collection.AddSingleton<PlayerPerformanceService>();
|
collection.AddSingleton<PlayerPerformanceService>();
|
||||||
@@ -124,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>();
|
||||||
@@ -145,6 +150,11 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
collection.AddSingleton<IpcCallerMare>();
|
collection.AddSingleton<IpcCallerMare>();
|
||||||
collection.AddSingleton<IpcManager>();
|
collection.AddSingleton<IpcManager>();
|
||||||
collection.AddSingleton<NotificationService>();
|
collection.AddSingleton<NotificationService>();
|
||||||
|
collection.AddSingleton<TemporarySyncshellNotificationService>();
|
||||||
|
collection.AddSingleton<PartyListTypingService>();
|
||||||
|
collection.AddSingleton<TypingIndicatorStateService>();
|
||||||
|
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));
|
||||||
@@ -176,17 +186,25 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
// add scoped services
|
// add scoped services
|
||||||
collection.AddScoped<CacheMonitor>();
|
collection.AddScoped<CacheMonitor>();
|
||||||
collection.AddScoped<UiFactory>();
|
collection.AddScoped<UiFactory>();
|
||||||
collection.AddScoped<WindowMediatorSubscriberBase, SettingsUi>();
|
collection.AddScoped<SettingsUi>();
|
||||||
collection.AddScoped<WindowMediatorSubscriberBase, CompactUi>();
|
collection.AddScoped<CompactUi>();
|
||||||
|
collection.AddScoped<EditProfileUi>();
|
||||||
|
collection.AddScoped<DataAnalysisUi>();
|
||||||
|
collection.AddScoped<CharaDataHubUi>();
|
||||||
|
collection.AddScoped<AutoDetectUi>();
|
||||||
|
collection.AddScoped<WindowMediatorSubscriberBase>(sp => sp.GetRequiredService<SettingsUi>());
|
||||||
|
collection.AddScoped<WindowMediatorSubscriberBase>(sp => sp.GetRequiredService<CompactUi>());
|
||||||
collection.AddScoped<WindowMediatorSubscriberBase, IntroUi>();
|
collection.AddScoped<WindowMediatorSubscriberBase, IntroUi>();
|
||||||
collection.AddScoped<WindowMediatorSubscriberBase, DownloadUi>();
|
collection.AddScoped<WindowMediatorSubscriberBase, DownloadUi>();
|
||||||
collection.AddScoped<WindowMediatorSubscriberBase, AutoDetectUi>();
|
collection.AddScoped<WindowMediatorSubscriberBase>(sp => sp.GetRequiredService<AutoDetectUi>());
|
||||||
|
collection.AddScoped<WindowMediatorSubscriberBase, ChangelogUi>();
|
||||||
collection.AddScoped<WindowMediatorSubscriberBase, PopoutProfileUi>();
|
collection.AddScoped<WindowMediatorSubscriberBase, PopoutProfileUi>();
|
||||||
collection.AddScoped<WindowMediatorSubscriberBase, DataAnalysisUi>();
|
collection.AddScoped<WindowMediatorSubscriberBase>(sp => sp.GetRequiredService<DataAnalysisUi>());
|
||||||
collection.AddScoped<WindowMediatorSubscriberBase, EventViewerUI>();
|
collection.AddScoped<WindowMediatorSubscriberBase, EventViewerUI>();
|
||||||
collection.AddScoped<WindowMediatorSubscriberBase, CharaDataHubUi>();
|
collection.AddScoped<WindowMediatorSubscriberBase, CharaDataHubUi>();
|
||||||
collection.AddScoped<WindowMediatorSubscriberBase, EditProfileUi>();
|
collection.AddScoped<WindowMediatorSubscriberBase>(sp => sp.GetRequiredService<EditProfileUi>());
|
||||||
collection.AddScoped<WindowMediatorSubscriberBase, PopupHandler>();
|
collection.AddScoped<WindowMediatorSubscriberBase, PopupHandler>();
|
||||||
|
collection.AddScoped<WindowMediatorSubscriberBase, TypingIndicatorOverlay>();
|
||||||
collection.AddScoped<IPopupHandler, ReportPopupHandler>();
|
collection.AddScoped<IPopupHandler, ReportPopupHandler>();
|
||||||
collection.AddScoped<IPopupHandler, BanUserPopupHandler>();
|
collection.AddScoped<IPopupHandler, BanUserPopupHandler>();
|
||||||
collection.AddScoped<CacheCreationService>();
|
collection.AddScoped<CacheCreationService>();
|
||||||
@@ -198,11 +216,13 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
collection.AddScoped<UiSharedService>();
|
collection.AddScoped<UiSharedService>();
|
||||||
collection.AddScoped<ChatService>();
|
collection.AddScoped<ChatService>();
|
||||||
collection.AddScoped<GuiHookService>();
|
collection.AddScoped<GuiHookService>();
|
||||||
|
collection.AddScoped<ChatTypingDetectionService>();
|
||||||
|
|
||||||
collection.AddHostedService(p => p.GetRequiredService<PluginWatcherService>());
|
collection.AddHostedService(p => p.GetRequiredService<PluginWatcherService>());
|
||||||
collection.AddHostedService(p => p.GetRequiredService<ConfigurationSaveService>());
|
collection.AddHostedService(p => p.GetRequiredService<ConfigurationSaveService>());
|
||||||
collection.AddHostedService(p => p.GetRequiredService<MareMediator>());
|
collection.AddHostedService(p => p.GetRequiredService<MareMediator>());
|
||||||
collection.AddHostedService(p => p.GetRequiredService<NotificationService>());
|
collection.AddHostedService(p => p.GetRequiredService<NotificationService>());
|
||||||
|
collection.AddHostedService(p => p.GetRequiredService<TemporarySyncshellNotificationService>());
|
||||||
collection.AddHostedService(p => p.GetRequiredService<FileCacheManager>());
|
collection.AddHostedService(p => p.GetRequiredService<FileCacheManager>());
|
||||||
collection.AddHostedService(p => p.GetRequiredService<ConfigurationMigrator>());
|
collection.AddHostedService(p => p.GetRequiredService<ConfigurationMigrator>());
|
||||||
collection.AddHostedService(p => p.GetRequiredService<DalamudUtilService>());
|
collection.AddHostedService(p => p.GetRequiredService<DalamudUtilService>());
|
||||||
@@ -212,9 +232,22 @@ 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<MareSynchronos.Services.AutoDetect.AutoDetectSuppressionService>());
|
||||||
})
|
})
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var partyListTypingService = _host.Services.GetRequiredService<PartyListTypingService>();
|
||||||
|
pluginInterface.UiBuilder.Draw += partyListTypingService.Draw;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
pluginLog.Warning(e, "Failed to initialize PartyListTypingService draw hook");
|
||||||
|
}
|
||||||
|
|
||||||
_ = Task.Run(async () => {
|
_ = Task.Run(async () => {
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using MareSynchronos.WebAPI.AutoDetect;
|
using MareSynchronos.WebAPI.AutoDetect;
|
||||||
using MareSynchronos.MareConfiguration;
|
using MareSynchronos.MareConfiguration;
|
||||||
using MareSynchronos.Services;
|
using MareSynchronos.Services;
|
||||||
using MareSynchronos.Services.Mediator;
|
using MareSynchronos.Services.Mediator;
|
||||||
using NotificationType = MareSynchronos.MareConfiguration.Models.NotificationType;
|
using NotificationType = MareSynchronos.MareConfiguration.Models.NotificationType;
|
||||||
|
using MareSynchronos.WebAPI;
|
||||||
|
using MareSynchronos.API.Dto.User;
|
||||||
|
using MareSynchronos.API.Data;
|
||||||
|
|
||||||
namespace MareSynchronos.Services.AutoDetect;
|
namespace MareSynchronos.Services.AutoDetect;
|
||||||
|
|
||||||
@@ -15,8 +23,15 @@ public class AutoDetectRequestService
|
|||||||
private readonly MareConfigService _configService;
|
private readonly MareConfigService _configService;
|
||||||
private readonly DalamudUtilService _dalamud;
|
private readonly DalamudUtilService _dalamud;
|
||||||
private readonly MareMediator _mediator;
|
private readonly MareMediator _mediator;
|
||||||
|
private readonly object _syncRoot = new();
|
||||||
|
private readonly Dictionary<string, DateTime> _activeCooldowns = new(StringComparer.Ordinal);
|
||||||
|
private readonly Dictionary<string, RefusalTracker> _refusalTrackers = new(StringComparer.Ordinal);
|
||||||
|
private readonly ConcurrentDictionary<string, PendingRequestInfo> _pendingRequests = new(StringComparer.Ordinal);
|
||||||
|
private static readonly TimeSpan RequestCooldown = TimeSpan.FromMinutes(5);
|
||||||
|
private static readonly TimeSpan RefusalLockDuration = TimeSpan.FromMinutes(15);
|
||||||
|
private readonly ApiController _apiController;
|
||||||
|
|
||||||
public AutoDetectRequestService(ILogger<AutoDetectRequestService> logger, DiscoveryConfigProvider configProvider, DiscoveryApiClient client, MareConfigService configService, MareMediator mediator, DalamudUtilService dalamudUtilService)
|
public AutoDetectRequestService(ILogger<AutoDetectRequestService> logger, DiscoveryConfigProvider configProvider, DiscoveryApiClient client, MareConfigService configService, MareMediator mediator, DalamudUtilService dalamudUtilService, ApiController apiController)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_configProvider = configProvider;
|
_configProvider = configProvider;
|
||||||
@@ -24,9 +39,10 @@ public class AutoDetectRequestService
|
|||||||
_configService = configService;
|
_configService = configService;
|
||||||
_mediator = mediator;
|
_mediator = mediator;
|
||||||
_dalamud = dalamudUtilService;
|
_dalamud = dalamudUtilService;
|
||||||
|
_apiController = apiController;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> SendRequestAsync(string token, CancellationToken ct = default)
|
public async Task<bool> SendRequestAsync(string? token, string? uid = null, string? targetDisplayName = null, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
if (!_configService.Current.AllowAutoDetectPairRequests)
|
if (!_configService.Current.AllowAutoDetectPairRequests)
|
||||||
{
|
{
|
||||||
@@ -34,6 +50,54 @@ public class AutoDetectRequestService
|
|||||||
_mediator.Publish(new NotificationMessage("Nearby request blocked", "Enable 'Allow pair requests' in Settings to send requests.", NotificationType.Info));
|
_mediator.Publish(new NotificationMessage("Nearby request blocked", "Enable 'Allow pair requests' in Settings to send requests.", NotificationType.Info));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(token) && string.IsNullOrEmpty(uid))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Nearby request blocked: no token or UID provided");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetKey = BuildTargetKey(uid, token, targetDisplayName);
|
||||||
|
if (!string.IsNullOrEmpty(targetKey))
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
lock (_syncRoot)
|
||||||
|
{
|
||||||
|
if (_refusalTrackers.TryGetValue(targetKey, out var tracker))
|
||||||
|
{
|
||||||
|
if (tracker.LockUntil.HasValue && tracker.LockUntil.Value > now)
|
||||||
|
{
|
||||||
|
PublishLockNotification(tracker.LockUntil.Value - now);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tracker.LockUntil.HasValue && tracker.LockUntil.Value <= now)
|
||||||
|
{
|
||||||
|
tracker.LockUntil = null;
|
||||||
|
tracker.Count = 0;
|
||||||
|
if (tracker.Count == 0 && tracker.LockUntil == null)
|
||||||
|
{
|
||||||
|
_refusalTrackers.Remove(targetKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_activeCooldowns.TryGetValue(targetKey, out var lastSent))
|
||||||
|
{
|
||||||
|
var elapsed = now - lastSent;
|
||||||
|
if (elapsed < RequestCooldown)
|
||||||
|
{
|
||||||
|
PublishCooldownNotification(RequestCooldown - elapsed);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elapsed >= RequestCooldown)
|
||||||
|
{
|
||||||
|
_activeCooldowns.Remove(targetKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
var endpoint = _configProvider.RequestEndpoint;
|
var endpoint = _configProvider.RequestEndpoint;
|
||||||
if (string.IsNullOrEmpty(endpoint))
|
if (string.IsNullOrEmpty(endpoint))
|
||||||
{
|
{
|
||||||
@@ -49,14 +113,64 @@ public class AutoDetectRequestService
|
|||||||
}
|
}
|
||||||
catch { }
|
catch { }
|
||||||
|
|
||||||
|
var requestToken = string.IsNullOrEmpty(token) ? null : token;
|
||||||
|
var requestUid = requestToken == null ? uid : null;
|
||||||
|
|
||||||
_logger.LogInformation("Nearby: sending pair request via {endpoint}", endpoint);
|
_logger.LogInformation("Nearby: sending pair request via {endpoint}", endpoint);
|
||||||
var ok = await _client.SendRequestAsync(endpoint!, token, displayName, ct).ConfigureAwait(false);
|
var ok = await _client.SendRequestAsync(endpoint!, requestToken, requestUid, displayName, ct).ConfigureAwait(false);
|
||||||
if (ok)
|
if (ok)
|
||||||
{
|
{
|
||||||
|
if (!string.IsNullOrEmpty(targetKey))
|
||||||
|
{
|
||||||
|
lock (_syncRoot)
|
||||||
|
{
|
||||||
|
_activeCooldowns[targetKey] = DateTime.UtcNow;
|
||||||
|
if (_refusalTrackers.TryGetValue(targetKey, out var tracker))
|
||||||
|
{
|
||||||
|
tracker.Count = 0;
|
||||||
|
tracker.LockUntil = null;
|
||||||
|
if (tracker.Count == 0 && tracker.LockUntil == null)
|
||||||
|
{
|
||||||
|
_refusalTrackers.Remove(targetKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
_mediator.Publish(new NotificationMessage("Nearby request sent", "The other user will receive a request notification.", NotificationType.Info));
|
_mediator.Publish(new NotificationMessage("Nearby request sent", "The other user will receive a request notification.", NotificationType.Info));
|
||||||
|
var pendingKey = EnsureTargetKey(targetKey);
|
||||||
|
var label = !string.IsNullOrWhiteSpace(targetDisplayName)
|
||||||
|
? targetDisplayName!
|
||||||
|
: (!string.IsNullOrEmpty(uid) ? uid : (!string.IsNullOrEmpty(token) ? token : pendingKey));
|
||||||
|
_pendingRequests[pendingKey] = new PendingRequestInfo(pendingKey, uid, token, label, DateTime.UtcNow);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
if (!string.IsNullOrEmpty(targetKey))
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
lock (_syncRoot)
|
||||||
|
{
|
||||||
|
_activeCooldowns.Remove(targetKey);
|
||||||
|
if (!_refusalTrackers.TryGetValue(targetKey, out var tracker))
|
||||||
|
{
|
||||||
|
tracker = new RefusalTracker();
|
||||||
|
_refusalTrackers[targetKey] = tracker;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tracker.LockUntil.HasValue && tracker.LockUntil.Value <= now)
|
||||||
|
{
|
||||||
|
tracker.LockUntil = null;
|
||||||
|
tracker.Count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
tracker.Count++;
|
||||||
|
if (tracker.Count >= 3)
|
||||||
|
{
|
||||||
|
tracker.LockUntil = now.Add(RefusalLockDuration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_pendingRequests.TryRemove(targetKey, out _);
|
||||||
|
}
|
||||||
_mediator.Publish(new NotificationMessage("Nearby request failed", "The server rejected the request. Try again soon.", NotificationType.Warning));
|
_mediator.Publish(new NotificationMessage("Nearby request failed", "The server rejected the request. Try again soon.", NotificationType.Warning));
|
||||||
}
|
}
|
||||||
return ok;
|
return ok;
|
||||||
@@ -80,4 +194,193 @@ public class AutoDetectRequestService
|
|||||||
_logger.LogInformation("Nearby: sending accept notify via {endpoint}", endpoint);
|
_logger.LogInformation("Nearby: sending accept notify via {endpoint}", endpoint);
|
||||||
return await _client.SendAcceptAsync(endpoint!, targetUid, displayName, ct).ConfigureAwait(false);
|
return await _client.SendAcceptAsync(endpoint!, targetUid, displayName, ct).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<bool> SendDirectUidRequestAsync(string uid, string? targetDisplayName = null, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
if (!_configService.Current.AllowAutoDetectPairRequests)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Nearby request blocked: AllowAutoDetectPairRequests is disabled");
|
||||||
|
_mediator.Publish(new NotificationMessage("Nearby request blocked", "Enable 'Allow pair requests' in Settings to send requests.", NotificationType.Info));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(uid))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Direct pair request aborted: UID is empty");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetKey = BuildTargetKey(uid, null, targetDisplayName);
|
||||||
|
if (!string.IsNullOrEmpty(targetKey))
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
lock (_syncRoot)
|
||||||
|
{
|
||||||
|
if (_refusalTrackers.TryGetValue(targetKey, out var tracker))
|
||||||
|
{
|
||||||
|
if (tracker.LockUntil.HasValue && tracker.LockUntil.Value > now)
|
||||||
|
{
|
||||||
|
PublishLockNotification(tracker.LockUntil.Value - now);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tracker.LockUntil.HasValue && tracker.LockUntil.Value <= now)
|
||||||
|
{
|
||||||
|
tracker.LockUntil = null;
|
||||||
|
tracker.Count = 0;
|
||||||
|
if (tracker.Count == 0 && tracker.LockUntil == null)
|
||||||
|
{
|
||||||
|
_refusalTrackers.Remove(targetKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_activeCooldowns.TryGetValue(targetKey, out var lastSent))
|
||||||
|
{
|
||||||
|
var elapsed = now - lastSent;
|
||||||
|
if (elapsed < RequestCooldown)
|
||||||
|
{
|
||||||
|
PublishCooldownNotification(RequestCooldown - elapsed);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elapsed >= RequestCooldown)
|
||||||
|
{
|
||||||
|
_activeCooldowns.Remove(targetKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _apiController.UserAddPair(new UserDto(new UserData(uid))).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(targetKey))
|
||||||
|
{
|
||||||
|
lock (_syncRoot)
|
||||||
|
{
|
||||||
|
_activeCooldowns[targetKey] = DateTime.UtcNow;
|
||||||
|
if (_refusalTrackers.TryGetValue(targetKey, out var tracker))
|
||||||
|
{
|
||||||
|
tracker.Count = 0;
|
||||||
|
tracker.LockUntil = null;
|
||||||
|
if (tracker.Count == 0 && tracker.LockUntil == null)
|
||||||
|
{
|
||||||
|
_refusalTrackers.Remove(targetKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_mediator.Publish(new NotificationMessage("Nearby request sent", "The other user will receive a request notification.", NotificationType.Info));
|
||||||
|
var pendingKey = EnsureTargetKey(targetKey);
|
||||||
|
var label = !string.IsNullOrWhiteSpace(targetDisplayName) ? targetDisplayName! : uid;
|
||||||
|
_pendingRequests[pendingKey] = new PendingRequestInfo(pendingKey, uid, null, label, DateTime.UtcNow);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Direct pair request failed for {uid}", uid);
|
||||||
|
if (!string.IsNullOrEmpty(targetKey))
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
lock (_syncRoot)
|
||||||
|
{
|
||||||
|
_activeCooldowns.Remove(targetKey);
|
||||||
|
if (!_refusalTrackers.TryGetValue(targetKey, out var tracker))
|
||||||
|
{
|
||||||
|
tracker = new RefusalTracker();
|
||||||
|
_refusalTrackers[targetKey] = tracker;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tracker.LockUntil.HasValue && tracker.LockUntil.Value <= now)
|
||||||
|
{
|
||||||
|
tracker.LockUntil = null;
|
||||||
|
tracker.Count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
tracker.Count++;
|
||||||
|
if (tracker.Count >= 3)
|
||||||
|
{
|
||||||
|
tracker.LockUntil = now.Add(RefusalLockDuration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_pendingRequests.TryRemove(targetKey, out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
_mediator.Publish(new NotificationMessage("Nearby request failed", "The server rejected the request. Try again soon.", NotificationType.Warning));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? BuildTargetKey(string? uid, string? token, string? displayName)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(uid)) return "uid:" + uid;
|
||||||
|
if (!string.IsNullOrEmpty(token)) return "token:" + token;
|
||||||
|
if (!string.IsNullOrEmpty(displayName)) return "name:" + displayName;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PublishCooldownNotification(TimeSpan remaining)
|
||||||
|
{
|
||||||
|
var durationText = FormatDuration(remaining);
|
||||||
|
_mediator.Publish(new NotificationMessage("Nearby request en attente", $"Nearby request déjà envoyée. Merci d'attendre environ {durationText} avant de réessayer.", NotificationType.Info, TimeSpan.FromSeconds(5)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PublishLockNotification(TimeSpan remaining)
|
||||||
|
{
|
||||||
|
var durationText = FormatDuration(remaining);
|
||||||
|
_mediator.Publish(new NotificationMessage("Nearby request bloquée", $"Nearby request bloquée après plusieurs refus. Réessayez dans {durationText}.", NotificationType.Warning, TimeSpan.FromSeconds(5)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatDuration(TimeSpan remaining)
|
||||||
|
{
|
||||||
|
if (remaining.TotalMinutes >= 1)
|
||||||
|
{
|
||||||
|
var minutes = Math.Max(1, (int)Math.Ceiling(remaining.TotalMinutes));
|
||||||
|
return minutes == 1 ? "1 minute" : minutes + " minutes";
|
||||||
|
}
|
||||||
|
|
||||||
|
var seconds = Math.Max(1, (int)Math.Ceiling(remaining.TotalSeconds));
|
||||||
|
return seconds == 1 ? "1 seconde" : seconds + " secondes";
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class RefusalTracker
|
||||||
|
{
|
||||||
|
public int Count;
|
||||||
|
public DateTime? LockUntil;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyCollection<PendingRequestInfo> GetPendingRequestsSnapshot()
|
||||||
|
{
|
||||||
|
return _pendingRequests.Values.OrderByDescending(v => v.SentAt).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemovePendingRequestByUid(string uid)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(uid)) return;
|
||||||
|
foreach (var kvp in _pendingRequests)
|
||||||
|
{
|
||||||
|
if (string.Equals(kvp.Value.Uid, uid, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
_pendingRequests.TryRemove(kvp.Key, out _);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemovePendingRequestByKey(string key)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(key)) return;
|
||||||
|
_pendingRequests.TryRemove(key, out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string EnsureTargetKey(string? targetKey)
|
||||||
|
{
|
||||||
|
return !string.IsNullOrEmpty(targetKey) ? targetKey! : "target:" + Guid.NewGuid().ToString("N");
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record PendingRequestInfo(string Key, string? Uid, string? Token, string TargetDisplayName, DateTime SentAt);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,209 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using Lumina.Excel.Sheets;
|
||||||
|
using MareSynchronos.MareConfiguration;
|
||||||
|
using MareSynchronos.MareConfiguration.Models;
|
||||||
|
using MareSynchronos.Services;
|
||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace MareSynchronos.Services.AutoDetect;
|
||||||
|
|
||||||
|
public sealed class AutoDetectSuppressionService : IHostedService, IMediatorSubscriber
|
||||||
|
{
|
||||||
|
private static readonly string[] ContentTypeKeywords =
|
||||||
|
[
|
||||||
|
"dungeon",
|
||||||
|
"donjon",
|
||||||
|
"raid",
|
||||||
|
"trial",
|
||||||
|
"défi",
|
||||||
|
"front",
|
||||||
|
"frontline",
|
||||||
|
"pvp",
|
||||||
|
"jcj",
|
||||||
|
"conflict",
|
||||||
|
"conflit"
|
||||||
|
];
|
||||||
|
|
||||||
|
private readonly ILogger<AutoDetectSuppressionService> _logger;
|
||||||
|
private readonly MareConfigService _configService;
|
||||||
|
private readonly IClientState _clientState;
|
||||||
|
private readonly IDataManager _dataManager;
|
||||||
|
private readonly MareMediator _mediator;
|
||||||
|
private readonly DalamudUtilService _dalamudUtilService;
|
||||||
|
|
||||||
|
private bool _isSuppressed;
|
||||||
|
private bool _hasSavedState;
|
||||||
|
private bool _savedDiscoveryEnabled;
|
||||||
|
private bool _savedAllowRequests;
|
||||||
|
private bool _suppressionWarningShown;
|
||||||
|
public bool IsSuppressed => _isSuppressed;
|
||||||
|
|
||||||
|
public AutoDetectSuppressionService(ILogger<AutoDetectSuppressionService> logger,
|
||||||
|
MareConfigService configService, IClientState clientState,
|
||||||
|
IDataManager dataManager, DalamudUtilService dalamudUtilService, MareMediator mediator)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_configService = configService;
|
||||||
|
_clientState = clientState;
|
||||||
|
_dataManager = dataManager;
|
||||||
|
_dalamudUtilService = dalamudUtilService;
|
||||||
|
_mediator = mediator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MareMediator Mediator => _mediator;
|
||||||
|
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_mediator.Subscribe<ZoneSwitchEndMessage>(this, _ => UpdateSuppressionState());
|
||||||
|
_mediator.Subscribe<DalamudLoginMessage>(this, _ => UpdateSuppressionState());
|
||||||
|
_mediator.Subscribe<DalamudLogoutMessage>(this, _ => ClearSuppression());
|
||||||
|
UpdateSuppressionState();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StopAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_mediator.UnsubscribeAll(this);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateSuppressionState()
|
||||||
|
{
|
||||||
|
_ = _dalamudUtilService.RunOnFrameworkThread(() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!_clientState.IsLoggedIn || _clientState.LocalPlayer == null)
|
||||||
|
{
|
||||||
|
ClearSuppression();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint territoryId = _clientState.TerritoryType;
|
||||||
|
bool shouldSuppress = ShouldSuppressForTerritory(territoryId);
|
||||||
|
ApplySuppression(shouldSuppress, territoryId);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to update AutoDetect suppression state");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplySuppression(bool shouldSuppress, uint territoryId)
|
||||||
|
{
|
||||||
|
if (shouldSuppress)
|
||||||
|
{
|
||||||
|
if (!_isSuppressed)
|
||||||
|
{
|
||||||
|
_savedDiscoveryEnabled = _configService.Current.EnableAutoDetectDiscovery;
|
||||||
|
_savedAllowRequests = _configService.Current.AllowAutoDetectPairRequests;
|
||||||
|
_hasSavedState = true;
|
||||||
|
_isSuppressed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool changed = false;
|
||||||
|
if (_configService.Current.EnableAutoDetectDiscovery)
|
||||||
|
{
|
||||||
|
_configService.Current.EnableAutoDetectDiscovery = false;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (_configService.Current.AllowAutoDetectPairRequests)
|
||||||
|
{
|
||||||
|
_configService.Current.AllowAutoDetectPairRequests = false;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("AutoDetect temporarily disabled in instanced content (territory {territoryId}).", territoryId);
|
||||||
|
if (!_suppressionWarningShown)
|
||||||
|
{
|
||||||
|
_suppressionWarningShown = true;
|
||||||
|
const string warningText = "Zone instanciée détectée : les fonctions AutoDetect/Nearby sont coupées pour économiser de la bande passante.";
|
||||||
|
_mediator.Publish(new DualNotificationMessage("AutoDetect désactivé",
|
||||||
|
warningText,
|
||||||
|
NotificationType.Warning, TimeSpan.FromSeconds(5)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!_isSuppressed) return;
|
||||||
|
|
||||||
|
bool changed = false;
|
||||||
|
bool wasSuppressed = _suppressionWarningShown;
|
||||||
|
if (_hasSavedState)
|
||||||
|
{
|
||||||
|
if (_configService.Current.EnableAutoDetectDiscovery != _savedDiscoveryEnabled)
|
||||||
|
{
|
||||||
|
_configService.Current.EnableAutoDetectDiscovery = _savedDiscoveryEnabled;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (_configService.Current.AllowAutoDetectPairRequests != _savedAllowRequests)
|
||||||
|
{
|
||||||
|
_configService.Current.AllowAutoDetectPairRequests = _savedAllowRequests;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_isSuppressed = false;
|
||||||
|
_hasSavedState = false;
|
||||||
|
_suppressionWarningShown = false;
|
||||||
|
|
||||||
|
if (changed || wasSuppressed)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("AutoDetect restored after leaving instanced content (territory {territoryId}).", territoryId);
|
||||||
|
const string restoredText = "Vous avez quitté la zone instanciée : AutoDetect/Nearby fonctionnent de nouveau.";
|
||||||
|
_mediator.Publish(new DualNotificationMessage("AutoDetect réactivé",
|
||||||
|
restoredText,
|
||||||
|
NotificationType.Info, TimeSpan.FromSeconds(5)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClearSuppression()
|
||||||
|
{
|
||||||
|
if (!_isSuppressed) return;
|
||||||
|
_isSuppressed = false;
|
||||||
|
if (_hasSavedState)
|
||||||
|
{
|
||||||
|
_configService.Current.EnableAutoDetectDiscovery = _savedDiscoveryEnabled;
|
||||||
|
_configService.Current.AllowAutoDetectPairRequests = _savedAllowRequests;
|
||||||
|
}
|
||||||
|
_hasSavedState = false;
|
||||||
|
_suppressionWarningShown = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool ShouldSuppressForTerritory(uint territoryId)
|
||||||
|
{
|
||||||
|
if (territoryId == 0) return false;
|
||||||
|
|
||||||
|
var cfcSheet = _dataManager.GetExcelSheet<ContentFinderCondition>();
|
||||||
|
if (cfcSheet == null) return false;
|
||||||
|
|
||||||
|
var cfc = cfcSheet.FirstOrDefault(c => c.TerritoryType.RowId == territoryId);
|
||||||
|
if (cfc.RowId == 0) return false;
|
||||||
|
|
||||||
|
if (MatchesSuppressionKeyword(cfc.Name.ToString())) return true;
|
||||||
|
|
||||||
|
var contentType = cfc.ContentType.Value;
|
||||||
|
if (contentType.RowId == 0) return false;
|
||||||
|
|
||||||
|
return MatchesSuppressionKeyword(contentType.Name.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool MatchesSuppressionKeyword(string? text)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(text)) return false;
|
||||||
|
return ContentTypeKeywords.Any(keyword => text.Contains(keyword, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,7 +17,6 @@ public class DiscoveryConfigProvider
|
|||||||
private readonly TokenProvider _tokenProvider;
|
private readonly TokenProvider _tokenProvider;
|
||||||
|
|
||||||
private WellKnownRoot? _config;
|
private WellKnownRoot? _config;
|
||||||
private DateTimeOffset _lastLoad = DateTimeOffset.MinValue;
|
|
||||||
|
|
||||||
public DiscoveryConfigProvider(ILogger<DiscoveryConfigProvider> logger, ServerConfigurationManager serverManager, TokenProvider tokenProvider)
|
public DiscoveryConfigProvider(ILogger<DiscoveryConfigProvider> logger, ServerConfigurationManager serverManager, TokenProvider tokenProvider)
|
||||||
{
|
{
|
||||||
@@ -51,7 +50,6 @@ public class DiscoveryConfigProvider
|
|||||||
|
|
||||||
root.NearbyDiscovery?.Hydrate();
|
root.NearbyDiscovery?.Hydrate();
|
||||||
_config = root;
|
_config = root;
|
||||||
_lastLoad = DateTimeOffset.UtcNow;
|
|
||||||
_logger.LogDebug("Loaded Nearby well-known (stapled), enabled={enabled}, expires={exp}", NearbyEnabled, _config?.NearbyDiscovery?.SaltExpiresAt);
|
_logger.LogDebug("Loaded Nearby well-known (stapled), enabled={enabled}, expires={exp}", NearbyEnabled, _config?.NearbyDiscovery?.SaltExpiresAt);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -97,7 +95,6 @@ public class DiscoveryConfigProvider
|
|||||||
|
|
||||||
root.NearbyDiscovery?.Hydrate();
|
root.NearbyDiscovery?.Hydrate();
|
||||||
_config = root;
|
_config = root;
|
||||||
_lastLoad = DateTimeOffset.UtcNow;
|
|
||||||
_logger.LogInformation("Loaded Nearby well-known (http {path}), enabled={enabled}", path, NearbyEnabled);
|
_logger.LogInformation("Loaded Nearby well-known (http {path}), enabled={enabled}", path, NearbyEnabled);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using MareSynchronos.Services.Mediator;
|
using MareSynchronos.Services.Mediator;
|
||||||
@@ -7,6 +8,7 @@ using MareSynchronos.WebAPI.AutoDetect;
|
|||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Collections.Generic;
|
||||||
using MareSynchronos.Utils;
|
using MareSynchronos.Utils;
|
||||||
|
|
||||||
namespace MareSynchronos.Services.AutoDetect;
|
namespace MareSynchronos.Services.AutoDetect;
|
||||||
@@ -34,6 +36,8 @@ public class NearbyDiscoveryService : IHostedService, IMediatorSubscriber
|
|||||||
private bool _lastAutoDetectState;
|
private bool _lastAutoDetectState;
|
||||||
private DateTime _lastHeartbeat = DateTime.MinValue;
|
private DateTime _lastHeartbeat = DateTime.MinValue;
|
||||||
private static readonly TimeSpan HeartbeatInterval = TimeSpan.FromSeconds(75);
|
private static readonly TimeSpan HeartbeatInterval = TimeSpan.FromSeconds(75);
|
||||||
|
private readonly object _entriesLock = new();
|
||||||
|
private List<NearbyEntry> _lastEntries = [];
|
||||||
|
|
||||||
public NearbyDiscoveryService(ILogger<NearbyDiscoveryService> logger, MareMediator mediator,
|
public NearbyDiscoveryService(ILogger<NearbyDiscoveryService> logger, MareMediator mediator,
|
||||||
MareConfigService config, DiscoveryConfigProvider configProvider, DalamudUtilService dalamudUtilService,
|
MareConfigService config, DiscoveryConfigProvider configProvider, DalamudUtilService dalamudUtilService,
|
||||||
@@ -52,6 +56,7 @@ public class NearbyDiscoveryService : IHostedService, IMediatorSubscriber
|
|||||||
|
|
||||||
public Task StartAsync(CancellationToken cancellationToken)
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
CancelAndDispose(ref _loopCts);
|
||||||
_loopCts = new CancellationTokenSource();
|
_loopCts = new CancellationTokenSource();
|
||||||
_mediator.Subscribe<ConnectedMessage>(this, _ => { _isConnected = true; _configProvider.TryLoadFromStapled(); });
|
_mediator.Subscribe<ConnectedMessage>(this, _ => { _isConnected = true; _configProvider.TryLoadFromStapled(); });
|
||||||
_mediator.Subscribe<DisconnectedMessage>(this, _ => { _isConnected = false; _lastPublishedSignature = null; });
|
_mediator.Subscribe<DisconnectedMessage>(this, _ => { _isConnected = false; _lastPublishedSignature = null; });
|
||||||
@@ -128,10 +133,41 @@ public class NearbyDiscoveryService : IHostedService, IMediatorSubscriber
|
|||||||
public Task StopAsync(CancellationToken cancellationToken)
|
public Task StopAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
_mediator.UnsubscribeAll(this);
|
_mediator.UnsubscribeAll(this);
|
||||||
try { _loopCts?.Cancel(); } catch { }
|
CancelAndDispose(ref _loopCts);
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<NearbyEntry> SnapshotEntries()
|
||||||
|
{
|
||||||
|
lock (_entriesLock)
|
||||||
|
{
|
||||||
|
return _lastEntries.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateSnapshot(List<NearbyEntry> entries)
|
||||||
|
{
|
||||||
|
lock (_entriesLock)
|
||||||
|
{
|
||||||
|
_lastEntries = entries.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CancelAndDispose(ref CancellationTokenSource? cts)
|
||||||
|
{
|
||||||
|
if (cts == null) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
cts.Cancel();
|
||||||
|
}
|
||||||
|
catch (ObjectDisposedException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
cts.Dispose();
|
||||||
|
cts = null;
|
||||||
|
}
|
||||||
|
|
||||||
private async Task Loop(CancellationToken ct)
|
private async Task Loop(CancellationToken ct)
|
||||||
{
|
{
|
||||||
_configProvider.TryLoadFromStapled();
|
_configProvider.TryLoadFromStapled();
|
||||||
@@ -426,6 +462,7 @@ public class NearbyDiscoveryService : IHostedService, IMediatorSubscriber
|
|||||||
_logger.LogDebug("Nearby: well-known not available or disabled; running in local-only mode");
|
_logger.LogDebug("Nearby: well-known not available or disabled; running in local-only mode");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
UpdateSnapshot(entries);
|
||||||
_mediator.Publish(new DiscoveryListUpdated(entries));
|
_mediator.Publish(new DiscoveryListUpdated(entries));
|
||||||
|
|
||||||
var delayMs = Math.Max(1000, _configProvider.MinQueryIntervalMs);
|
var delayMs = Math.Max(1000, _configProvider.MinQueryIntervalMs);
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
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;
|
||||||
|
|
||||||
@@ -12,17 +15,21 @@ 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 Regex ReqRegex = new(@"^Nearby Request: (.+) \[(?<uid>[A-Z0-9]+)\]$", RegexOptions.Compiled);
|
private static readonly TimeSpan RegexTimeout = TimeSpan.FromSeconds(1);
|
||||||
private static readonly Regex AcceptRegex = new(@"^Nearby Accept: (.+) \[(?<uid>[A-Z0-9]+)\]$", RegexOptions.Compiled);
|
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);
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
public MareMediator Mediator => _mediator;
|
public MareMediator Mediator => _mediator;
|
||||||
@@ -41,6 +48,8 @@ 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);
|
||||||
|
_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;
|
||||||
@@ -62,11 +71,29 @@ 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)
|
||||||
|
{
|
||||||
|
if (!string.Equals(msg.TargetUid, _api.UID, StringComparison.Ordinal))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var display = !string.IsNullOrWhiteSpace(msg.DisplayName)
|
||||||
|
? msg.DisplayName!
|
||||||
|
: (!string.IsNullOrWhiteSpace(msg.SourceAlias) ? msg.SourceAlias : msg.SourceUid);
|
||||||
|
|
||||||
|
_pending[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)));
|
||||||
|
_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);
|
||||||
|
_notificationTracker.Remove(NotificationCategory.AutoDetect, uid);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> AcceptAsync(string uid)
|
public async Task<bool> AcceptAsync(string uid)
|
||||||
@@ -75,7 +102,9 @@ public sealed class NearbyPendingService : IMediatorSubscriber
|
|||||||
{
|
{
|
||||||
await _api.UserAddPair(new MareSynchronos.API.Dto.User.UserDto(new MareSynchronos.API.Data.UserData(uid))).ConfigureAwait(false);
|
await _api.UserAddPair(new MareSynchronos.API.Dto.User.UserDto(new MareSynchronos.API.Data.UserData(uid))).ConfigureAwait(false);
|
||||||
_pending.TryRemove(uid, out _);
|
_pending.TryRemove(uid, out _);
|
||||||
|
_requestService.RemovePendingRequestByUid(uid);
|
||||||
_ = _requestService.SendAcceptNotifyAsync(uid);
|
_ = _requestService.SendAcceptNotifyAsync(uid);
|
||||||
|
_notificationTracker.Remove(NotificationCategory.AutoDetect, uid);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|||||||
146
MareSynchronos/Services/AutoDetect/SyncshellDiscoveryService.cs
Normal file
146
MareSynchronos/Services/AutoDetect/SyncshellDiscoveryService.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ using MareSynchronos.Services.CharaData.Models;
|
|||||||
using MareSynchronos.Utils;
|
using MareSynchronos.Utils;
|
||||||
using MareSynchronos.WebAPI.Files;
|
using MareSynchronos.WebAPI.Files;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
namespace MareSynchronos.Services;
|
namespace MareSynchronos.Services;
|
||||||
|
|
||||||
@@ -295,6 +296,32 @@ public sealed class CharaDataFileHandler : IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal async Task<byte[]?> CreateCharaFileBytesAsync(string description, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
var tempFilePath = Path.Combine(Path.GetTempPath(), "umbra_mcdfshare_" + Guid.NewGuid().ToString("N") + ".mcdf");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await SaveCharaFileAsync(description, tempFilePath).ConfigureAwait(false);
|
||||||
|
if (!File.Exists(tempFilePath)) return null;
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
return await File.ReadAllBytesAsync(tempFilePath, token).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (File.Exists(tempFilePath))
|
||||||
|
{
|
||||||
|
File.Delete(tempFilePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
internal async Task<List<string>> UploadFiles(List<string> fileList, ValueProgress<string> uploadProgress, CancellationToken token)
|
internal async Task<List<string>> UploadFiles(List<string> fileList, ValueProgress<string> uploadProgress, CancellationToken token)
|
||||||
{
|
{
|
||||||
return await _fileUploadManager.UploadFiles(fileList, uploadProgress, token).ConfigureAwait(false);
|
return await _fileUploadManager.UploadFiles(fileList, uploadProgress, token).ConfigureAwait(false);
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ using MareSynchronos.Utils;
|
|||||||
using MareSynchronos.WebAPI;
|
using MareSynchronos.WebAPI;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
using System.IO;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
namespace MareSynchronos.Services;
|
namespace MareSynchronos.Services;
|
||||||
|
|
||||||
@@ -457,6 +459,14 @@ public sealed partial class CharaDataManager : DisposableMediatorSubscriberBase
|
|||||||
LoadedMcdfHeader = _fileHandler.LoadCharaFileHeader(filePath);
|
LoadedMcdfHeader = _fileHandler.LoadCharaFileHeader(filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<string> LoadMcdfFromBytes(byte[] data, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
var tempFilePath = Path.Combine(Path.GetTempPath(), "umbra_mcdfshare_" + Guid.NewGuid().ToString("N") + ".mcdf");
|
||||||
|
await File.WriteAllBytesAsync(tempFilePath, data, token).ConfigureAwait(false);
|
||||||
|
LoadedMcdfHeader = _fileHandler.LoadCharaFileHeader(tempFilePath);
|
||||||
|
return tempFilePath;
|
||||||
|
}
|
||||||
|
|
||||||
public void McdfApplyToTarget(string charaName)
|
public void McdfApplyToTarget(string charaName)
|
||||||
{
|
{
|
||||||
if (LoadedMcdfHeader == null || !LoadedMcdfHeader.IsCompletedSuccessfully) return;
|
if (LoadedMcdfHeader == null || !LoadedMcdfHeader.IsCompletedSuccessfully) return;
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ public sealed class CharaDataNearbyManager : DisposableMediatorSubscriberBase
|
|||||||
private Task? _filterEntriesRunningTask;
|
private Task? _filterEntriesRunningTask;
|
||||||
private (Guid VfxId, PoseEntryExtended Pose)? _hoveredVfx = null;
|
private (Guid VfxId, PoseEntryExtended Pose)? _hoveredVfx = null;
|
||||||
private DateTime _lastExecutionTime = DateTime.UtcNow;
|
private DateTime _lastExecutionTime = DateTime.UtcNow;
|
||||||
private SemaphoreSlim _sharedDataUpdateSemaphore = new(1, 1);
|
private readonly SemaphoreSlim _sharedDataUpdateSemaphore = new(1, 1);
|
||||||
public CharaDataNearbyManager(ILogger<CharaDataNearbyManager> logger, MareMediator mediator,
|
public CharaDataNearbyManager(ILogger<CharaDataNearbyManager> logger, MareMediator mediator,
|
||||||
DalamudUtilService dalamudUtilService, VfxSpawnManager vfxSpawnManager,
|
DalamudUtilService dalamudUtilService, VfxSpawnManager vfxSpawnManager,
|
||||||
ServerConfigurationManager serverConfigurationManager,
|
ServerConfigurationManager serverConfigurationManager,
|
||||||
@@ -201,7 +201,7 @@ public sealed class CharaDataNearbyManager : DisposableMediatorSubscriberBase
|
|||||||
foreach (var data in _metaInfoCache.Where(d => (string.IsNullOrWhiteSpace(UserNoteFilter)
|
foreach (var data in _metaInfoCache.Where(d => (string.IsNullOrWhiteSpace(UserNoteFilter)
|
||||||
|| ((d.Key.Alias ?? string.Empty).Contains(UserNoteFilter, StringComparison.OrdinalIgnoreCase)
|
|| ((d.Key.Alias ?? string.Empty).Contains(UserNoteFilter, StringComparison.OrdinalIgnoreCase)
|
||||||
|| d.Key.UID.Contains(UserNoteFilter, StringComparison.OrdinalIgnoreCase)
|
|| d.Key.UID.Contains(UserNoteFilter, StringComparison.OrdinalIgnoreCase)
|
||||||
|| (_serverConfigurationManager.GetNoteForUid(UserNoteFilter) ?? string.Empty).Contains(UserNoteFilter, StringComparison.OrdinalIgnoreCase))))
|
|| (_serverConfigurationManager.GetNoteForUid(d.Key.UID) ?? string.Empty).Contains(UserNoteFilter, StringComparison.OrdinalIgnoreCase))))
|
||||||
.ToDictionary(k => k.Key, k => k.Value))
|
.ToDictionary(k => k.Key, k => k.Value))
|
||||||
{
|
{
|
||||||
// filter all poses based on territory, that always must be correct
|
// filter all poses based on territory, that always must be correct
|
||||||
@@ -266,26 +266,47 @@ public sealed class CharaDataNearbyManager : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
private void ManageWispsNearby(List<PoseEntryExtended> previousPoses)
|
private void ManageWispsNearby(List<PoseEntryExtended> previousPoses)
|
||||||
{
|
{
|
||||||
foreach (var data in _nearbyData.Keys)
|
int maxWisps = _charaDataConfigService.Current.NearbyMaxWisps;
|
||||||
|
if (maxWisps <= 0)
|
||||||
{
|
{
|
||||||
if (_poseVfx.TryGetValue(data, out var _)) continue;
|
ClearAllVfx();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Guid? vfxGuid;
|
const int hardLimit = 200;
|
||||||
if (data.MetaInfo.IsOwnData)
|
if (maxWisps > hardLimit) maxWisps = hardLimit;
|
||||||
|
|
||||||
|
var orderedAllowedPoses = _nearbyData
|
||||||
|
.OrderBy(kvp => kvp.Value.Distance)
|
||||||
|
.Take(maxWisps)
|
||||||
|
.Select(kvp => kvp.Key)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var allowedPoseSet = orderedAllowedPoses.ToHashSet();
|
||||||
|
|
||||||
|
foreach (var data in orderedAllowedPoses)
|
||||||
{
|
{
|
||||||
vfxGuid = _vfxSpawnManager.SpawnObject(data.Position, data.Rotation, Vector3.One * 2, 0.8f, 0.5f, 0.0f, 0.7f);
|
if (_poseVfx.TryGetValue(data, out _)) continue;
|
||||||
}
|
|
||||||
else
|
Guid? vfxGuid = data.MetaInfo.IsOwnData
|
||||||
{
|
? _vfxSpawnManager.SpawnObject(data.Position, data.Rotation, Vector3.One * 2, 0.8f, 0.5f, 0.0f, 0.7f)
|
||||||
vfxGuid = _vfxSpawnManager.SpawnObject(data.Position, data.Rotation, Vector3.One * 2);
|
: _vfxSpawnManager.SpawnObject(data.Position, data.Rotation, Vector3.One * 2);
|
||||||
}
|
|
||||||
if (vfxGuid != null)
|
if (vfxGuid != null)
|
||||||
{
|
{
|
||||||
_poseVfx[data] = vfxGuid.Value;
|
_poseVfx[data] = vfxGuid.Value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var data in previousPoses.Except(_nearbyData.Keys))
|
foreach (var data in previousPoses.Except(allowedPoseSet))
|
||||||
|
{
|
||||||
|
if (_poseVfx.Remove(data, out var guid))
|
||||||
|
{
|
||||||
|
_vfxSpawnManager.DespawnObject(guid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var data in _poseVfx.Keys.Except(allowedPoseSet).ToList())
|
||||||
{
|
{
|
||||||
if (_poseVfx.Remove(data, out var guid))
|
if (_poseVfx.Remove(data, out var guid))
|
||||||
{
|
{
|
||||||
|
|||||||
309
MareSynchronos/Services/CharaData/McdfShareManager.cs
Normal file
309
MareSynchronos/Services/CharaData/McdfShareManager.cs
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using MareSynchronos.API.Dto.McdfShare;
|
||||||
|
using MareSynchronos.MareConfiguration.Models;
|
||||||
|
using MareSynchronos.Services.ServerConfiguration;
|
||||||
|
using MareSynchronos.WebAPI;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace MareSynchronos.Services.CharaData;
|
||||||
|
|
||||||
|
public sealed class McdfShareManager
|
||||||
|
{
|
||||||
|
private readonly ILogger<McdfShareManager> _logger;
|
||||||
|
private readonly ApiController _apiController;
|
||||||
|
private readonly CharaDataFileHandler _fileHandler;
|
||||||
|
private readonly CharaDataManager _charaDataManager;
|
||||||
|
private readonly ServerConfigurationManager _serverConfigurationManager;
|
||||||
|
private readonly SemaphoreSlim _operationSemaphore = new(1, 1);
|
||||||
|
private readonly List<McdfShareEntryDto> _ownShares = new();
|
||||||
|
private readonly List<McdfShareEntryDto> _sharedWithMe = new();
|
||||||
|
private Task? _currentTask;
|
||||||
|
|
||||||
|
public McdfShareManager(ILogger<McdfShareManager> logger, ApiController apiController,
|
||||||
|
CharaDataFileHandler fileHandler, CharaDataManager charaDataManager,
|
||||||
|
ServerConfigurationManager serverConfigurationManager)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_apiController = apiController;
|
||||||
|
_fileHandler = fileHandler;
|
||||||
|
_charaDataManager = charaDataManager;
|
||||||
|
_serverConfigurationManager = serverConfigurationManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<McdfShareEntryDto> OwnShares => _ownShares;
|
||||||
|
public IReadOnlyList<McdfShareEntryDto> SharedShares => _sharedWithMe;
|
||||||
|
public bool IsBusy => _currentTask is { IsCompleted: false };
|
||||||
|
public string? LastError { get; private set; }
|
||||||
|
public string? LastSuccess { get; private set; }
|
||||||
|
|
||||||
|
public Task RefreshAsync(CancellationToken token)
|
||||||
|
{
|
||||||
|
return RunOperation(() => InternalRefreshAsync(token));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task CreateShareAsync(string description, IReadOnlyList<string> allowedIndividuals, IReadOnlyList<string> allowedSyncshells, DateTime? expiresAtUtc, CancellationToken token)
|
||||||
|
{
|
||||||
|
return RunOperation(async () =>
|
||||||
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
var mcdfBytes = await _fileHandler.CreateCharaFileBytesAsync(description, token).ConfigureAwait(false);
|
||||||
|
if (mcdfBytes == null || mcdfBytes.Length == 0)
|
||||||
|
{
|
||||||
|
LastError = "Impossible de préparer les données MCDF.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var secretKey = _serverConfigurationManager.GetSecretKey(out bool hasMultiple);
|
||||||
|
if (hasMultiple)
|
||||||
|
{
|
||||||
|
LastError = "Plusieurs clés secrètes sont configurées pour ce personnage. Corrigez cela dans les paramètres.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(secretKey))
|
||||||
|
{
|
||||||
|
LastError = "Aucune clé secrète n'est configurée pour ce personnage.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var shareId = Guid.NewGuid();
|
||||||
|
byte[] salt = RandomNumberGenerator.GetBytes(16);
|
||||||
|
byte[] nonce = RandomNumberGenerator.GetBytes(12);
|
||||||
|
byte[] key = DeriveKey(secretKey, shareId, salt);
|
||||||
|
|
||||||
|
byte[] cipher = new byte[mcdfBytes.Length];
|
||||||
|
byte[] tag = new byte[16];
|
||||||
|
|
||||||
|
using (var aes = new AesGcm(key, 16))
|
||||||
|
{
|
||||||
|
aes.Encrypt(nonce, mcdfBytes, cipher, tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
var uploadDto = new McdfShareUploadRequestDto
|
||||||
|
{
|
||||||
|
ShareId = shareId,
|
||||||
|
Description = description,
|
||||||
|
CipherData = cipher,
|
||||||
|
Nonce = nonce,
|
||||||
|
Salt = salt,
|
||||||
|
Tag = tag,
|
||||||
|
ExpiresAtUtc = expiresAtUtc,
|
||||||
|
AllowedIndividuals = allowedIndividuals.ToList(),
|
||||||
|
AllowedSyncshells = allowedSyncshells.ToList()
|
||||||
|
};
|
||||||
|
|
||||||
|
await _apiController.McdfShareUpload(uploadDto).ConfigureAwait(false);
|
||||||
|
await InternalRefreshAsync(token).ConfigureAwait(false);
|
||||||
|
LastSuccess = "Partage MCDF créé.";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task DeleteShareAsync(Guid shareId)
|
||||||
|
{
|
||||||
|
return RunOperation(async () =>
|
||||||
|
{
|
||||||
|
var result = await _apiController.McdfShareDelete(shareId).ConfigureAwait(false);
|
||||||
|
if (!result)
|
||||||
|
{
|
||||||
|
LastError = "Le serveur a refusé de supprimer le partage MCDF.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ownShares.RemoveAll(s => s.Id == shareId);
|
||||||
|
_sharedWithMe.RemoveAll(s => s.Id == shareId);
|
||||||
|
await InternalRefreshAsync(CancellationToken.None).ConfigureAwait(false);
|
||||||
|
LastSuccess = "Partage MCDF supprimé.";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task UpdateShareAsync(McdfShareUpdateRequestDto updateRequest)
|
||||||
|
{
|
||||||
|
return RunOperation(async () =>
|
||||||
|
{
|
||||||
|
var updated = await _apiController.McdfShareUpdate(updateRequest).ConfigureAwait(false);
|
||||||
|
if (updated == null)
|
||||||
|
{
|
||||||
|
LastError = "Le serveur a refusé de mettre à jour le partage MCDF.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var idx = _ownShares.FindIndex(s => s.Id == updated.Id);
|
||||||
|
if (idx >= 0)
|
||||||
|
{
|
||||||
|
_ownShares[idx] = updated;
|
||||||
|
}
|
||||||
|
LastSuccess = "Partage MCDF mis à jour.";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task ApplyShareAsync(Guid shareId, CancellationToken token)
|
||||||
|
{
|
||||||
|
return RunOperation(async () =>
|
||||||
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
var plainBytes = await DownloadAndDecryptShareAsync(shareId, token).ConfigureAwait(false);
|
||||||
|
if (plainBytes == null)
|
||||||
|
{
|
||||||
|
LastError ??= "Échec du téléchargement du partage MCDF.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tempPath = await _charaDataManager.LoadMcdfFromBytes(plainBytes, token).ConfigureAwait(false);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _charaDataManager.McdfApplyToGposeTarget().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (File.Exists(tempPath))
|
||||||
|
{
|
||||||
|
File.Delete(tempPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LastSuccess = "Partage MCDF appliqué sur la cible GPose.";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task ExportShareAsync(Guid shareId, string filePath, CancellationToken token)
|
||||||
|
{
|
||||||
|
return RunOperation(async () =>
|
||||||
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
var plainBytes = await DownloadAndDecryptShareAsync(shareId, token).ConfigureAwait(false);
|
||||||
|
if (plainBytes == null)
|
||||||
|
{
|
||||||
|
LastError ??= "Échec du téléchargement du partage MCDF.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var directory = Path.GetDirectoryName(filePath);
|
||||||
|
if (!string.IsNullOrEmpty(directory))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(directory);
|
||||||
|
}
|
||||||
|
|
||||||
|
await File.WriteAllBytesAsync(filePath, plainBytes, token).ConfigureAwait(false);
|
||||||
|
LastSuccess = "Partage MCDF exporté.";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task DownloadShareToFileAsync(McdfShareEntryDto entry, string filePath, CancellationToken token)
|
||||||
|
{
|
||||||
|
return ExportShareAsync(entry.Id, filePath, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<byte[]?> DownloadAndDecryptShareAsync(Guid shareId, CancellationToken token)
|
||||||
|
{
|
||||||
|
var payload = await _apiController.McdfShareDownload(shareId).ConfigureAwait(false);
|
||||||
|
if (payload == null)
|
||||||
|
{
|
||||||
|
LastError = "Partage indisponible.";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var secretKey = _serverConfigurationManager.GetSecretKey(out bool hasMultiple);
|
||||||
|
if (hasMultiple)
|
||||||
|
{
|
||||||
|
LastError = "Plusieurs clés secrètes sont configurées pour ce personnage.";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(secretKey))
|
||||||
|
{
|
||||||
|
LastError = "Aucune clé secrète n'est configurée pour ce personnage.";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] key = DeriveKey(secretKey, payload.ShareId, payload.Salt);
|
||||||
|
byte[] plaintext = new byte[payload.CipherData.Length];
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var aes = new AesGcm(key, 16);
|
||||||
|
aes.Decrypt(payload.Nonce, payload.CipherData, payload.Tag, plaintext);
|
||||||
|
}
|
||||||
|
catch (CryptographicException ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to decrypt MCDF share {ShareId}", shareId);
|
||||||
|
LastError = "Impossible de déchiffrer le partage MCDF.";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
return plaintext;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task InternalRefreshAsync(CancellationToken token)
|
||||||
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
var own = await _apiController.McdfShareGetOwn().ConfigureAwait(false);
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
var shared = await _apiController.McdfShareGetShared().ConfigureAwait(false);
|
||||||
|
_ownShares.Clear();
|
||||||
|
_ownShares.AddRange(own);
|
||||||
|
_sharedWithMe.Clear();
|
||||||
|
_sharedWithMe.AddRange(shared);
|
||||||
|
LastSuccess = "Partages MCDF actualisés.";
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task RunOperation(Func<Task> operation)
|
||||||
|
{
|
||||||
|
async Task Wrapper()
|
||||||
|
{
|
||||||
|
await _operationSemaphore.WaitAsync().ConfigureAwait(false);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
LastError = null;
|
||||||
|
LastSuccess = null;
|
||||||
|
await operation().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error during MCDF share operation");
|
||||||
|
LastError = ex.Message;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_operationSemaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var task = Wrapper();
|
||||||
|
_currentTask = task;
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] DeriveKey(string secretKey, Guid shareId, byte[] salt)
|
||||||
|
{
|
||||||
|
byte[] secretBytes;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
secretBytes = Convert.FromHexString(secretKey);
|
||||||
|
}
|
||||||
|
catch (FormatException)
|
||||||
|
{
|
||||||
|
// fallback to UTF8 if not hex
|
||||||
|
secretBytes = System.Text.Encoding.UTF8.GetBytes(secretKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] shareBytes = shareId.ToByteArray();
|
||||||
|
byte[] material = new byte[secretBytes.Length + shareBytes.Length + salt.Length];
|
||||||
|
Buffer.BlockCopy(secretBytes, 0, material, 0, secretBytes.Length);
|
||||||
|
Buffer.BlockCopy(shareBytes, 0, material, secretBytes.Length, shareBytes.Length);
|
||||||
|
Buffer.BlockCopy(salt, 0, material, secretBytes.Length + shareBytes.Length, salt.Length);
|
||||||
|
return SHA256.HashData(material);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
using Lumina.Data.Files;
|
using System;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using Lumina.Data.Files;
|
||||||
using MareSynchronos.API.Data;
|
using MareSynchronos.API.Data;
|
||||||
using MareSynchronos.API.Data.Enum;
|
using MareSynchronos.API.Data.Enum;
|
||||||
using MareSynchronos.FileCache;
|
using MareSynchronos.FileCache;
|
||||||
|
using MareSynchronos.MareConfiguration;
|
||||||
|
using MareSynchronos.MareConfiguration.Models;
|
||||||
using MareSynchronos.Services.Mediator;
|
using MareSynchronos.Services.Mediator;
|
||||||
using MareSynchronos.UI;
|
using MareSynchronos.UI;
|
||||||
using MareSynchronos.Utils;
|
using MareSynchronos.Utils;
|
||||||
@@ -14,40 +18,52 @@ public sealed class CharacterAnalyzer : DisposableMediatorSubscriberBase
|
|||||||
private readonly FileCacheManager _fileCacheManager;
|
private readonly FileCacheManager _fileCacheManager;
|
||||||
private readonly XivDataAnalyzer _xivDataAnalyzer;
|
private readonly XivDataAnalyzer _xivDataAnalyzer;
|
||||||
private CancellationTokenSource? _analysisCts;
|
private CancellationTokenSource? _analysisCts;
|
||||||
private CancellationTokenSource _baseAnalysisCts = new();
|
private CancellationTokenSource? _baseAnalysisCts = new();
|
||||||
private string _lastDataHash = string.Empty;
|
private string _lastDataHash = string.Empty;
|
||||||
|
private CharacterAnalysisSummary _previousSummary = CharacterAnalysisSummary.Empty;
|
||||||
|
private DateTime _lastAutoAnalysis = DateTime.MinValue;
|
||||||
|
private string _lastAutoAnalysisHash = string.Empty;
|
||||||
|
private const int AutoAnalysisFileDeltaThreshold = 25;
|
||||||
|
private const long AutoAnalysisSizeDeltaThreshold = 50L * 1024 * 1024;
|
||||||
|
private static readonly TimeSpan AutoAnalysisCooldown = TimeSpan.FromMinutes(2);
|
||||||
|
private const long NotificationSizeThreshold = 300L * 1024 * 1024;
|
||||||
|
private const long NotificationTriangleThreshold = 150_000;
|
||||||
|
private bool _sizeWarningShown;
|
||||||
|
private bool _triangleWarningShown;
|
||||||
|
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
|
||||||
|
|
||||||
public CharacterAnalyzer(ILogger<CharacterAnalyzer> logger, MareMediator mediator, FileCacheManager fileCacheManager, XivDataAnalyzer modelAnalyzer)
|
public CharacterAnalyzer(ILogger<CharacterAnalyzer> logger, MareMediator mediator, FileCacheManager fileCacheManager, XivDataAnalyzer modelAnalyzer, PlayerPerformanceConfigService playerPerformanceConfigService)
|
||||||
: base(logger, mediator)
|
: base(logger, mediator)
|
||||||
{
|
{
|
||||||
Mediator.Subscribe<CharacterDataCreatedMessage>(this, (msg) =>
|
Mediator.Subscribe<CharacterDataCreatedMessage>(this, (msg) =>
|
||||||
{
|
{
|
||||||
_baseAnalysisCts = _baseAnalysisCts.CancelRecreate();
|
var tokenSource = EnsureFreshCts(ref _baseAnalysisCts);
|
||||||
var token = _baseAnalysisCts.Token;
|
var token = tokenSource.Token;
|
||||||
_ = BaseAnalysis(msg.CharacterData, token);
|
_ = BaseAnalysis(msg.CharacterData, token);
|
||||||
});
|
});
|
||||||
_fileCacheManager = fileCacheManager;
|
_fileCacheManager = fileCacheManager;
|
||||||
_xivDataAnalyzer = modelAnalyzer;
|
_xivDataAnalyzer = modelAnalyzer;
|
||||||
|
_playerPerformanceConfigService = playerPerformanceConfigService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int CurrentFile { get; internal set; }
|
public int CurrentFile { get; internal set; }
|
||||||
public bool IsAnalysisRunning => _analysisCts != null;
|
public bool IsAnalysisRunning => _analysisCts != null;
|
||||||
public int TotalFiles { get; internal set; }
|
public int TotalFiles { get; internal set; }
|
||||||
|
public CharacterAnalysisSummary CurrentSummary { get; private set; } = CharacterAnalysisSummary.Empty;
|
||||||
|
public DateTime? LastCompletedAnalysis { get; private set; }
|
||||||
internal Dictionary<ObjectKind, Dictionary<string, FileDataEntry>> LastAnalysis { get; } = [];
|
internal Dictionary<ObjectKind, Dictionary<string, FileDataEntry>> LastAnalysis { get; } = [];
|
||||||
|
|
||||||
public void CancelAnalyze()
|
public void CancelAnalyze()
|
||||||
{
|
{
|
||||||
_analysisCts?.CancelDispose();
|
CancelAndDispose(ref _analysisCts);
|
||||||
_analysisCts = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ComputeAnalysis(bool print = true, bool recalculate = false)
|
public async Task ComputeAnalysis(bool print = true, bool recalculate = false)
|
||||||
{
|
{
|
||||||
Logger.LogDebug("=== Calculating Character Analysis ===");
|
Logger.LogDebug("=== Calculating Character Analysis ===");
|
||||||
|
|
||||||
_analysisCts = _analysisCts?.CancelRecreate() ?? new();
|
var analysisCts = EnsureFreshCts(ref _analysisCts);
|
||||||
|
var cancelToken = analysisCts.Token;
|
||||||
var cancelToken = _analysisCts.Token;
|
|
||||||
|
|
||||||
var allFiles = LastAnalysis.SelectMany(v => v.Value.Select(d => d.Value)).ToList();
|
var allFiles = LastAnalysis.SelectMany(v => v.Value.Select(d => d.Value)).ToList();
|
||||||
if (allFiles.Exists(c => !c.IsComputed || recalculate))
|
if (allFiles.Exists(c => !c.IsComputed || recalculate))
|
||||||
@@ -80,10 +96,16 @@ public sealed class CharacterAnalyzer : DisposableMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RefreshSummary(false, _lastDataHash);
|
||||||
|
|
||||||
Mediator.Publish(new CharacterDataAnalyzedMessage());
|
Mediator.Publish(new CharacterDataAnalyzedMessage());
|
||||||
|
|
||||||
_analysisCts.CancelDispose();
|
if (!cancelToken.IsCancellationRequested)
|
||||||
_analysisCts = null;
|
{
|
||||||
|
LastCompletedAnalysis = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
CancelAndDispose(ref _analysisCts);
|
||||||
|
|
||||||
if (print) PrintAnalysis();
|
if (print) PrintAnalysis();
|
||||||
}
|
}
|
||||||
@@ -94,8 +116,8 @@ public sealed class CharacterAnalyzer : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
if (!disposing) return;
|
if (!disposing) return;
|
||||||
|
|
||||||
_analysisCts?.CancelDispose();
|
CancelAndDispose(ref _analysisCts);
|
||||||
_baseAnalysisCts.CancelDispose();
|
CancelAndDispose(ref _baseAnalysisCts);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task BaseAnalysis(CharacterData charaData, CancellationToken token)
|
private async Task BaseAnalysis(CharacterData charaData, CancellationToken token)
|
||||||
@@ -142,9 +164,11 @@ public sealed class CharacterAnalyzer : DisposableMediatorSubscriberBase
|
|||||||
LastAnalysis[obj.Key] = data;
|
LastAnalysis[obj.Key] = data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_lastDataHash = charaData.DataHash.Value;
|
||||||
|
RefreshSummary(true, _lastDataHash);
|
||||||
|
|
||||||
Mediator.Publish(new CharacterDataAnalyzedMessage());
|
Mediator.Publish(new CharacterDataAnalyzedMessage());
|
||||||
|
|
||||||
_lastDataHash = charaData.DataHash.Value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void PrintAnalysis()
|
private void PrintAnalysis()
|
||||||
@@ -193,6 +217,169 @@ public sealed class CharacterAnalyzer : DisposableMediatorSubscriberBase
|
|||||||
Logger.LogInformation("IMPORTANT NOTES:\n\r- For uploads and downloads only the compressed size is relevant.\n\r- An unusually high total files count beyond 200 and up will also increase your download time to others significantly.");
|
Logger.LogInformation("IMPORTANT NOTES:\n\r- For uploads and downloads only the compressed size is relevant.\n\r- An unusually high total files count beyond 200 and up will also increase your download time to others significantly.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void RefreshSummary(bool evaluateAutoAnalysis, string dataHash)
|
||||||
|
{
|
||||||
|
var summary = CalculateSummary();
|
||||||
|
CurrentSummary = summary;
|
||||||
|
|
||||||
|
if (evaluateAutoAnalysis)
|
||||||
|
{
|
||||||
|
EvaluateAutoAnalysis(summary, dataHash);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_previousSummary = summary;
|
||||||
|
|
||||||
|
if (!summary.HasUncomputedEntries && string.Equals(_lastAutoAnalysisHash, dataHash, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
_lastAutoAnalysisHash = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EvaluateThresholdNotifications(summary);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CharacterAnalysisSummary CalculateSummary()
|
||||||
|
{
|
||||||
|
if (LastAnalysis.Count == 0)
|
||||||
|
{
|
||||||
|
return CharacterAnalysisSummary.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
long original = 0;
|
||||||
|
long compressed = 0;
|
||||||
|
long triangles = 0;
|
||||||
|
int files = 0;
|
||||||
|
bool hasUncomputed = false;
|
||||||
|
|
||||||
|
foreach (var obj in LastAnalysis.Values)
|
||||||
|
{
|
||||||
|
foreach (var entry in obj.Values)
|
||||||
|
{
|
||||||
|
files++;
|
||||||
|
original += entry.OriginalSize;
|
||||||
|
compressed += entry.CompressedSize;
|
||||||
|
triangles += entry.Triangles;
|
||||||
|
hasUncomputed |= !entry.IsComputed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CharacterAnalysisSummary(files, original, compressed, triangles, hasUncomputed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EvaluateAutoAnalysis(CharacterAnalysisSummary newSummary, string dataHash)
|
||||||
|
{
|
||||||
|
var previous = _previousSummary;
|
||||||
|
_previousSummary = newSummary;
|
||||||
|
|
||||||
|
if (newSummary.TotalFiles == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(_lastAutoAnalysisHash, dataHash, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsAnalysisRunning)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
if (now - _lastAutoAnalysis < AutoAnalysisCooldown)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool firstSummary = previous.TotalFiles == 0;
|
||||||
|
bool filesIncreased = newSummary.TotalFiles - previous.TotalFiles >= AutoAnalysisFileDeltaThreshold;
|
||||||
|
bool sizeIncreased = newSummary.TotalCompressedSize - previous.TotalCompressedSize >= AutoAnalysisSizeDeltaThreshold;
|
||||||
|
bool needsCompute = newSummary.HasUncomputedEntries;
|
||||||
|
|
||||||
|
if (!firstSummary && !filesIncreased && !sizeIncreased && !needsCompute)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_lastAutoAnalysis = now;
|
||||||
|
_lastAutoAnalysisHash = dataHash;
|
||||||
|
_ = ComputeAnalysis(print: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EvaluateThresholdNotifications(CharacterAnalysisSummary summary)
|
||||||
|
{
|
||||||
|
if (summary.IsEmpty || summary.HasUncomputedEntries)
|
||||||
|
{
|
||||||
|
ResetThresholdFlagsIfNeeded(summary);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_playerPerformanceConfigService.Current.ShowSelfAnalysisWarnings)
|
||||||
|
{
|
||||||
|
ResetThresholdFlagsIfNeeded(summary);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool sizeExceeded = summary.TotalCompressedSize >= NotificationSizeThreshold;
|
||||||
|
bool trianglesExceeded = summary.TotalTriangles >= NotificationTriangleThreshold;
|
||||||
|
List<string> exceededReasons = new();
|
||||||
|
|
||||||
|
if (sizeExceeded && !_sizeWarningShown)
|
||||||
|
{
|
||||||
|
exceededReasons.Add($"un poids partagé de {UiSharedService.ByteToString(summary.TotalCompressedSize)} (≥ 300 MiB)");
|
||||||
|
_sizeWarningShown = true;
|
||||||
|
}
|
||||||
|
else if (!sizeExceeded && _sizeWarningShown)
|
||||||
|
{
|
||||||
|
_sizeWarningShown = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trianglesExceeded && !_triangleWarningShown)
|
||||||
|
{
|
||||||
|
exceededReasons.Add($"un total de {UiSharedService.TrisToString(summary.TotalTriangles)} triangles (≥ 150k)");
|
||||||
|
_triangleWarningShown = true;
|
||||||
|
}
|
||||||
|
else if (!trianglesExceeded && _triangleWarningShown)
|
||||||
|
{
|
||||||
|
_triangleWarningShown = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exceededReasons.Count == 0) return;
|
||||||
|
|
||||||
|
string combined = string.Join(" et ", exceededReasons);
|
||||||
|
string message = $"Attention : votre self-analysis indique {combined}. Des joueurs risquent de ne pas vous voir et UmbraSync peut activer un auto-pause. Pensez à réduire textures ou modèles lourds.";
|
||||||
|
Mediator.Publish(new DualNotificationMessage("Self Analysis", message, NotificationType.Warning));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ResetThresholdFlagsIfNeeded(CharacterAnalysisSummary summary)
|
||||||
|
{
|
||||||
|
if (summary.IsEmpty)
|
||||||
|
{
|
||||||
|
_sizeWarningShown = false;
|
||||||
|
_triangleWarningShown = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (summary.TotalCompressedSize < NotificationSizeThreshold)
|
||||||
|
{
|
||||||
|
_sizeWarningShown = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (summary.TotalTriangles < NotificationTriangleThreshold)
|
||||||
|
{
|
||||||
|
_triangleWarningShown = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Auto)]
|
||||||
|
public readonly record struct CharacterAnalysisSummary(int TotalFiles, long TotalOriginalSize, long TotalCompressedSize, long TotalTriangles, bool HasUncomputedEntries)
|
||||||
|
{
|
||||||
|
public static CharacterAnalysisSummary Empty => new();
|
||||||
|
public bool IsEmpty => TotalFiles == 0 && TotalOriginalSize == 0 && TotalCompressedSize == 0 && TotalTriangles == 0;
|
||||||
|
}
|
||||||
|
|
||||||
internal sealed record FileDataEntry(string Hash, string FileType, List<string> GamePaths, List<string> FilePaths, long OriginalSize, long CompressedSize, long Triangles)
|
internal sealed record FileDataEntry(string Hash, string FileType, List<string> GamePaths, List<string> FilePaths, long OriginalSize, long CompressedSize, long Triangles)
|
||||||
{
|
{
|
||||||
public bool IsComputed => OriginalSize > 0 && CompressedSize > 0;
|
public bool IsComputed => OriginalSize > 0 && CompressedSize > 0;
|
||||||
@@ -239,4 +426,26 @@ public sealed class CharacterAnalyzer : DisposableMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static CancellationTokenSource EnsureFreshCts(ref CancellationTokenSource? cts)
|
||||||
|
{
|
||||||
|
CancelAndDispose(ref cts);
|
||||||
|
cts = new CancellationTokenSource();
|
||||||
|
return cts;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CancelAndDispose(ref CancellationTokenSource? cts)
|
||||||
|
{
|
||||||
|
if (cts == null) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
cts.Cancel();
|
||||||
|
}
|
||||||
|
catch (ObjectDisposedException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
cts.Dispose();
|
||||||
|
cts = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
|
using System;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
using Dalamud.Game.Text;
|
using Dalamud.Game.Text;
|
||||||
using Dalamud.Game.Text.SeStringHandling;
|
using Dalamud.Game.Text.SeStringHandling;
|
||||||
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
||||||
@@ -18,6 +21,7 @@ namespace MareSynchronos.Services;
|
|||||||
public class ChatService : DisposableMediatorSubscriberBase
|
public class ChatService : DisposableMediatorSubscriberBase
|
||||||
{
|
{
|
||||||
public const int DefaultColor = 710;
|
public const int DefaultColor = 710;
|
||||||
|
private const string ManualPairInvitePrefix = "[UmbraPairInvite|";
|
||||||
public const int CommandMaxNumber = 50;
|
public const int CommandMaxNumber = 50;
|
||||||
|
|
||||||
private readonly ILogger<ChatService> _logger;
|
private readonly ILogger<ChatService> _logger;
|
||||||
@@ -30,6 +34,13 @@ public class ChatService : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
private readonly Lazy<GameChatHooks> _gameChatHooks;
|
private readonly Lazy<GameChatHooks> _gameChatHooks;
|
||||||
|
|
||||||
|
private readonly object _typingLock = new();
|
||||||
|
private CancellationTokenSource? _typingCts;
|
||||||
|
private bool _isTypingAnnounced;
|
||||||
|
private DateTime _lastTypingSent = DateTime.MinValue;
|
||||||
|
private static readonly TimeSpan TypingIdle = TimeSpan.FromSeconds(2);
|
||||||
|
private static readonly TimeSpan TypingResendInterval = TimeSpan.FromMilliseconds(750);
|
||||||
|
|
||||||
public ChatService(ILogger<ChatService> logger, DalamudUtilService dalamudUtil, MareMediator mediator, ApiController apiController,
|
public ChatService(ILogger<ChatService> logger, DalamudUtilService dalamudUtil, MareMediator mediator, ApiController apiController,
|
||||||
PairManager pairManager, ILoggerFactory loggerFactory, IGameInteropProvider gameInteropProvider, IChatGui chatGui,
|
PairManager pairManager, ILoggerFactory loggerFactory, IGameInteropProvider gameInteropProvider, IChatGui chatGui,
|
||||||
MareConfigService mareConfig, ServerConfigurationManager serverConfigurationManager) : base(logger, mediator)
|
MareConfigService mareConfig, ServerConfigurationManager serverConfigurationManager) : base(logger, mediator)
|
||||||
@@ -46,13 +57,12 @@ public class ChatService : DisposableMediatorSubscriberBase
|
|||||||
Mediator.Subscribe<GroupChatMsgMessage>(this, HandleGroupChat);
|
Mediator.Subscribe<GroupChatMsgMessage>(this, HandleGroupChat);
|
||||||
|
|
||||||
_gameChatHooks = new(() => new GameChatHooks(loggerFactory.CreateLogger<GameChatHooks>(), gameInteropProvider, SendChatShell));
|
_gameChatHooks = new(() => new GameChatHooks(loggerFactory.CreateLogger<GameChatHooks>(), gameInteropProvider, SendChatShell));
|
||||||
|
|
||||||
// Initialize chat hooks in advance
|
|
||||||
_ = Task.Run(() =>
|
_ = Task.Run(() =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_ = _gameChatHooks.Value;
|
_ = _gameChatHooks.Value;
|
||||||
|
_isTypingAnnounced = false;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -64,9 +74,80 @@ public class ChatService : DisposableMediatorSubscriberBase
|
|||||||
protected override void Dispose(bool disposing)
|
protected override void Dispose(bool disposing)
|
||||||
{
|
{
|
||||||
base.Dispose(disposing);
|
base.Dispose(disposing);
|
||||||
|
_typingCts?.Cancel();
|
||||||
|
_typingCts?.Dispose();
|
||||||
if (_gameChatHooks.IsValueCreated)
|
if (_gameChatHooks.IsValueCreated)
|
||||||
_gameChatHooks.Value!.Dispose();
|
_gameChatHooks.Value!.Dispose();
|
||||||
}
|
}
|
||||||
|
public void NotifyTypingKeystroke()
|
||||||
|
{
|
||||||
|
lock (_typingLock)
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
if (!_isTypingAnnounced || (now - _lastTypingSent) >= TypingResendInterval)
|
||||||
|
{
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try { await _apiController.UserSetTypingState(true).ConfigureAwait(false); }
|
||||||
|
catch (Exception ex) { _logger.LogDebug(ex, "NotifyTypingKeystroke: failed to send typing=true"); }
|
||||||
|
});
|
||||||
|
_isTypingAnnounced = true;
|
||||||
|
_lastTypingSent = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
_typingCts?.Cancel();
|
||||||
|
_typingCts?.Dispose();
|
||||||
|
_typingCts = new CancellationTokenSource();
|
||||||
|
var token = _typingCts.Token;
|
||||||
|
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.Delay(TypingIdle, token).ConfigureAwait(false);
|
||||||
|
await _apiController.UserSetTypingState(false).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException)
|
||||||
|
{
|
||||||
|
// reset timer
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogDebug(ex, "NotifyTypingKeystroke: failed to send typing=false");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
lock (_typingLock)
|
||||||
|
{
|
||||||
|
if (!token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
_isTypingAnnounced = false;
|
||||||
|
_lastTypingSent = DateTime.MinValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public void ClearTypingState()
|
||||||
|
{
|
||||||
|
lock (_typingLock)
|
||||||
|
{
|
||||||
|
_typingCts?.Cancel();
|
||||||
|
_typingCts?.Dispose();
|
||||||
|
_typingCts = null;
|
||||||
|
if (_isTypingAnnounced)
|
||||||
|
{
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try { await _apiController.UserSetTypingState(false).ConfigureAwait(false); }
|
||||||
|
catch (Exception ex) { _logger.LogDebug(ex, "ClearTypingState: failed to send typing=false"); }
|
||||||
|
});
|
||||||
|
_isTypingAnnounced = false;
|
||||||
|
_lastTypingSent = DateTime.MinValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void HandleUserChat(UserChatMsgMessage message)
|
private void HandleUserChat(UserChatMsgMessage message)
|
||||||
{
|
{
|
||||||
@@ -113,6 +194,10 @@ public class ChatService : DisposableMediatorSubscriberBase
|
|||||||
var extraChatTags = _mareConfig.Current.ExtraChatTags;
|
var extraChatTags = _mareConfig.Current.ExtraChatTags;
|
||||||
var logKind = ResolveShellLogKind(shellConfig.LogKind);
|
var logKind = ResolveShellLogKind(shellConfig.LogKind);
|
||||||
|
|
||||||
|
var payload = SeString.Parse(message.ChatMsg.PayloadContent);
|
||||||
|
if (TryHandleManualPairInvite(message, payload))
|
||||||
|
return;
|
||||||
|
|
||||||
var msg = new SeStringBuilder();
|
var msg = new SeStringBuilder();
|
||||||
if (extraChatTags)
|
if (extraChatTags)
|
||||||
{
|
{
|
||||||
@@ -124,7 +209,6 @@ public class ChatService : DisposableMediatorSubscriberBase
|
|||||||
msg.AddText($"[SS{shellNumber}]<");
|
msg.AddText($"[SS{shellNumber}]<");
|
||||||
if (message.ChatMsg.Sender.UID.Equals(_apiController.UID, StringComparison.Ordinal))
|
if (message.ChatMsg.Sender.UID.Equals(_apiController.UID, StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
// Don't link to your own character
|
|
||||||
msg.AddText(chatMsg.SenderName);
|
msg.AddText(chatMsg.SenderName);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -132,7 +216,7 @@ public class ChatService : DisposableMediatorSubscriberBase
|
|||||||
msg.Add(new PlayerPayload(chatMsg.SenderName, chatMsg.SenderHomeWorldId));
|
msg.Add(new PlayerPayload(chatMsg.SenderName, chatMsg.SenderHomeWorldId));
|
||||||
}
|
}
|
||||||
msg.AddText("> ");
|
msg.AddText("> ");
|
||||||
msg.Append(SeString.Parse(message.ChatMsg.PayloadContent));
|
msg.Append(payload);
|
||||||
if (color != 0)
|
if (color != 0)
|
||||||
msg.AddUiForegroundOff();
|
msg.AddUiForegroundOff();
|
||||||
|
|
||||||
@@ -143,7 +227,51 @@ public class ChatService : DisposableMediatorSubscriberBase
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Print an example message to the configured global chat channel
|
private bool TryHandleManualPairInvite(GroupChatMsgMessage message, SeString payload)
|
||||||
|
{
|
||||||
|
var textValue = payload.TextValue;
|
||||||
|
if (string.IsNullOrEmpty(textValue) || !textValue.StartsWith(ManualPairInvitePrefix, StringComparison.Ordinal))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var content = textValue[ManualPairInvitePrefix.Length..];
|
||||||
|
if (content.EndsWith("]", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
content = content[..^1];
|
||||||
|
}
|
||||||
|
|
||||||
|
var parts = content.Split('|');
|
||||||
|
if (parts.Length < 4)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
var sourceUid = parts[0];
|
||||||
|
var sourceAlias = DecodeInviteField(parts[1]);
|
||||||
|
var targetUid = parts[2];
|
||||||
|
var displayName = DecodeInviteField(parts[3]);
|
||||||
|
var inviteId = parts.Length > 4 ? parts[4] : Guid.NewGuid().ToString("N");
|
||||||
|
|
||||||
|
if (!string.Equals(targetUid, _apiController.UID, StringComparison.Ordinal))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
Mediator.Publish(new ManualPairInviteMessage(sourceUid, sourceAlias, targetUid, string.IsNullOrEmpty(displayName) ? null : displayName, inviteId));
|
||||||
|
_logger.LogDebug("Received manual pair invite from {source} via syncshell", sourceUid);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string DecodeInviteField(string encoded)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(encoded)) return string.Empty;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var bytes = Convert.FromBase64String(encoded);
|
||||||
|
return Encoding.UTF8.GetString(bytes);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return encoded;
|
||||||
|
}
|
||||||
|
}
|
||||||
public void PrintChannelExample(string message, string gid = "")
|
public void PrintChannelExample(string message, string gid = "")
|
||||||
{
|
{
|
||||||
int chatType = _mareConfig.Current.ChatLogKind;
|
int chatType = _mareConfig.Current.ChatLogKind;
|
||||||
@@ -164,8 +292,6 @@ public class ChatService : DisposableMediatorSubscriberBase
|
|||||||
Type = (XivChatType)chatType
|
Type = (XivChatType)chatType
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Called to update the active chat shell name if its renamed
|
|
||||||
public void MaybeUpdateShellName(int shellNumber)
|
public void MaybeUpdateShellName(int shellNumber)
|
||||||
{
|
{
|
||||||
if (_mareConfig.Current.DisableSyncshellChat)
|
if (_mareConfig.Current.DisableSyncshellChat)
|
||||||
@@ -178,7 +304,6 @@ public class ChatService : DisposableMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
if (_gameChatHooks.IsValueCreated && _gameChatHooks.Value.ChatChannelOverride != null)
|
if (_gameChatHooks.IsValueCreated && _gameChatHooks.Value.ChatChannelOverride != null)
|
||||||
{
|
{
|
||||||
// Very dumb and won't handle re-numbering -- need to identify the active chat channel more reliably later
|
|
||||||
if (_gameChatHooks.Value.ChatChannelOverride.ChannelName.StartsWith($"SS [{shellNumber}]", StringComparison.Ordinal))
|
if (_gameChatHooks.Value.ChatChannelOverride.ChannelName.StartsWith($"SS [{shellNumber}]", StringComparison.Ordinal))
|
||||||
SwitchChatShell(shellNumber);
|
SwitchChatShell(shellNumber);
|
||||||
}
|
}
|
||||||
@@ -197,7 +322,6 @@ public class ChatService : DisposableMediatorSubscriberBase
|
|||||||
if (shellConfig.Enabled && shellConfig.ShellNumber == shellNumber)
|
if (shellConfig.Enabled && shellConfig.ShellNumber == shellNumber)
|
||||||
{
|
{
|
||||||
var name = _serverConfigurationManager.GetNoteForGid(group.Key.GID) ?? group.Key.AliasOrGID;
|
var name = _serverConfigurationManager.GetNoteForGid(group.Key.GID) ?? group.Key.AliasOrGID;
|
||||||
// BUG: This doesn't always update the chat window e.g. when renaming a group
|
|
||||||
_gameChatHooks.Value.ChatChannelOverride = new()
|
_gameChatHooks.Value.ChatChannelOverride = new()
|
||||||
{
|
{
|
||||||
ChannelName = $"SS [{shellNumber}]: {name}",
|
ChannelName = $"SS [{shellNumber}]: {name}",
|
||||||
@@ -221,7 +345,6 @@ public class ChatService : DisposableMediatorSubscriberBase
|
|||||||
if (shellConfig.Enabled && shellConfig.ShellNumber == shellNumber)
|
if (shellConfig.Enabled && shellConfig.ShellNumber == shellNumber)
|
||||||
{
|
{
|
||||||
_ = Task.Run(async () => {
|
_ = Task.Run(async () => {
|
||||||
// Should cache the name and home world instead of fetching it every time
|
|
||||||
var chatMsg = await _dalamudUtil.RunOnFrameworkThread(() => {
|
var chatMsg = await _dalamudUtil.RunOnFrameworkThread(() => {
|
||||||
return new ChatMessage()
|
return new ChatMessage()
|
||||||
{
|
{
|
||||||
@@ -230,6 +353,7 @@ public class ChatService : DisposableMediatorSubscriberBase
|
|||||||
PayloadContent = chatBytes
|
PayloadContent = chatBytes
|
||||||
};
|
};
|
||||||
}).ConfigureAwait(false);
|
}).ConfigureAwait(false);
|
||||||
|
ClearTypingState();
|
||||||
await _apiController.GroupChatSendMsg(new(group.Key), chatMsg).ConfigureAwait(false);
|
await _apiController.GroupChatSendMsg(new(group.Key), chatMsg).ConfigureAwait(false);
|
||||||
}).ConfigureAwait(false);
|
}).ConfigureAwait(false);
|
||||||
return;
|
return;
|
||||||
|
|||||||
68
MareSynchronos/Services/ChatTwoCompatibilityService.cs
Normal file
68
MareSynchronos/Services/ChatTwoCompatibilityService.cs
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Dalamud.Plugin;
|
||||||
|
using MareSynchronos.MareConfiguration.Models;
|
||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace MareSynchronos.Services;
|
||||||
|
|
||||||
|
public sealed class ChatTwoCompatibilityService : MediatorSubscriberBase, IHostedService
|
||||||
|
{
|
||||||
|
private const string ChatTwoInternalName = "ChatTwo";
|
||||||
|
private readonly IDalamudPluginInterface _pluginInterface;
|
||||||
|
private bool _warningShown;
|
||||||
|
|
||||||
|
public ChatTwoCompatibilityService(ILogger<ChatTwoCompatibilityService> logger, IDalamudPluginInterface pluginInterface, MareMediator mediator)
|
||||||
|
: base(logger, mediator)
|
||||||
|
{
|
||||||
|
_pluginInterface = pluginInterface;
|
||||||
|
|
||||||
|
Mediator.SubscribeKeyed<PluginChangeMessage>(this, ChatTwoInternalName, OnChatTwoStateChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var initialState = PluginWatcherService.GetInitialPluginState(_pluginInterface, ChatTwoInternalName);
|
||||||
|
if (initialState?.IsLoaded == true)
|
||||||
|
{
|
||||||
|
ShowWarning();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogDebug(ex, "Failed to inspect ChatTwo initial state");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StopAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
Mediator.UnsubscribeAll(this);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnChatTwoStateChanged(PluginChangeMessage message)
|
||||||
|
{
|
||||||
|
if (message.IsLoaded)
|
||||||
|
{
|
||||||
|
ShowWarning();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShowWarning()
|
||||||
|
{
|
||||||
|
if (_warningShown) return;
|
||||||
|
_warningShown = true;
|
||||||
|
|
||||||
|
const string warningTitle = "ChatTwo détecté";
|
||||||
|
const string warningBody = "Actuellement, le plugin ChatTwo n'est pas compatible avec la bulle d'écriture d'UmbraSync. Désactivez ChatTwo si vous souhaitez conserver l'indicateur de saisie.";
|
||||||
|
|
||||||
|
Mediator.Publish(new NotificationMessage(warningTitle, warningBody, NotificationType.Warning, TimeSpan.FromSeconds(10)));
|
||||||
|
}
|
||||||
|
}
|
||||||
294
MareSynchronos/Services/ChatTypingDetectionService.cs
Normal file
294
MareSynchronos/Services/ChatTypingDetectionService.cs
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using Dalamud.Utility;
|
||||||
|
using Dalamud.Game.Text;
|
||||||
|
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.UI.Shell;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using MareSynchronos.PlayerData.Pairs;
|
||||||
|
using MareSynchronos.WebAPI;
|
||||||
|
|
||||||
|
namespace MareSynchronos.Services;
|
||||||
|
|
||||||
|
public sealed class ChatTypingDetectionService : IDisposable
|
||||||
|
{
|
||||||
|
private readonly ILogger<ChatTypingDetectionService> _logger;
|
||||||
|
private readonly IFramework _framework;
|
||||||
|
private readonly IClientState _clientState;
|
||||||
|
private readonly IGameGui _gameGui;
|
||||||
|
private readonly ChatService _chatService;
|
||||||
|
private readonly TypingIndicatorStateService _typingStateService;
|
||||||
|
private readonly ApiController _apiController;
|
||||||
|
private readonly PairManager _pairManager;
|
||||||
|
private readonly IPartyList _partyList;
|
||||||
|
|
||||||
|
private string _lastChatText = string.Empty;
|
||||||
|
private bool _isTyping;
|
||||||
|
private bool _notifyingRemote;
|
||||||
|
private bool _serverSupportWarnLogged;
|
||||||
|
private bool _remoteNotificationsEnabled;
|
||||||
|
|
||||||
|
public ChatTypingDetectionService(ILogger<ChatTypingDetectionService> logger, IFramework framework,
|
||||||
|
IClientState clientState, IGameGui gameGui, ChatService chatService, PairManager pairManager, IPartyList partyList,
|
||||||
|
TypingIndicatorStateService typingStateService, ApiController apiController)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_framework = framework;
|
||||||
|
_clientState = clientState;
|
||||||
|
_gameGui = gameGui;
|
||||||
|
_chatService = chatService;
|
||||||
|
_pairManager = pairManager;
|
||||||
|
_partyList = partyList;
|
||||||
|
_typingStateService = typingStateService;
|
||||||
|
_apiController = apiController;
|
||||||
|
|
||||||
|
_framework.Update += OnFrameworkUpdate;
|
||||||
|
_logger.LogInformation("ChatTypingDetectionService initialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_framework.Update -= OnFrameworkUpdate;
|
||||||
|
ResetTypingState();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnFrameworkUpdate(IFramework framework)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!_clientState.IsLoggedIn)
|
||||||
|
{
|
||||||
|
ResetTypingState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryGetChatInput(out var chatText) || string.IsNullOrEmpty(chatText))
|
||||||
|
{
|
||||||
|
ResetTypingState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsIgnoredCommand(chatText))
|
||||||
|
{
|
||||||
|
ResetTypingState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var notifyRemote = ShouldNotifyRemote();
|
||||||
|
UpdateRemoteNotificationLogState(notifyRemote);
|
||||||
|
if (!notifyRemote && _notifyingRemote)
|
||||||
|
{
|
||||||
|
_chatService.ClearTypingState();
|
||||||
|
_notifyingRemote = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_isTyping || !string.Equals(chatText, _lastChatText, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
if (notifyRemote)
|
||||||
|
{
|
||||||
|
_chatService.NotifyTypingKeystroke();
|
||||||
|
_notifyingRemote = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_typingStateService.SetSelfTypingLocal(true);
|
||||||
|
_isTyping = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_lastChatText = chatText;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogTrace(ex, "ChatTypingDetectionService tick failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ResetTypingState()
|
||||||
|
{
|
||||||
|
if (!_isTyping)
|
||||||
|
{
|
||||||
|
_lastChatText = string.Empty;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_isTyping = false;
|
||||||
|
_lastChatText = string.Empty;
|
||||||
|
_chatService.ClearTypingState();
|
||||||
|
_notifyingRemote = false;
|
||||||
|
_typingStateService.SetSelfTypingLocal(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsIgnoredCommand(string chatText)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(chatText))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var trimmed = chatText.TrimStart();
|
||||||
|
if (!trimmed.StartsWith('/'))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var firstTokenEnd = trimmed.IndexOf(' ');
|
||||||
|
var command = firstTokenEnd >= 0 ? trimmed[..firstTokenEnd] : trimmed;
|
||||||
|
command = command.TrimEnd();
|
||||||
|
|
||||||
|
var comparison = StringComparison.OrdinalIgnoreCase;
|
||||||
|
return command.StartsWith("/tell", comparison)
|
||||||
|
|| command.StartsWith("/t", comparison)
|
||||||
|
|| command.StartsWith("/xllog", comparison)
|
||||||
|
|| command.StartsWith("/umbra", comparison)
|
||||||
|
|| command.StartsWith("/fc", comparison)
|
||||||
|
|| command.StartsWith("/freecompany", comparison);
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe bool ShouldNotifyRemote()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var supportsTypingState = _apiController.SystemInfoDto.SupportsTypingState;
|
||||||
|
var connected = _apiController.IsConnected;
|
||||||
|
if (!connected || !supportsTypingState)
|
||||||
|
{
|
||||||
|
if (!_serverSupportWarnLogged)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("TypingDetection: server support unavailable (connected={connected}, supports={supports})", connected, supportsTypingState);
|
||||||
|
_serverSupportWarnLogged = true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_serverSupportWarnLogged = false;
|
||||||
|
|
||||||
|
var shellModule = RaptureShellModule.Instance();
|
||||||
|
if (shellModule == null)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("TypingDetection: shell module null");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var chatType = (XivChatType)shellModule->ChatType;
|
||||||
|
switch (chatType)
|
||||||
|
{
|
||||||
|
case XivChatType.Say:
|
||||||
|
case XivChatType.Shout:
|
||||||
|
case XivChatType.Yell:
|
||||||
|
return true;
|
||||||
|
case XivChatType.Party:
|
||||||
|
case XivChatType.CrossParty:
|
||||||
|
var eligible = PartyContainsPairedMember();
|
||||||
|
return eligible;
|
||||||
|
case XivChatType.Debug:
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
_logger.LogTrace("TypingDetection: channel {type} rejected", chatType);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogTrace(ex, "ChatTypingDetectionService: failed to evaluate chat channel");
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool PartyContainsPairedMember()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var pairedNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var pair in _pairManager.GetOnlineUserPairs())
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(pair.PlayerName))
|
||||||
|
pairedNames.Add(pair.PlayerName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pairedNames.Count == 0)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("TypingDetection: no paired names online");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var member in _partyList)
|
||||||
|
{
|
||||||
|
var name = member?.Name?.TextValue;
|
||||||
|
if (string.IsNullOrEmpty(name))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (pairedNames.Contains(name))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogDebug(ex, "ChatTypingDetectionService: failed to check party composition");
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug("TypingDetection: no paired members in party");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private unsafe bool TryGetChatInput(out string chatText)
|
||||||
|
{
|
||||||
|
chatText = string.Empty;
|
||||||
|
|
||||||
|
var addon = _gameGui.GetAddonByName("ChatLog", 1);
|
||||||
|
if (addon.Address == nint.Zero)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var chatLog = (AtkUnitBase*)addon.Address;
|
||||||
|
if (chatLog == null || !chatLog->IsVisible)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var textInputNode = chatLog->UldManager.NodeList[16];
|
||||||
|
if (textInputNode == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var componentNode = textInputNode->GetAsAtkComponentNode();
|
||||||
|
if (componentNode == null || componentNode->Component == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var cursorNode = componentNode->Component->UldManager.NodeList[14];
|
||||||
|
if (cursorNode == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var cursorVisible = cursorNode->IsVisible();
|
||||||
|
if (!cursorVisible)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var chatInputNode = componentNode->Component->UldManager.NodeList[1];
|
||||||
|
if (chatInputNode == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var textNode = chatInputNode->GetAsAtkTextNode();
|
||||||
|
if (textNode == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var rawText = textNode->GetText();
|
||||||
|
if (rawText == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
chatText = rawText.AsDalamudSeString().ToString();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateRemoteNotificationLogState(bool notifyRemote)
|
||||||
|
{
|
||||||
|
if (notifyRemote && !_remoteNotificationsEnabled)
|
||||||
|
{
|
||||||
|
_remoteNotificationsEnabled = true;
|
||||||
|
_logger.LogInformation("TypingDetection: remote notifications enabled");
|
||||||
|
}
|
||||||
|
else if (!notifyRemote && _remoteNotificationsEnabled)
|
||||||
|
{
|
||||||
|
_remoteNotificationsEnabled = false;
|
||||||
|
_logger.LogInformation("TypingDetection: remote notifications disabled");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,12 +9,14 @@ using MareSynchronos.UI;
|
|||||||
using MareSynchronos.WebAPI;
|
using MareSynchronos.WebAPI;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Numerics;
|
||||||
|
|
||||||
namespace MareSynchronos.Services;
|
namespace MareSynchronos.Services;
|
||||||
|
|
||||||
public sealed class CommandManagerService : IDisposable
|
public sealed class CommandManagerService : IDisposable
|
||||||
{
|
{
|
||||||
private const string _commandName = "/usync";
|
private const string _commandName = "/usync";
|
||||||
|
private const string _autoDetectCommand = "/autodetect";
|
||||||
private const string _ssCommandPrefix = "/ums";
|
private const string _ssCommandPrefix = "/ums";
|
||||||
|
|
||||||
private readonly ApiController _apiController;
|
private readonly ApiController _apiController;
|
||||||
@@ -43,6 +45,11 @@ public sealed class CommandManagerService : IDisposable
|
|||||||
HelpMessage = "Opens the UmbraSync UI"
|
HelpMessage = "Opens the UmbraSync UI"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
_commandManager.AddHandler(_autoDetectCommand, new CommandInfo(OnAutoDetectCommand)
|
||||||
|
{
|
||||||
|
HelpMessage = "Opens the AutoDetect window"
|
||||||
|
});
|
||||||
|
|
||||||
// Lazy registration of all possible /ss# commands which tbf is what the game does for linkshells anyway
|
// Lazy registration of all possible /ss# commands which tbf is what the game does for linkshells anyway
|
||||||
for (int i = 1; i <= ChatService.CommandMaxNumber; ++i)
|
for (int i = 1; i <= ChatService.CommandMaxNumber; ++i)
|
||||||
{
|
{
|
||||||
@@ -56,12 +63,21 @@ public sealed class CommandManagerService : IDisposable
|
|||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_commandManager.RemoveHandler(_commandName);
|
_commandManager.RemoveHandler(_commandName);
|
||||||
|
_commandManager.RemoveHandler(_autoDetectCommand);
|
||||||
|
|
||||||
for (int i = 1; i <= ChatService.CommandMaxNumber; ++i)
|
for (int i = 1; i <= ChatService.CommandMaxNumber; ++i)
|
||||||
_commandManager.RemoveHandler($"{_ssCommandPrefix}{i}");
|
_commandManager.RemoveHandler($"{_ssCommandPrefix}{i}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnAutoDetectCommand(string command, string args)
|
||||||
|
{
|
||||||
|
UiSharedService.AccentColor = new Vector4(0x8D / 255f, 0x37 / 255f, 0xC0 / 255f, 1f);
|
||||||
|
UiSharedService.AccentHoverColor = new Vector4(0x3A / 255f, 0x15 / 255f, 0x50 / 255f, 1f);
|
||||||
|
UiSharedService.AccentActiveColor = UiSharedService.AccentHoverColor;
|
||||||
|
_mediator.Publish(new UiToggleMessage(typeof(AutoDetectUi)));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private void OnCommand(string command, string args)
|
private void OnCommand(string command, string args)
|
||||||
{
|
{
|
||||||
var splitArgs = args.ToLowerInvariant().Trim().Split(" ", StringSplitOptions.RemoveEmptyEntries);
|
var splitArgs = args.ToLowerInvariant().Trim().Split(" ", StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ using Microsoft.Extensions.Logging;
|
|||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject;
|
using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject;
|
||||||
using DalamudGameObject = Dalamud.Game.ClientState.Objects.Types.IGameObject;
|
using DalamudGameObject = Dalamud.Game.ClientState.Objects.Types.IGameObject;
|
||||||
|
|
||||||
@@ -52,6 +54,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
private readonly ILogger<DalamudUtilService> _logger;
|
private readonly ILogger<DalamudUtilService> _logger;
|
||||||
private readonly IObjectTable _objectTable;
|
private readonly IObjectTable _objectTable;
|
||||||
private readonly PerformanceCollectorService _performanceCollector;
|
private readonly PerformanceCollectorService _performanceCollector;
|
||||||
|
private readonly Dictionary<string, ConditionFlag> _conditionLookup = new(StringComparer.OrdinalIgnoreCase);
|
||||||
private uint? _classJobId = 0;
|
private uint? _classJobId = 0;
|
||||||
private DateTime _delayedFrameworkUpdateCheck = DateTime.UtcNow;
|
private DateTime _delayedFrameworkUpdateCheck = DateTime.UtcNow;
|
||||||
private string _lastGlobalBlockPlayer = string.Empty;
|
private string _lastGlobalBlockPlayer = string.Empty;
|
||||||
@@ -172,6 +175,20 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
public bool IsInCombatOrPerforming { get; private set; } = false;
|
public bool IsInCombatOrPerforming { get; private set; } = false;
|
||||||
public bool HasModifiedGameFiles => _gameData.HasModifiedGameDataFiles;
|
public bool HasModifiedGameFiles => _gameData.HasModifiedGameDataFiles;
|
||||||
|
|
||||||
|
public bool IsConditionActive(string flagName)
|
||||||
|
{
|
||||||
|
if (_conditionLookup.TryGetValue(flagName, out var cachedFlag))
|
||||||
|
return _condition[cachedFlag];
|
||||||
|
|
||||||
|
if (Enum.TryParse<ConditionFlag>(flagName, true, out var flag))
|
||||||
|
{
|
||||||
|
_conditionLookup[flagName] = flag;
|
||||||
|
return _condition[flag];
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
public Lazy<Dictionary<ushort, string>> WorldData { get; private set; }
|
public Lazy<Dictionary<ushort, string>> WorldData { get; private set; }
|
||||||
public Lazy<Dictionary<int, Lumina.Excel.Sheets.UIColor>> UiColors { get; private set; }
|
public Lazy<Dictionary<int, Lumina.Excel.Sheets.UIColor>> UiColors { get; private set; }
|
||||||
public Lazy<Dictionary<uint, string>> TerritoryData { get; private set; }
|
public Lazy<Dictionary<uint, string>> TerritoryData { get; private set; }
|
||||||
|
|||||||
@@ -1,18 +1,21 @@
|
|||||||
|
using System;
|
||||||
using Dalamud.Game.Gui.NamePlate;
|
using Dalamud.Game.Gui.NamePlate;
|
||||||
using Dalamud.Game.Text.SeStringHandling;
|
using Dalamud.Game.Text.SeStringHandling;
|
||||||
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
|
using MareSynchronos.API.Dto.User;
|
||||||
using MareSynchronos.MareConfiguration;
|
using MareSynchronos.MareConfiguration;
|
||||||
using MareSynchronos.PlayerData.Pairs;
|
using MareSynchronos.PlayerData.Pairs;
|
||||||
using MareSynchronos.Services.Mediator;
|
using MareSynchronos.Services.Mediator;
|
||||||
using MareSynchronos.UI;
|
using MareSynchronos.UI;
|
||||||
|
using MareSynchronos.WebAPI;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Dalamud.Game.Text;
|
||||||
|
|
||||||
namespace MareSynchronos.Services;
|
namespace MareSynchronos.Services;
|
||||||
|
|
||||||
public class GuiHookService : DisposableMediatorSubscriberBase
|
public class GuiHookService : DisposableMediatorSubscriberBase
|
||||||
{
|
{
|
||||||
private readonly ILogger<GuiHookService> _logger;
|
|
||||||
private readonly DalamudUtilService _dalamudUtil;
|
private readonly DalamudUtilService _dalamudUtil;
|
||||||
private readonly MareConfigService _configService;
|
private readonly MareConfigService _configService;
|
||||||
private readonly INamePlateGui _namePlateGui;
|
private readonly INamePlateGui _namePlateGui;
|
||||||
@@ -27,7 +30,6 @@ public class GuiHookService : DisposableMediatorSubscriberBase
|
|||||||
INamePlateGui namePlateGui, IGameConfig gameConfig, IPartyList partyList, PairManager pairManager)
|
INamePlateGui namePlateGui, IGameConfig gameConfig, IPartyList partyList, PairManager pairManager)
|
||||||
: base(logger, mediator)
|
: base(logger, mediator)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
|
||||||
_dalamudUtil = dalamudUtil;
|
_dalamudUtil = dalamudUtil;
|
||||||
_configService = configService;
|
_configService = configService;
|
||||||
_namePlateGui = namePlateGui;
|
_namePlateGui = namePlateGui;
|
||||||
@@ -41,11 +43,14 @@ public class GuiHookService : DisposableMediatorSubscriberBase
|
|||||||
Mediator.Subscribe<DelayedFrameworkUpdateMessage>(this, (_) => GameSettingsCheck());
|
Mediator.Subscribe<DelayedFrameworkUpdateMessage>(this, (_) => GameSettingsCheck());
|
||||||
Mediator.Subscribe<PairHandlerVisibleMessage>(this, (_) => RequestRedraw());
|
Mediator.Subscribe<PairHandlerVisibleMessage>(this, (_) => RequestRedraw());
|
||||||
Mediator.Subscribe<NameplateRedrawMessage>(this, (_) => RequestRedraw());
|
Mediator.Subscribe<NameplateRedrawMessage>(this, (_) => RequestRedraw());
|
||||||
|
Mediator.Subscribe<UserTypingStateMessage>(this, (_) => RequestRedraw());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RequestRedraw(bool force = false)
|
public void RequestRedraw(bool force = false)
|
||||||
{
|
{
|
||||||
if (!_configService.Current.UseNameColors)
|
var useColors = _configService.Current.UseNameColors;
|
||||||
|
|
||||||
|
if (!useColors)
|
||||||
{
|
{
|
||||||
if (!_isModified && !force)
|
if (!_isModified && !force)
|
||||||
return;
|
return;
|
||||||
@@ -69,7 +74,8 @@ public class GuiHookService : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
private void OnNamePlateUpdate(INamePlateUpdateContext context, IReadOnlyList<INamePlateUpdateHandler> handlers)
|
private void OnNamePlateUpdate(INamePlateUpdateContext context, IReadOnlyList<INamePlateUpdateHandler> handlers)
|
||||||
{
|
{
|
||||||
if (!_configService.Current.UseNameColors)
|
var applyColors = _configService.Current.UseNameColors;
|
||||||
|
if (!applyColors)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var visibleUsers = _pairManager.GetOnlineUserPairs().Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue);
|
var visibleUsers = _pairManager.GetOnlineUserPairs().Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue);
|
||||||
@@ -89,6 +95,8 @@ public class GuiHookService : DisposableMediatorSubscriberBase
|
|||||||
if (_namePlateRoleColorsEnabled && partyMembers.Contains(handler.GameObject?.Address ?? nint.MaxValue))
|
if (_namePlateRoleColorsEnabled && partyMembers.Contains(handler.GameObject?.Address ?? nint.MaxValue))
|
||||||
continue;
|
continue;
|
||||||
var pair = visibleUsersDict[handler.GameObjectId];
|
var pair = visibleUsersDict[handler.GameObjectId];
|
||||||
|
if (applyColors)
|
||||||
|
{
|
||||||
var colors = !pair.IsApplicationBlocked ? _configService.Current.NameColors : _configService.Current.BlockedNameColors;
|
var colors = !pair.IsApplicationBlocked ? _configService.Current.NameColors : _configService.Current.BlockedNameColors;
|
||||||
handler.NameParts.TextWrap = (
|
handler.NameParts.TextWrap = (
|
||||||
BuildColorStartSeString(colors),
|
BuildColorStartSeString(colors),
|
||||||
@@ -96,6 +104,9 @@ public class GuiHookService : DisposableMediatorSubscriberBase
|
|||||||
);
|
);
|
||||||
_isModified = true;
|
_isModified = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
using MareSynchronos.API.Data.Comparer;
|
using MareSynchronos.API.Data.Comparer;
|
||||||
using MareSynchronos.MareConfiguration;
|
using MareSynchronos.MareConfiguration;
|
||||||
using MareSynchronos.Services.Mediator;
|
using MareSynchronos.Services.Mediator;
|
||||||
using MareSynchronos.Services.ServerConfiguration;
|
|
||||||
using MareSynchronos.WebAPI;
|
using MareSynchronos.WebAPI;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
@@ -15,7 +14,6 @@ public class MareProfileManager : MediatorSubscriberBase
|
|||||||
private const string _nsfw = "Profile not displayed - NSFW";
|
private const string _nsfw = "Profile not displayed - NSFW";
|
||||||
private readonly ApiController _apiController;
|
private readonly ApiController _apiController;
|
||||||
private readonly MareConfigService _mareConfigService;
|
private readonly MareConfigService _mareConfigService;
|
||||||
private readonly ServerConfigurationManager _serverConfigurationManager;
|
|
||||||
private readonly ConcurrentDictionary<UserData, MareProfileData> _mareProfiles = new(UserDataComparer.Instance);
|
private readonly ConcurrentDictionary<UserData, MareProfileData> _mareProfiles = new(UserDataComparer.Instance);
|
||||||
|
|
||||||
private readonly MareProfileData _defaultProfileData = new(IsFlagged: false, IsNSFW: false, string.Empty, _noDescription);
|
private readonly MareProfileData _defaultProfileData = new(IsFlagged: false, IsNSFW: false, string.Empty, _noDescription);
|
||||||
@@ -23,11 +21,10 @@ public class MareProfileManager : MediatorSubscriberBase
|
|||||||
private readonly MareProfileData _nsfwProfileData = new(IsFlagged: false, IsNSFW: false, string.Empty, _nsfw);
|
private readonly MareProfileData _nsfwProfileData = new(IsFlagged: false, IsNSFW: false, string.Empty, _nsfw);
|
||||||
|
|
||||||
public MareProfileManager(ILogger<MareProfileManager> logger, MareConfigService mareConfigService,
|
public MareProfileManager(ILogger<MareProfileManager> logger, MareConfigService mareConfigService,
|
||||||
MareMediator mediator, ApiController apiController, ServerConfigurationManager serverConfigurationManager) : base(logger, mediator)
|
MareMediator mediator, ApiController apiController) : base(logger, mediator)
|
||||||
{
|
{
|
||||||
_mareConfigService = mareConfigService;
|
_mareConfigService = mareConfigService;
|
||||||
_apiController = apiController;
|
_apiController = apiController;
|
||||||
_serverConfigurationManager = serverConfigurationManager;
|
|
||||||
|
|
||||||
Mediator.Subscribe<ClearProfileDataMessage>(this, (msg) =>
|
Mediator.Subscribe<ClearProfileDataMessage>(this, (msg) =>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ using System.Text;
|
|||||||
|
|
||||||
namespace MareSynchronos.Services.Mediator;
|
namespace MareSynchronos.Services.Mediator;
|
||||||
|
|
||||||
public sealed class MareMediator : IHostedService
|
public sealed class MareMediator : IHostedService, IDisposable
|
||||||
{
|
{
|
||||||
private readonly Lock _addRemoveLock = new();
|
private readonly Lock _addRemoveLock = new();
|
||||||
private readonly ConcurrentDictionary<SubscriberAction, DateTime> _lastErrorTime = [];
|
private readonly ConcurrentDictionary<SubscriberAction, DateTime> _lastErrorTime = [];
|
||||||
@@ -109,6 +109,26 @@ public sealed class MareMediator : IHostedService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed) return;
|
||||||
|
_disposed = true;
|
||||||
|
if (!_loopCts.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_loopCts.Cancel();
|
||||||
|
}
|
||||||
|
catch (ObjectDisposedException)
|
||||||
|
{
|
||||||
|
// already disposed, swallow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_loopCts.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
public void SubscribeKeyed<T>(IMediatorSubscriber subscriber, string key, Action<T> action) where T : MessageBase
|
public void SubscribeKeyed<T>(IMediatorSubscriber subscriber, string key, Action<T> action) where T : MessageBase
|
||||||
{
|
{
|
||||||
lock (_addRemoveLock)
|
lock (_addRemoveLock)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using MareSynchronos.API.Data;
|
|||||||
using MareSynchronos.API.Dto;
|
using MareSynchronos.API.Dto;
|
||||||
using MareSynchronos.API.Dto.CharaData;
|
using MareSynchronos.API.Dto.CharaData;
|
||||||
using MareSynchronos.API.Dto.Group;
|
using MareSynchronos.API.Dto.Group;
|
||||||
|
using MareSynchronos.API.Dto.User;
|
||||||
using MareSynchronos.MareConfiguration.Models;
|
using MareSynchronos.MareConfiguration.Models;
|
||||||
using MareSynchronos.PlayerData.Handlers;
|
using MareSynchronos.PlayerData.Handlers;
|
||||||
using MareSynchronos.PlayerData.Pairs;
|
using MareSynchronos.PlayerData.Pairs;
|
||||||
@@ -52,6 +53,7 @@ public record HaltScanMessage(string Source) : MessageBase;
|
|||||||
public record ResumeScanMessage(string Source) : MessageBase;
|
public record ResumeScanMessage(string Source) : MessageBase;
|
||||||
public record NotificationMessage
|
public record NotificationMessage
|
||||||
(string Title, string Message, NotificationType Type, TimeSpan? TimeShownOnScreen = null) : MessageBase;
|
(string Title, string Message, NotificationType Type, TimeSpan? TimeShownOnScreen = null) : MessageBase;
|
||||||
|
public record DualNotificationMessage(string Title, string Message, NotificationType Type, TimeSpan? ToastDuration = null) : MessageBase;
|
||||||
public record CreateCacheForObjectMessage(GameObjectHandler ObjectToCreateFor) : MessageBase;
|
public record CreateCacheForObjectMessage(GameObjectHandler ObjectToCreateFor) : MessageBase;
|
||||||
public record ClearCacheForObjectMessage(GameObjectHandler ObjectToCreateFor) : MessageBase;
|
public record ClearCacheForObjectMessage(GameObjectHandler ObjectToCreateFor) : MessageBase;
|
||||||
public record CharacterDataCreatedMessage(CharacterData CharacterData) : SameThreadMessage;
|
public record CharacterDataCreatedMessage(CharacterData CharacterData) : SameThreadMessage;
|
||||||
@@ -90,6 +92,7 @@ public record PenumbraDirectoryChangedMessage(string? ModDirectory) : MessageBas
|
|||||||
public record PenumbraRedrawCharacterMessage(ICharacter Character) : SameThreadMessage;
|
public record PenumbraRedrawCharacterMessage(ICharacter Character) : SameThreadMessage;
|
||||||
public record UserChatMsgMessage(SignedChatMessage ChatMsg) : MessageBase;
|
public record UserChatMsgMessage(SignedChatMessage ChatMsg) : MessageBase;
|
||||||
public record GroupChatMsgMessage(GroupDto GroupInfo, SignedChatMessage ChatMsg) : MessageBase;
|
public record GroupChatMsgMessage(GroupDto GroupInfo, SignedChatMessage ChatMsg) : MessageBase;
|
||||||
|
public record UserTypingStateMessage(TypingStateDto Typing) : MessageBase;
|
||||||
public record RecalculatePerformanceMessage(string? UID) : MessageBase;
|
public record RecalculatePerformanceMessage(string? UID) : MessageBase;
|
||||||
public record NameplateRedrawMessage : MessageBase;
|
public record NameplateRedrawMessage : MessageBase;
|
||||||
public record HoldPairApplicationMessage(string UID, string Source) : KeyedMessage(UID);
|
public record HoldPairApplicationMessage(string UID, string Source) : KeyedMessage(UID);
|
||||||
@@ -112,6 +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 ApplyDefaultPairPermissionsMessage(UserPairDto Pair) : MessageBase;
|
||||||
|
public record ApplyDefaultGroupPermissionsMessage(GroupPairFullInfoDto GroupPair) : 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 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
|
||||||
|
|||||||
73
MareSynchronos/Services/Notification/NotificationTracker.cs
Normal file
73
MareSynchronos/Services/Notification/NotificationTracker.cs
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using Dalamud.Game.Text.SeStringHandling;
|
using System;
|
||||||
|
using Dalamud.Game.Text.SeStringHandling;
|
||||||
using Dalamud.Interface.ImGuiNotification;
|
using Dalamud.Interface.ImGuiNotification;
|
||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
using MareSynchronos.MareConfiguration;
|
using MareSynchronos.MareConfiguration;
|
||||||
@@ -31,6 +32,7 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
|
|||||||
public Task StartAsync(CancellationToken cancellationToken)
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
Mediator.Subscribe<NotificationMessage>(this, ShowNotification);
|
Mediator.Subscribe<NotificationMessage>(this, ShowNotification);
|
||||||
|
Mediator.Subscribe<DualNotificationMessage>(this, ShowDualNotification);
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,41 +83,103 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
|
|||||||
|
|
||||||
if (!_dalamudUtilService.IsLoggedIn) return;
|
if (!_dalamudUtilService.IsLoggedIn) return;
|
||||||
|
|
||||||
switch (msg.Type)
|
bool appendInstruction;
|
||||||
|
bool forceChat = ShouldForceChat(msg, out appendInstruction);
|
||||||
|
var effectiveMessage = forceChat && appendInstruction ? AppendAutoDetectInstruction(msg.Message) : msg.Message;
|
||||||
|
var adjustedMsg = forceChat && appendInstruction ? msg with { Message = effectiveMessage } : msg;
|
||||||
|
|
||||||
|
switch (adjustedMsg.Type)
|
||||||
{
|
{
|
||||||
case NotificationType.Info:
|
case NotificationType.Info:
|
||||||
ShowNotificationLocationBased(msg, _configurationService.Current.InfoNotification);
|
ShowNotificationLocationBased(adjustedMsg, _configurationService.Current.InfoNotification, forceChat);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case NotificationType.Warning:
|
case NotificationType.Warning:
|
||||||
ShowNotificationLocationBased(msg, _configurationService.Current.WarningNotification);
|
ShowNotificationLocationBased(adjustedMsg, _configurationService.Current.WarningNotification, forceChat);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case NotificationType.Error:
|
case NotificationType.Error:
|
||||||
ShowNotificationLocationBased(msg, _configurationService.Current.ErrorNotification);
|
ShowNotificationLocationBased(adjustedMsg, _configurationService.Current.ErrorNotification, forceChat);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ShowNotificationLocationBased(NotificationMessage msg, NotificationLocation location)
|
private void ShowDualNotification(DualNotificationMessage message)
|
||||||
{
|
{
|
||||||
switch (location)
|
if (!_dalamudUtilService.IsLoggedIn) return;
|
||||||
|
|
||||||
|
var baseMsg = new NotificationMessage(message.Title, message.Message, message.Type, message.ToastDuration);
|
||||||
|
ShowToast(baseMsg);
|
||||||
|
ShowChat(baseMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ShouldForceChat(NotificationMessage msg, out bool appendInstruction)
|
||||||
|
{
|
||||||
|
appendInstruction = false;
|
||||||
|
|
||||||
|
bool IsNearbyRequestText(string? text)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(text)) return false;
|
||||||
|
return text.Contains("Nearby request", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| text.Contains("Nearby Request", StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsNearbyAcceptText(string? text)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(text)) return false;
|
||||||
|
return text.Contains("Nearby Accept", StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isAccept = IsNearbyAcceptText(msg.Title) || IsNearbyAcceptText(msg.Message);
|
||||||
|
if (isAccept)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
bool isRequest = IsNearbyRequestText(msg.Title) || IsNearbyRequestText(msg.Message);
|
||||||
|
if (isRequest)
|
||||||
|
{
|
||||||
|
appendInstruction = !IsRequestSentConfirmation(msg);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsRequestSentConfirmation(NotificationMessage msg)
|
||||||
|
{
|
||||||
|
if (string.Equals(msg.Title, "Nearby request sent", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(msg.Message) && msg.Message.Contains("The other user will receive a request notification.", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string AppendAutoDetectInstruction(string? message)
|
||||||
|
{
|
||||||
|
const string suffix = " | Ouvrez /autodetect pour gérer l'invitation.";
|
||||||
|
if (string.IsNullOrWhiteSpace(message))
|
||||||
|
return suffix.TrimStart(' ', '|');
|
||||||
|
|
||||||
|
if (message.Contains("/autodetect", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return message;
|
||||||
|
|
||||||
|
return message.TrimEnd() + suffix;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShowNotificationLocationBased(NotificationMessage msg, NotificationLocation location, bool forceChat)
|
||||||
|
{
|
||||||
|
bool showToast = location is NotificationLocation.Toast or NotificationLocation.Both;
|
||||||
|
bool showChat = forceChat || location is NotificationLocation.Chat or NotificationLocation.Both;
|
||||||
|
|
||||||
|
if (showToast)
|
||||||
{
|
{
|
||||||
case NotificationLocation.Toast:
|
|
||||||
ShowToast(msg);
|
ShowToast(msg);
|
||||||
break;
|
}
|
||||||
|
|
||||||
case NotificationLocation.Chat:
|
if (showChat)
|
||||||
|
{
|
||||||
ShowChat(msg);
|
ShowChat(msg);
|
||||||
break;
|
|
||||||
|
|
||||||
case NotificationLocation.Both:
|
|
||||||
ShowToast(msg);
|
|
||||||
ShowChat(msg);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case NotificationLocation.Nowhere:
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using MareSynchronos.API.Data;
|
using System;
|
||||||
|
using MareSynchronos.API.Data;
|
||||||
using MareSynchronos.API.Data.Enum;
|
using MareSynchronos.API.Data.Enum;
|
||||||
using MareSynchronos.FileCache;
|
using MareSynchronos.FileCache;
|
||||||
using MareSynchronos.PlayerData.Pairs;
|
using MareSynchronos.PlayerData.Pairs;
|
||||||
@@ -14,7 +15,7 @@ public sealed class PairAnalyzer : DisposableMediatorSubscriberBase
|
|||||||
private readonly FileCacheManager _fileCacheManager;
|
private readonly FileCacheManager _fileCacheManager;
|
||||||
private readonly XivDataAnalyzer _xivDataAnalyzer;
|
private readonly XivDataAnalyzer _xivDataAnalyzer;
|
||||||
private CancellationTokenSource? _analysisCts;
|
private CancellationTokenSource? _analysisCts;
|
||||||
private CancellationTokenSource _baseAnalysisCts = new();
|
private CancellationTokenSource? _baseAnalysisCts = new();
|
||||||
private string _lastDataHash = string.Empty;
|
private string _lastDataHash = string.Empty;
|
||||||
|
|
||||||
public PairAnalyzer(ILogger<PairAnalyzer> logger, Pair pair, MareMediator mediator, FileCacheManager fileCacheManager, XivDataAnalyzer modelAnalyzer)
|
public PairAnalyzer(ILogger<PairAnalyzer> logger, Pair pair, MareMediator mediator, FileCacheManager fileCacheManager, XivDataAnalyzer modelAnalyzer)
|
||||||
@@ -24,8 +25,8 @@ public sealed class PairAnalyzer : DisposableMediatorSubscriberBase
|
|||||||
#if DEBUG
|
#if DEBUG
|
||||||
Mediator.SubscribeKeyed<PairDataAppliedMessage>(this, pair.UserData.UID, (msg) =>
|
Mediator.SubscribeKeyed<PairDataAppliedMessage>(this, pair.UserData.UID, (msg) =>
|
||||||
{
|
{
|
||||||
_baseAnalysisCts = _baseAnalysisCts.CancelRecreate();
|
var tokenSource = EnsureFreshCts(ref _baseAnalysisCts);
|
||||||
var token = _baseAnalysisCts.Token;
|
var token = tokenSource.Token;
|
||||||
if (msg.CharacterData != null)
|
if (msg.CharacterData != null)
|
||||||
{
|
{
|
||||||
_ = BaseAnalysis(msg.CharacterData, token);
|
_ = BaseAnalysis(msg.CharacterData, token);
|
||||||
@@ -56,17 +57,15 @@ public sealed class PairAnalyzer : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
public void CancelAnalyze()
|
public void CancelAnalyze()
|
||||||
{
|
{
|
||||||
_analysisCts?.CancelDispose();
|
CancelAndDispose(ref _analysisCts);
|
||||||
_analysisCts = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ComputeAnalysis(bool print = true, bool recalculate = false)
|
public async Task ComputeAnalysis(bool print = true, bool recalculate = false)
|
||||||
{
|
{
|
||||||
Logger.LogDebug("=== Calculating Character Analysis ===");
|
Logger.LogDebug("=== Calculating Character Analysis ===");
|
||||||
|
|
||||||
_analysisCts = _analysisCts?.CancelRecreate() ?? new();
|
var analysisCts = EnsureFreshCts(ref _analysisCts);
|
||||||
|
var cancelToken = analysisCts.Token;
|
||||||
var cancelToken = _analysisCts.Token;
|
|
||||||
|
|
||||||
var allFiles = LastAnalysis.SelectMany(v => v.Value.Select(d => d.Value)).ToList();
|
var allFiles = LastAnalysis.SelectMany(v => v.Value.Select(d => d.Value)).ToList();
|
||||||
if (allFiles.Exists(c => !c.IsComputed || recalculate))
|
if (allFiles.Exists(c => !c.IsComputed || recalculate))
|
||||||
@@ -102,8 +101,7 @@ public sealed class PairAnalyzer : DisposableMediatorSubscriberBase
|
|||||||
LastPlayerName = Pair.PlayerName ?? string.Empty;
|
LastPlayerName = Pair.PlayerName ?? string.Empty;
|
||||||
Mediator.Publish(new PairDataAnalyzedMessage(Pair.UserData.UID));
|
Mediator.Publish(new PairDataAnalyzedMessage(Pair.UserData.UID));
|
||||||
|
|
||||||
_analysisCts.CancelDispose();
|
CancelAndDispose(ref _analysisCts);
|
||||||
_analysisCts = null;
|
|
||||||
|
|
||||||
if (print) PrintAnalysis();
|
if (print) PrintAnalysis();
|
||||||
}
|
}
|
||||||
@@ -114,8 +112,8 @@ public sealed class PairAnalyzer : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
if (!disposing) return;
|
if (!disposing) return;
|
||||||
|
|
||||||
_analysisCts?.CancelDispose();
|
CancelAndDispose(ref _analysisCts);
|
||||||
_baseAnalysisCts.CancelDispose();
|
CancelAndDispose(ref _baseAnalysisCts);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task BaseAnalysis(CharacterData charaData, CancellationToken token)
|
private async Task BaseAnalysis(CharacterData charaData, CancellationToken token)
|
||||||
@@ -211,4 +209,26 @@ public sealed class PairAnalyzer : DisposableMediatorSubscriberBase
|
|||||||
UiSharedService.ByteToString(LastAnalysis.Values.Sum(c => c.Values.Sum(v => v.OriginalSize))),
|
UiSharedService.ByteToString(LastAnalysis.Values.Sum(c => c.Values.Sum(v => v.OriginalSize))),
|
||||||
UiSharedService.ByteToString(LastAnalysis.Values.Sum(c => c.Values.Sum(v => v.CompressedSize))));
|
UiSharedService.ByteToString(LastAnalysis.Values.Sum(c => c.Values.Sum(v => v.CompressedSize))));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static CancellationTokenSource EnsureFreshCts(ref CancellationTokenSource? cts)
|
||||||
|
{
|
||||||
|
CancelAndDispose(ref cts);
|
||||||
|
cts = new CancellationTokenSource();
|
||||||
|
return cts;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CancelAndDispose(ref CancellationTokenSource? cts)
|
||||||
|
{
|
||||||
|
if (cts == null) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
cts.Cancel();
|
||||||
|
}
|
||||||
|
catch (ObjectDisposedException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
cts.Dispose();
|
||||||
|
cts = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
77
MareSynchronos/Services/PartyListTypingService.cs
Normal file
77
MareSynchronos/Services/PartyListTypingService.cs
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
using MareSynchronos.MareConfiguration;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using MareSynchronos.PlayerData.Pairs;
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using Dalamud.Game.Text;
|
||||||
|
using Dalamud.Game.Text.SeStringHandling;
|
||||||
|
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using MareSynchronos.API.Dto.User;
|
||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace MareSynchronos.Services;
|
||||||
|
|
||||||
|
public class PartyListTypingService : DisposableMediatorSubscriberBase
|
||||||
|
{
|
||||||
|
private readonly ILogger<PartyListTypingService> _logger;
|
||||||
|
private readonly IPartyList _partyList;
|
||||||
|
private readonly MareConfigService _configService;
|
||||||
|
private readonly PairManager _pairManager;
|
||||||
|
private readonly TypingIndicatorStateService _typingStateService;
|
||||||
|
private static readonly TimeSpan TypingDisplayTime = TimeSpan.FromSeconds(2);
|
||||||
|
private static readonly TimeSpan TypingDisplayFade = TypingDisplayTime;
|
||||||
|
|
||||||
|
public PartyListTypingService(ILogger<PartyListTypingService> logger,
|
||||||
|
MareMediator mediator,
|
||||||
|
IPartyList partyList,
|
||||||
|
PairManager pairManager,
|
||||||
|
MareConfigService configService,
|
||||||
|
TypingIndicatorStateService typingStateService)
|
||||||
|
: base(logger, mediator)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_partyList = partyList;
|
||||||
|
_pairManager = pairManager;
|
||||||
|
_configService = configService;
|
||||||
|
_typingStateService = typingStateService;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Draw()
|
||||||
|
{
|
||||||
|
if (!_configService.Current.TypingIndicatorShowOnPartyList) return;
|
||||||
|
// Build map of visible users by AliasOrUID -> UID (case-insensitive)
|
||||||
|
var visibleByAlias = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var visibleUsers = _pairManager.GetVisibleUsers();
|
||||||
|
foreach (var u in visibleUsers)
|
||||||
|
{
|
||||||
|
var alias = string.IsNullOrEmpty(u.AliasOrUID) ? u.UID : u.AliasOrUID;
|
||||||
|
if (!visibleByAlias.ContainsKey(alias)) visibleByAlias[alias] = u.UID;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogDebug(ex, "PartyListTypingService: failed to get visible users");
|
||||||
|
}
|
||||||
|
|
||||||
|
var activeTypers = _typingStateService.GetActiveTypers(TypingDisplayTime);
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
|
||||||
|
foreach (var member in _partyList)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(member.Name?.TextValue)) continue;
|
||||||
|
|
||||||
|
var displayName = member.Name.TextValue;
|
||||||
|
if (visibleByAlias.TryGetValue(displayName, out var uid)
|
||||||
|
&& activeTypers.TryGetValue(uid, out var entry)
|
||||||
|
&& (now - entry.LastUpdate) <= TypingDisplayFade)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("PartyListTypingService: bubble would be shown for {name}", displayName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,7 +23,6 @@ public class PlayerPerformanceService : DisposableMediatorSubscriberBase
|
|||||||
private readonly MareMediator _mediator;
|
private readonly MareMediator _mediator;
|
||||||
private readonly ServerConfigurationManager _serverConfigurationManager;
|
private readonly ServerConfigurationManager _serverConfigurationManager;
|
||||||
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
|
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
|
||||||
private readonly Dictionary<string, bool> _warnedForPlayers = new(StringComparer.Ordinal);
|
|
||||||
|
|
||||||
public PlayerPerformanceService(ILogger<PlayerPerformanceService> logger, MareMediator mediator,
|
public PlayerPerformanceService(ILogger<PlayerPerformanceService> logger, MareMediator mediator,
|
||||||
ServerConfigurationManager serverConfigurationManager,
|
ServerConfigurationManager serverConfigurationManager,
|
||||||
|
|||||||
358
MareSynchronos/Services/SyncDefaultsService.cs
Normal file
358
MareSynchronos/Services/SyncDefaultsService.cs
Normal file
@@ -0,0 +1,358 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using MareSynchronos.API.Data;
|
||||||
|
using MareSynchronos.API.Data.Enum;
|
||||||
|
using MareSynchronos.API.Data.Extensions;
|
||||||
|
using MareSynchronos.API.Dto.Group;
|
||||||
|
using MareSynchronos.API.Dto.User;
|
||||||
|
using MareSynchronos.MareConfiguration;
|
||||||
|
using MareSynchronos.MareConfiguration.Configurations;
|
||||||
|
using MareSynchronos.MareConfiguration.Models;
|
||||||
|
using MareSynchronos.PlayerData.Pairs;
|
||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using MareSynchronos.WebAPI;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using NotificationType = MareSynchronos.MareConfiguration.Models.NotificationType;
|
||||||
|
|
||||||
|
namespace MareSynchronos.Services;
|
||||||
|
|
||||||
|
public sealed class SyncDefaultsService : DisposableMediatorSubscriberBase
|
||||||
|
{
|
||||||
|
private readonly ApiController _apiController;
|
||||||
|
private readonly MareConfigService _configService;
|
||||||
|
private readonly PairManager _pairManager;
|
||||||
|
|
||||||
|
public SyncDefaultsService(ILogger<SyncDefaultsService> logger, MareMediator mediator,
|
||||||
|
MareConfigService configService, ApiController apiController, PairManager pairManager) : base(logger, mediator)
|
||||||
|
{
|
||||||
|
_configService = configService;
|
||||||
|
_apiController = apiController;
|
||||||
|
_pairManager = pairManager;
|
||||||
|
|
||||||
|
Mediator.Subscribe<ApplyDefaultPairPermissionsMessage>(this, OnApplyPairDefaults);
|
||||||
|
Mediator.Subscribe<ApplyDefaultGroupPermissionsMessage>(this, OnApplyGroupDefaults);
|
||||||
|
Mediator.Subscribe<ApplyDefaultsToAllSyncsMessage>(this, msg => ApplyDefaultsToAll(msg));
|
||||||
|
Mediator.Subscribe<PairSyncOverrideChanged>(this, OnPairOverrideChanged);
|
||||||
|
Mediator.Subscribe<GroupSyncOverrideChanged>(this, OnGroupOverrideChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnApplyPairDefaults(ApplyDefaultPairPermissionsMessage message)
|
||||||
|
{
|
||||||
|
var config = _configService.Current;
|
||||||
|
var permissions = message.Pair.OwnPermissions;
|
||||||
|
var overrides = TryGetPairOverride(message.Pair.User.UID);
|
||||||
|
if (!ApplyDefaults(ref permissions, config, overrides))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_ = _apiController.UserSetPairPermissions(new UserPermissionsDto(message.Pair.User, permissions));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnApplyGroupDefaults(ApplyDefaultGroupPermissionsMessage message)
|
||||||
|
{
|
||||||
|
if (!string.Equals(message.GroupPair.User.UID, _apiController.UID, StringComparison.Ordinal))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var config = _configService.Current;
|
||||||
|
var permissions = message.GroupPair.GroupUserPermissions;
|
||||||
|
var overrides = TryGetGroupOverride(message.GroupPair.Group.GID);
|
||||||
|
if (!ApplyDefaults(ref permissions, config, overrides))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_ = _apiController.GroupChangeIndividualPermissionState(new GroupPairUserPermissionDto(message.GroupPair.Group, message.GroupPair.User, permissions));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ApplyDefaultsToAllAsync(ApplyDefaultsToAllSyncsMessage message)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var config = _configService.Current;
|
||||||
|
var tasks = new List<Task>();
|
||||||
|
int updatedPairs = 0;
|
||||||
|
int updatedGroups = 0;
|
||||||
|
|
||||||
|
foreach (var pair in _pairManager.DirectPairs.Where(p => p.UserPair != null).ToList())
|
||||||
|
{
|
||||||
|
var permissions = pair.UserPair!.OwnPermissions;
|
||||||
|
var overrides = TryGetPairOverride(pair.UserData.UID);
|
||||||
|
if (!ApplyDefaults(ref permissions, config, overrides))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
updatedPairs++;
|
||||||
|
tasks.Add(_apiController.UserSetPairPermissions(new UserPermissionsDto(pair.UserData, permissions)));
|
||||||
|
}
|
||||||
|
|
||||||
|
var selfUser = new UserData(_apiController.UID);
|
||||||
|
foreach (var groupInfo in _pairManager.Groups.Values.ToList())
|
||||||
|
{
|
||||||
|
var permissions = groupInfo.GroupUserPermissions;
|
||||||
|
var overrides = TryGetGroupOverride(groupInfo.Group.GID);
|
||||||
|
if (!ApplyDefaults(ref permissions, config, overrides))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
updatedGroups++;
|
||||||
|
tasks.Add(_apiController.GroupChangeIndividualPermissionState(new GroupPairUserPermissionDto(groupInfo.Group, selfUser, permissions)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tasks.Count > 0)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(ex, "Failed applying default sync settings to all pairs/groups");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var summary = BuildSummaryMessage(updatedPairs, updatedGroups);
|
||||||
|
var primary = BuildPrimaryMessage(message);
|
||||||
|
var combined = string.IsNullOrEmpty(primary) ? summary : string.Concat(primary, ' ', summary);
|
||||||
|
Mediator.Publish(new DualNotificationMessage("Préférences appliquées", combined, NotificationType.Info));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex, "Unexpected error while applying default sync settings to all pairs/groups");
|
||||||
|
Mediator.Publish(new DualNotificationMessage("Préférences appliquées", "Une erreur est survenue lors de l'application des paramètres par défaut.", NotificationType.Error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyDefaultsToAll(ApplyDefaultsToAllSyncsMessage message) => _ = ApplyDefaultsToAllAsync(message);
|
||||||
|
|
||||||
|
private static string? BuildPrimaryMessage(ApplyDefaultsToAllSyncsMessage message)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(message.Context) || message.Disabled == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var state = message.Disabled.Value ? "désactivée" : "activée";
|
||||||
|
return $"Synchronisation {message.Context} par défaut {state}.";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildSummaryMessage(int pairs, int groups)
|
||||||
|
{
|
||||||
|
if (pairs == 0 && groups == 0)
|
||||||
|
return "Aucun pair ou syncshell n'avait besoin d'être modifié.";
|
||||||
|
|
||||||
|
if (pairs > 0 && groups > 0)
|
||||||
|
return $"Mise à jour de {pairs} pair(s) et {groups} syncshell(s).";
|
||||||
|
|
||||||
|
if (pairs > 0)
|
||||||
|
return $"Mise à jour de {pairs} pair(s).";
|
||||||
|
|
||||||
|
return $"Mise à jour de {groups} syncshell(s).";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPairOverrideChanged(PairSyncOverrideChanged message)
|
||||||
|
{
|
||||||
|
var overrides = _configService.Current.PairSyncOverrides ??= new(StringComparer.Ordinal);
|
||||||
|
var entry = overrides.TryGetValue(message.Uid, out var existing) ? existing : new SyncOverrideEntry();
|
||||||
|
bool changed = false;
|
||||||
|
|
||||||
|
if (message.DisableSounds.HasValue)
|
||||||
|
{
|
||||||
|
var val = message.DisableSounds.Value;
|
||||||
|
var defaultVal = _configService.Current.DefaultDisableSounds;
|
||||||
|
var newValue = val == defaultVal ? (bool?)null : val;
|
||||||
|
if (entry.DisableSounds != newValue)
|
||||||
|
{
|
||||||
|
entry.DisableSounds = newValue;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.DisableAnimations.HasValue)
|
||||||
|
{
|
||||||
|
var val = message.DisableAnimations.Value;
|
||||||
|
var defaultVal = _configService.Current.DefaultDisableAnimations;
|
||||||
|
var newValue = val == defaultVal ? (bool?)null : val;
|
||||||
|
if (entry.DisableAnimations != newValue)
|
||||||
|
{
|
||||||
|
entry.DisableAnimations = newValue;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.DisableVfx.HasValue)
|
||||||
|
{
|
||||||
|
var val = message.DisableVfx.Value;
|
||||||
|
var defaultVal = _configService.Current.DefaultDisableVfx;
|
||||||
|
var newValue = val == defaultVal ? (bool?)null : val;
|
||||||
|
if (entry.DisableVfx != newValue)
|
||||||
|
{
|
||||||
|
entry.DisableVfx = newValue;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!changed) return;
|
||||||
|
|
||||||
|
if (entry.IsEmpty)
|
||||||
|
overrides.Remove(message.Uid);
|
||||||
|
else
|
||||||
|
overrides[message.Uid] = entry;
|
||||||
|
|
||||||
|
_configService.Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnGroupOverrideChanged(GroupSyncOverrideChanged message)
|
||||||
|
{
|
||||||
|
var overrides = _configService.Current.GroupSyncOverrides ??= new(StringComparer.Ordinal);
|
||||||
|
var entry = overrides.TryGetValue(message.Gid, out var existing) ? existing : new SyncOverrideEntry();
|
||||||
|
bool changed = false;
|
||||||
|
|
||||||
|
if (message.DisableSounds.HasValue)
|
||||||
|
{
|
||||||
|
var val = message.DisableSounds.Value;
|
||||||
|
var defaultVal = _configService.Current.DefaultDisableSounds;
|
||||||
|
var newValue = val == defaultVal ? (bool?)null : val;
|
||||||
|
if (entry.DisableSounds != newValue)
|
||||||
|
{
|
||||||
|
entry.DisableSounds = newValue;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.DisableAnimations.HasValue)
|
||||||
|
{
|
||||||
|
var val = message.DisableAnimations.Value;
|
||||||
|
var defaultVal = _configService.Current.DefaultDisableAnimations;
|
||||||
|
var newValue = val == defaultVal ? (bool?)null : val;
|
||||||
|
if (entry.DisableAnimations != newValue)
|
||||||
|
{
|
||||||
|
entry.DisableAnimations = newValue;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.DisableVfx.HasValue)
|
||||||
|
{
|
||||||
|
var val = message.DisableVfx.Value;
|
||||||
|
var defaultVal = _configService.Current.DefaultDisableVfx;
|
||||||
|
var newValue = val == defaultVal ? (bool?)null : val;
|
||||||
|
if (entry.DisableVfx != newValue)
|
||||||
|
{
|
||||||
|
entry.DisableVfx = newValue;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!changed) return;
|
||||||
|
|
||||||
|
if (entry.IsEmpty)
|
||||||
|
overrides.Remove(message.Gid);
|
||||||
|
else
|
||||||
|
overrides[message.Gid] = entry;
|
||||||
|
|
||||||
|
_configService.Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
private SyncOverrideEntry? TryGetPairOverride(string uid)
|
||||||
|
{
|
||||||
|
var overrides = _configService.Current.PairSyncOverrides;
|
||||||
|
return overrides != null && overrides.TryGetValue(uid, out var entry) ? entry : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private SyncOverrideEntry? TryGetGroupOverride(string gid)
|
||||||
|
{
|
||||||
|
var overrides = _configService.Current.GroupSyncOverrides;
|
||||||
|
return overrides != null && overrides.TryGetValue(gid, out var entry) ? entry : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ApplyDefaults(ref UserPermissions permissions, MareConfig config, SyncOverrideEntry? overrides)
|
||||||
|
{
|
||||||
|
bool changed = false;
|
||||||
|
if (overrides?.DisableSounds is bool overrideSounds)
|
||||||
|
{
|
||||||
|
if (permissions.IsDisableSounds() != overrideSounds)
|
||||||
|
{
|
||||||
|
permissions.SetDisableSounds(overrideSounds);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (permissions.IsDisableSounds() != config.DefaultDisableSounds)
|
||||||
|
{
|
||||||
|
permissions.SetDisableSounds(config.DefaultDisableSounds);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (overrides?.DisableAnimations is bool overrideAnims)
|
||||||
|
{
|
||||||
|
if (permissions.IsDisableAnimations() != overrideAnims)
|
||||||
|
{
|
||||||
|
permissions.SetDisableAnimations(overrideAnims);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (permissions.IsDisableAnimations() != config.DefaultDisableAnimations)
|
||||||
|
{
|
||||||
|
permissions.SetDisableAnimations(config.DefaultDisableAnimations);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (overrides?.DisableVfx is bool overrideVfx)
|
||||||
|
{
|
||||||
|
if (permissions.IsDisableVFX() != overrideVfx)
|
||||||
|
{
|
||||||
|
permissions.SetDisableVFX(overrideVfx);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (permissions.IsDisableVFX() != config.DefaultDisableVfx)
|
||||||
|
{
|
||||||
|
permissions.SetDisableVFX(config.DefaultDisableVfx);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ApplyDefaults(ref GroupUserPermissions permissions, MareConfig config, SyncOverrideEntry? overrides)
|
||||||
|
{
|
||||||
|
bool changed = false;
|
||||||
|
if (overrides?.DisableSounds is bool overrideSounds)
|
||||||
|
{
|
||||||
|
if (permissions.IsDisableSounds() != overrideSounds)
|
||||||
|
{
|
||||||
|
permissions.SetDisableSounds(overrideSounds);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (permissions.IsDisableSounds() != config.DefaultDisableSounds)
|
||||||
|
{
|
||||||
|
permissions.SetDisableSounds(config.DefaultDisableSounds);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (overrides?.DisableAnimations is bool overrideAnims)
|
||||||
|
{
|
||||||
|
if (permissions.IsDisableAnimations() != overrideAnims)
|
||||||
|
{
|
||||||
|
permissions.SetDisableAnimations(overrideAnims);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (permissions.IsDisableAnimations() != config.DefaultDisableAnimations)
|
||||||
|
{
|
||||||
|
permissions.SetDisableAnimations(config.DefaultDisableAnimations);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (overrides?.DisableVfx is bool overrideVfx)
|
||||||
|
{
|
||||||
|
if (permissions.IsDisableVFX() != overrideVfx)
|
||||||
|
{
|
||||||
|
permissions.SetDisableVFX(overrideVfx);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (permissions.IsDisableVFX() != config.DefaultDisableVfx)
|
||||||
|
{
|
||||||
|
permissions.SetDisableVFX(config.DefaultDisableVfx);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
}
|
||||||
225
MareSynchronos/Services/TemporarySyncshellNotificationService.cs
Normal file
225
MareSynchronos/Services/TemporarySyncshellNotificationService.cs
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using System.Threading;
|
||||||
|
using MareSynchronos.API.Dto.Group;
|
||||||
|
using MareSynchronos.MareConfiguration.Models;
|
||||||
|
using MareSynchronos.PlayerData.Pairs;
|
||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using MareSynchronos.WebAPI;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace MareSynchronos.Services;
|
||||||
|
|
||||||
|
public sealed class TemporarySyncshellNotificationService : MediatorSubscriberBase, IHostedService
|
||||||
|
{
|
||||||
|
private static readonly int[] NotificationThresholdMinutes = [30, 15, 5, 1];
|
||||||
|
private readonly ApiController _apiController;
|
||||||
|
private readonly PairManager _pairManager;
|
||||||
|
private readonly Lock _stateLock = new();
|
||||||
|
private readonly Dictionary<string, TrackedGroup> _trackedGroups = new(StringComparer.Ordinal);
|
||||||
|
private CancellationTokenSource? _loopCts;
|
||||||
|
private Task? _loopTask;
|
||||||
|
|
||||||
|
public TemporarySyncshellNotificationService(ILogger<TemporarySyncshellNotificationService> logger, MareMediator mediator, PairManager pairManager, ApiController apiController)
|
||||||
|
: base(logger, mediator)
|
||||||
|
{
|
||||||
|
_pairManager = pairManager;
|
||||||
|
_apiController = apiController;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_loopCts = new CancellationTokenSource();
|
||||||
|
Mediator.Subscribe<ConnectedMessage>(this, _ => ResetTrackedGroups());
|
||||||
|
Mediator.Subscribe<DisconnectedMessage>(this, _ => ResetTrackedGroups());
|
||||||
|
_loopTask = Task.Run(() => MonitorLoopAsync(_loopCts.Token), _loopCts.Token);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task StopAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
Mediator.UnsubscribeAll(this);
|
||||||
|
if (_loopCts == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_loopCts.Cancel();
|
||||||
|
if (_loopTask != null)
|
||||||
|
{
|
||||||
|
await _loopTask.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_loopTask = null;
|
||||||
|
_loopCts.Dispose();
|
||||||
|
_loopCts = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task MonitorLoopAsync(CancellationToken ct)
|
||||||
|
{
|
||||||
|
var delay = TimeSpan.FromSeconds(30);
|
||||||
|
while (!ct.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
CheckGroups();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogDebug(ex, "Failed to check temporary syncshell expirations");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.Delay(delay, ct).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CheckGroups()
|
||||||
|
{
|
||||||
|
var nowUtc = DateTime.UtcNow;
|
||||||
|
var groupsSnapshot = _pairManager.Groups.Values.ToList();
|
||||||
|
var notifications = new List<NotificationPayload>();
|
||||||
|
var expiredGroups = new List<GroupFullInfoDto>();
|
||||||
|
var seenTemporaryGids = new HashSet<string>(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
using (var guard = _stateLock.EnterScope())
|
||||||
|
{
|
||||||
|
foreach (var group in groupsSnapshot)
|
||||||
|
{
|
||||||
|
if (!group.IsTemporary || group.ExpiresAt == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(_apiController.UID) || !string.Equals(group.OwnerUID, _apiController.UID, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var gid = group.Group.GID;
|
||||||
|
seenTemporaryGids.Add(gid);
|
||||||
|
var expiresAtUtc = NormalizeToUtc(group.ExpiresAt.Value);
|
||||||
|
var remaining = expiresAtUtc - nowUtc;
|
||||||
|
|
||||||
|
if (!_trackedGroups.TryGetValue(gid, out var state))
|
||||||
|
{
|
||||||
|
state = new TrackedGroup(expiresAtUtc);
|
||||||
|
_trackedGroups[gid] = state;
|
||||||
|
}
|
||||||
|
else if (state.ExpiresAtUtc != expiresAtUtc)
|
||||||
|
{
|
||||||
|
state.UpdateExpiresAt(expiresAtUtc);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remaining <= TimeSpan.Zero)
|
||||||
|
{
|
||||||
|
_trackedGroups.Remove(gid);
|
||||||
|
expiredGroups.Add(group);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!state.LastRemaining.HasValue)
|
||||||
|
{
|
||||||
|
state.UpdateRemaining(remaining);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var previousRemaining = state.LastRemaining.Value;
|
||||||
|
|
||||||
|
foreach (var thresholdMinutes in NotificationThresholdMinutes)
|
||||||
|
{
|
||||||
|
var threshold = TimeSpan.FromMinutes(thresholdMinutes);
|
||||||
|
if (previousRemaining > threshold && remaining <= threshold)
|
||||||
|
{
|
||||||
|
notifications.Add(new NotificationPayload(group, thresholdMinutes, expiresAtUtc));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.UpdateRemaining(remaining);
|
||||||
|
}
|
||||||
|
|
||||||
|
var toRemove = _trackedGroups.Keys.Where(k => !seenTemporaryGids.Contains(k)).ToList();
|
||||||
|
foreach (var gid in toRemove)
|
||||||
|
{
|
||||||
|
_trackedGroups.Remove(gid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var expiredGroup in expiredGroups)
|
||||||
|
{
|
||||||
|
Logger.LogInformation("Temporary syncshell {gid} expired locally; removing", expiredGroup.Group.GID);
|
||||||
|
_pairManager.RemoveGroup(expiredGroup.Group);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var notification in notifications)
|
||||||
|
{
|
||||||
|
PublishNotification(notification.Group, notification.ThresholdMinutes, notification.ExpiresAtUtc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PublishNotification(GroupFullInfoDto group, int thresholdMinutes, DateTime expiresAtUtc)
|
||||||
|
{
|
||||||
|
string displayName = string.IsNullOrWhiteSpace(group.GroupAlias) ? group.Group.GID : group.GroupAlias!;
|
||||||
|
string threshold = thresholdMinutes == 1 ? "1 minute" : $"{thresholdMinutes} minutes";
|
||||||
|
string expiresLocal = expiresAtUtc.ToLocalTime().ToString("t", CultureInfo.CurrentCulture);
|
||||||
|
|
||||||
|
string message = $"La Syncshell temporaire \"{displayName}\" sera supprimee dans {threshold} (a {expiresLocal}).";
|
||||||
|
Mediator.Publish(new NotificationMessage("Syncshell temporaire", message, NotificationType.Warning, TimeSpan.FromSeconds(6)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DateTime NormalizeToUtc(DateTime expiresAt)
|
||||||
|
{
|
||||||
|
return expiresAt.Kind switch
|
||||||
|
{
|
||||||
|
DateTimeKind.Utc => expiresAt,
|
||||||
|
DateTimeKind.Local => expiresAt.ToUniversalTime(),
|
||||||
|
_ => DateTime.SpecifyKind(expiresAt, DateTimeKind.Utc)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ResetTrackedGroups()
|
||||||
|
{
|
||||||
|
using (var guard = _stateLock.EnterScope())
|
||||||
|
{
|
||||||
|
_trackedGroups.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TrackedGroup
|
||||||
|
{
|
||||||
|
public TrackedGroup(DateTime expiresAtUtc)
|
||||||
|
{
|
||||||
|
ExpiresAtUtc = expiresAtUtc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DateTime ExpiresAtUtc { get; private set; }
|
||||||
|
public TimeSpan? LastRemaining { get; private set; }
|
||||||
|
|
||||||
|
public void UpdateExpiresAt(DateTime expiresAtUtc)
|
||||||
|
{
|
||||||
|
ExpiresAtUtc = expiresAtUtc;
|
||||||
|
LastRemaining = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateRemaining(TimeSpan remaining)
|
||||||
|
{
|
||||||
|
LastRemaining = remaining;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed record NotificationPayload(GroupFullInfoDto Group, int ThresholdMinutes, DateTime ExpiresAtUtc);
|
||||||
|
}
|
||||||
117
MareSynchronos/Services/TypingIndicatorStateService.cs
Normal file
117
MareSynchronos/Services/TypingIndicatorStateService.cs
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Linq;
|
||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using MareSynchronos.WebAPI;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using MareSynchronos.API.Data;
|
||||||
|
|
||||||
|
namespace MareSynchronos.Services;
|
||||||
|
|
||||||
|
public sealed class TypingIndicatorStateService : IMediatorSubscriber, IDisposable
|
||||||
|
{
|
||||||
|
private sealed record TypingEntry(UserData User, DateTime FirstSeen, DateTime LastUpdate);
|
||||||
|
|
||||||
|
private readonly ConcurrentDictionary<string, TypingEntry> _typingUsers = new(StringComparer.Ordinal);
|
||||||
|
private readonly ApiController _apiController;
|
||||||
|
private readonly ILogger<TypingIndicatorStateService> _logger;
|
||||||
|
private DateTime _selfTypingLast = DateTime.MinValue;
|
||||||
|
private DateTime _selfTypingStart = DateTime.MinValue;
|
||||||
|
private bool _selfTypingActive;
|
||||||
|
|
||||||
|
public TypingIndicatorStateService(ILogger<TypingIndicatorStateService> logger, MareMediator mediator, ApiController apiController)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_apiController = apiController;
|
||||||
|
Mediator = mediator;
|
||||||
|
|
||||||
|
mediator.Subscribe<UserTypingStateMessage>(this, OnTypingState);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Mediator.UnsubscribeAll(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public MareMediator Mediator { get; }
|
||||||
|
|
||||||
|
public void SetSelfTypingLocal(bool isTyping)
|
||||||
|
{
|
||||||
|
if (isTyping)
|
||||||
|
{
|
||||||
|
if (!_selfTypingActive)
|
||||||
|
_selfTypingStart = DateTime.UtcNow;
|
||||||
|
_selfTypingLast = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_selfTypingStart = DateTime.MinValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
_selfTypingActive = isTyping;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnTypingState(UserTypingStateMessage msg)
|
||||||
|
{
|
||||||
|
var uid = msg.Typing.User.UID;
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
|
||||||
|
if (string.Equals(uid, _apiController.UID, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
_selfTypingActive = msg.Typing.IsTyping;
|
||||||
|
if (_selfTypingActive)
|
||||||
|
{
|
||||||
|
if (_selfTypingStart == DateTime.MinValue)
|
||||||
|
_selfTypingStart = now;
|
||||||
|
_selfTypingLast = now;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_selfTypingStart = DateTime.MinValue;
|
||||||
|
}
|
||||||
|
_logger.LogInformation("Typing state self -> {state}", _selfTypingActive);
|
||||||
|
}
|
||||||
|
else if (msg.Typing.IsTyping)
|
||||||
|
{
|
||||||
|
_typingUsers.AddOrUpdate(uid,
|
||||||
|
_ => new TypingEntry(msg.Typing.User, now, now),
|
||||||
|
(_, existing) => new TypingEntry(msg.Typing.User, existing.FirstSeen, now));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_typingUsers.TryRemove(uid, out _);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetSelfTyping(TimeSpan maxAge, out DateTime startTyping, out DateTime lastTyping)
|
||||||
|
{
|
||||||
|
startTyping = _selfTypingStart;
|
||||||
|
lastTyping = _selfTypingLast;
|
||||||
|
if (!_selfTypingActive)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
if ((now - _selfTypingLast) >= maxAge)
|
||||||
|
{
|
||||||
|
_selfTypingActive = false;
|
||||||
|
_selfTypingStart = DateTime.MinValue;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyDictionary<string, (UserData User, DateTime FirstSeen, DateTime LastUpdate)> GetActiveTypers(TimeSpan maxAge)
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
foreach (var kvp in _typingUsers.ToArray())
|
||||||
|
{
|
||||||
|
if ((now - kvp.Value.LastUpdate) >= maxAge)
|
||||||
|
{
|
||||||
|
_typingUsers.TryRemove(kvp.Key, out _);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return _typingUsers.ToDictionary(k => k.Key, v => (v.Value.User, v.Value.FirstSeen, v.Value.LastUpdate), StringComparer.Ordinal);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,13 +40,13 @@ 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)
|
||||||
{
|
{
|
||||||
return new StandaloneProfileUi(_loggerFactory.CreateLogger<StandaloneProfileUi>(), _mareMediator,
|
return new StandaloneProfileUi(_loggerFactory.CreateLogger<StandaloneProfileUi>(), _mareMediator,
|
||||||
_uiSharedService, _serverConfigManager, _mareProfileManager, _pairManager, pair, _performanceCollectorService);
|
_uiSharedService, _serverConfigManager, _mareProfileManager, pair, _performanceCollectorService);
|
||||||
}
|
}
|
||||||
|
|
||||||
public PermissionWindowUI CreatePermissionPopupUi(Pair pair)
|
public PermissionWindowUI CreatePermissionPopupUi(Pair pair)
|
||||||
|
|||||||
@@ -1,17 +1,23 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using Dalamud.Bindings.ImGui;
|
using Dalamud.Bindings.ImGui;
|
||||||
using Dalamud.Interface.Colors;
|
using Dalamud.Interface.Colors;
|
||||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
|
||||||
using Dalamud.Interface.Utility;
|
using Dalamud.Interface.Utility;
|
||||||
using Dalamud.Interface.Utility.Raii;
|
using Dalamud.Interface.Utility.Raii;
|
||||||
using Dalamud.Plugin.Services;
|
using MareSynchronos.API.Dto.Group;
|
||||||
using MareSynchronos.MareConfiguration;
|
using MareSynchronos.MareConfiguration;
|
||||||
using MareSynchronos.Services;
|
|
||||||
using MareSynchronos.PlayerData.Pairs;
|
using MareSynchronos.PlayerData.Pairs;
|
||||||
using MareSynchronos.Services.Mediator;
|
using MareSynchronos.Services.Mediator;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using MareSynchronos.Services;
|
||||||
|
using MareSynchronos.Services.AutoDetect;
|
||||||
|
using NotificationType = MareSynchronos.MareConfiguration.Models.NotificationType;
|
||||||
|
|
||||||
namespace MareSynchronos.UI;
|
namespace MareSynchronos.UI;
|
||||||
|
|
||||||
@@ -19,22 +25,35 @@ public class AutoDetectUi : WindowMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
private readonly MareConfigService _configService;
|
private readonly MareConfigService _configService;
|
||||||
private readonly DalamudUtilService _dalamud;
|
private readonly DalamudUtilService _dalamud;
|
||||||
private readonly IObjectTable _objectTable;
|
private readonly AutoDetectRequestService _requestService;
|
||||||
private readonly Services.AutoDetect.AutoDetectRequestService _requestService;
|
private readonly NearbyDiscoveryService _discoveryService;
|
||||||
|
private readonly NearbyPendingService _pendingService;
|
||||||
private readonly PairManager _pairManager;
|
private readonly PairManager _pairManager;
|
||||||
private List<Services.Mediator.NearbyEntry> _entries = new();
|
private List<Services.Mediator.NearbyEntry> _entries;
|
||||||
|
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, IObjectTable objectTable,
|
MareConfigService configService, DalamudUtilService dalamudUtilService,
|
||||||
Services.AutoDetect.AutoDetectRequestService requestService, PairManager pairManager,
|
AutoDetectRequestService requestService, NearbyPendingService pendingService, PairManager pairManager,
|
||||||
|
NearbyDiscoveryService discoveryService, SyncshellDiscoveryService syncshellDiscoveryService,
|
||||||
PerformanceCollectorService performanceCollectorService)
|
PerformanceCollectorService performanceCollectorService)
|
||||||
: base(logger, mediator, "AutoDetect", performanceCollectorService)
|
: base(logger, mediator, "AutoDetect", performanceCollectorService)
|
||||||
{
|
{
|
||||||
_configService = configService;
|
_configService = configService;
|
||||||
_dalamud = dalamudUtilService;
|
_dalamud = dalamudUtilService;
|
||||||
_objectTable = objectTable;
|
|
||||||
_requestService = requestService;
|
_requestService = requestService;
|
||||||
|
_pendingService = pendingService;
|
||||||
_pairManager = pairManager;
|
_pairManager = pairManager;
|
||||||
|
_discoveryService = discoveryService;
|
||||||
|
_syncshellDiscoveryService = syncshellDiscoveryService;
|
||||||
|
Mediator.Subscribe<Services.Mediator.DiscoveryListUpdated>(this, OnDiscoveryUpdated);
|
||||||
|
Mediator.Subscribe<SyncshellDiscoveryUpdated>(this, OnSyncshellDiscoveryUpdated);
|
||||||
|
_entries = _discoveryService.SnapshotEntries();
|
||||||
|
|
||||||
Flags |= ImGuiWindowFlags.NoScrollbar;
|
Flags |= ImGuiWindowFlags.NoScrollbar;
|
||||||
SizeConstraints = new WindowSizeConstraints()
|
SizeConstraints = new WindowSizeConstraints()
|
||||||
@@ -53,9 +72,139 @@ public class AutoDetectUi : WindowMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
using var idScope = ImRaii.PushId("autodetect-ui");
|
using var idScope = ImRaii.PushId("autodetect-ui");
|
||||||
|
|
||||||
|
var incomingInvites = _pendingService.Pending.ToList();
|
||||||
|
var outgoingInvites = _requestService.GetPendingRequestsSnapshot();
|
||||||
|
|
||||||
|
Vector4 accent = UiSharedService.AccentColor;
|
||||||
|
if (accent.W <= 0f) accent = ImGuiColors.ParsedPurple;
|
||||||
|
Vector4 inactiveTab = new(accent.X * 0.45f, accent.Y * 0.45f, accent.Z * 0.45f, Math.Clamp(accent.W + 0.15f, 0f, 1f));
|
||||||
|
Vector4 hoverTab = UiSharedService.AccentHoverColor;
|
||||||
|
|
||||||
|
using var tabs = ImRaii.TabBar("AutoDetectTabs");
|
||||||
|
if (!tabs.Success) return;
|
||||||
|
|
||||||
|
var incomingCount = incomingInvites.Count;
|
||||||
|
DrawStyledTab($"Invitations ({incomingCount})", accent, inactiveTab, hoverTab, () =>
|
||||||
|
{
|
||||||
|
DrawInvitationsTab(incomingInvites, outgoingInvites);
|
||||||
|
});
|
||||||
|
|
||||||
|
DrawStyledTab("Proximité", accent, inactiveTab, hoverTab, DrawNearbyTab);
|
||||||
|
DrawStyledTab("Syncshell", accent, inactiveTab, hoverTab, DrawSyncshellTab);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DrawInline()
|
||||||
|
{
|
||||||
|
DrawInternal();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DrawStyledTab(string label, Vector4 accent, Vector4 inactive, Vector4 hover, Action draw, bool disabled = false)
|
||||||
|
{
|
||||||
|
var tabColor = disabled ? ImGuiColors.DalamudGrey3 : inactive;
|
||||||
|
var tabHover = disabled ? ImGuiColors.DalamudGrey3 : hover;
|
||||||
|
var tabActive = disabled ? ImGuiColors.DalamudGrey2 : accent;
|
||||||
|
using var baseColor = ImRaii.PushColor(ImGuiCol.Tab, tabColor);
|
||||||
|
using var hoverColor = ImRaii.PushColor(ImGuiCol.TabHovered, tabHover);
|
||||||
|
using var activeColor = ImRaii.PushColor(ImGuiCol.TabActive, tabActive);
|
||||||
|
using var activeText = ImRaii.PushColor(ImGuiCol.Text, disabled ? ImGuiColors.DalamudGrey2 : Vector4.One, false);
|
||||||
|
using var tab = ImRaii.TabItem(label);
|
||||||
|
if (tab.Success)
|
||||||
|
{
|
||||||
|
draw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawInvitationsTab(List<KeyValuePair<string, string>> incomingInvites, IReadOnlyCollection<AutoDetectRequestService.PendingRequestInfo> outgoingInvites)
|
||||||
|
{
|
||||||
|
if (incomingInvites.Count == 0 && outgoingInvites.Count == 0)
|
||||||
|
{
|
||||||
|
UiSharedService.ColorTextWrapped("Aucune invitation en attente. Cette page regroupera les demandes reçues et celles que vous avez envoyées.", ImGuiColors.DalamudGrey3);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (incomingInvites.Count == 0)
|
||||||
|
{
|
||||||
|
UiSharedService.ColorTextWrapped("Vous n'avez aucune invitation de pair en attente pour le moment.", ImGuiColors.DalamudGrey3);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGuiHelpers.ScaledDummy(4);
|
||||||
|
float leftWidth = Math.Max(220f * ImGuiHelpers.GlobalScale, ImGui.CalcTextSize("Invitations reçues (00)").X + ImGui.GetStyle().FramePadding.X * 4f);
|
||||||
|
var avail = ImGui.GetContentRegionAvail();
|
||||||
|
|
||||||
|
ImGui.BeginChild("incoming-requests", new Vector2(leftWidth, avail.Y), true);
|
||||||
|
ImGui.TextColored(ImGuiColors.DalamudOrange, $"Invitations reçues ({incomingInvites.Count})");
|
||||||
|
ImGui.Separator();
|
||||||
|
if (incomingInvites.Count == 0)
|
||||||
|
{
|
||||||
|
ImGui.TextDisabled("Aucune invitation reçue.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach (var (uid, name) in incomingInvites.OrderBy(k => k.Value, StringComparer.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
using var id = ImRaii.PushId(uid);
|
||||||
|
bool processing = _acceptInFlight.Contains(uid);
|
||||||
|
ImGui.TextUnformatted(name);
|
||||||
|
ImGui.TextDisabled(uid);
|
||||||
|
if (processing)
|
||||||
|
{
|
||||||
|
ImGui.TextDisabled("Traitement en cours...");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (ImGui.Button("Accepter"))
|
||||||
|
{
|
||||||
|
TriggerAccept(uid);
|
||||||
|
}
|
||||||
|
ImGui.SameLine();
|
||||||
|
if (ImGui.Button("Refuser"))
|
||||||
|
{
|
||||||
|
_pendingService.Remove(uid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui.Separator();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui.EndChild();
|
||||||
|
|
||||||
|
ImGui.SameLine();
|
||||||
|
|
||||||
|
ImGui.BeginChild("outgoing-requests", new Vector2(0, avail.Y), true);
|
||||||
|
ImGui.TextColored(ImGuiColors.DalamudOrange, $"Invitations envoyées ({outgoingInvites.Count})");
|
||||||
|
ImGui.Separator();
|
||||||
|
if (outgoingInvites.Count == 0)
|
||||||
|
{
|
||||||
|
ImGui.TextDisabled("Aucune invitation envoyée en attente.");
|
||||||
|
ImGui.EndChild();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var info in outgoingInvites.OrderByDescending(i => i.SentAt))
|
||||||
|
{
|
||||||
|
using var id = ImRaii.PushId(info.Key);
|
||||||
|
ImGui.TextUnformatted(info.TargetDisplayName);
|
||||||
|
if (!string.IsNullOrEmpty(info.Uid))
|
||||||
|
{
|
||||||
|
ImGui.TextDisabled(info.Uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.TextDisabled($"Envoyée il y a {FormatDuration(DateTime.UtcNow - info.SentAt)}");
|
||||||
|
if (ImGui.Button("Retirer"))
|
||||||
|
{
|
||||||
|
_requestService.RemovePendingRequestByKey(info.Key);
|
||||||
|
}
|
||||||
|
UiSharedService.AttachToolTip("Retire uniquement cette entrée locale de suivi.");
|
||||||
|
ImGui.Separator();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.EndChild();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawNearbyTab()
|
||||||
|
{
|
||||||
if (!_configService.Current.EnableAutoDetectDiscovery)
|
if (!_configService.Current.EnableAutoDetectDiscovery)
|
||||||
{
|
{
|
||||||
UiSharedService.ColorTextWrapped("Nearby detection is disabled. Enable it in Settings to start detecting nearby Umbra users.", ImGuiColors.DalamudYellow);
|
UiSharedService.ColorTextWrapped("AutoDetect est désactivé. Activez-le dans les paramètres pour détecter les utilisateurs Umbra à proximité.", ImGuiColors.DalamudYellow);
|
||||||
ImGuiHelpers.ScaledDummy(6);
|
ImGuiHelpers.ScaledDummy(6);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,61 +221,229 @@ public class AutoDetectUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
ImGuiHelpers.ScaledDummy(6);
|
ImGuiHelpers.ScaledDummy(6);
|
||||||
|
|
||||||
// Table header
|
var sourceEntries = _entries.Count > 0 ? _entries : _discoveryService.SnapshotEntries();
|
||||||
if (ImGui.BeginTable("autodetect-nearby", 5, ImGuiTableFlags.SizingStretchProp))
|
var orderedEntries = sourceEntries
|
||||||
|
.Where(e => e.IsMatch)
|
||||||
|
.OrderBy(e => float.IsNaN(e.Distance) ? float.MaxValue : e.Distance)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (orderedEntries.Count == 0)
|
||||||
{
|
{
|
||||||
ImGui.TableSetupColumn("Name");
|
UiSharedService.ColorTextWrapped("Aucune présence UmbraSync détectée à proximité pour le moment.", ImGuiColors.DalamudGrey3);
|
||||||
ImGui.TableSetupColumn("World");
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ImGui.BeginTable("autodetect-nearby", 5, ImGuiTableFlags.SizingStretchProp | ImGuiTableFlags.RowBg))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.TableSetupColumn("Nom");
|
||||||
|
ImGui.TableSetupColumn("Monde");
|
||||||
ImGui.TableSetupColumn("Distance");
|
ImGui.TableSetupColumn("Distance");
|
||||||
ImGui.TableSetupColumn("Status");
|
ImGui.TableSetupColumn("Statut");
|
||||||
ImGui.TableSetupColumn("Action");
|
ImGui.TableSetupColumn("Action");
|
||||||
ImGui.TableHeadersRow();
|
ImGui.TableHeadersRow();
|
||||||
|
|
||||||
var data = _entries.Count > 0 ? _entries.Where(e => e.IsMatch).ToList() : new List<Services.Mediator.NearbyEntry>();
|
for (int i = 0; i < orderedEntries.Count; i++)
|
||||||
foreach (var e in data)
|
|
||||||
{
|
{
|
||||||
|
var entry = orderedEntries[i];
|
||||||
|
bool alreadyPaired = IsAlreadyPairedByUidOrAlias(entry);
|
||||||
|
bool overDistance = !float.IsNaN(entry.Distance) && entry.Distance > maxDist;
|
||||||
|
bool canRequest = entry.AcceptPairRequests && !string.IsNullOrEmpty(entry.Token) && !alreadyPaired;
|
||||||
|
|
||||||
|
string displayName = entry.DisplayName ?? entry.Name;
|
||||||
|
string worldName = entry.WorldId == 0
|
||||||
|
? "-"
|
||||||
|
: (_dalamud.WorldData.Value.TryGetValue(entry.WorldId, out var mappedWorld) ? mappedWorld : entry.WorldId.ToString(CultureInfo.InvariantCulture));
|
||||||
|
string distanceText = float.IsNaN(entry.Distance) ? "-" : $"{entry.Distance:0.0} m";
|
||||||
|
|
||||||
|
string status = alreadyPaired
|
||||||
|
? "Déjà appairé"
|
||||||
|
: overDistance
|
||||||
|
? $"Hors portée (> {maxDist} m)"
|
||||||
|
: !entry.AcceptPairRequests
|
||||||
|
? "Invitations refusées"
|
||||||
|
: string.IsNullOrEmpty(entry.Token)
|
||||||
|
? "Indisponible"
|
||||||
|
: "Disponible";
|
||||||
|
|
||||||
ImGui.TableNextColumn();
|
ImGui.TableNextColumn();
|
||||||
ImGui.TextUnformatted(e.Name);
|
ImGui.TextUnformatted(displayName);
|
||||||
|
|
||||||
ImGui.TableNextColumn();
|
ImGui.TableNextColumn();
|
||||||
ImGui.TextUnformatted(e.WorldId == 0 ? "-" : (_dalamud.WorldData.Value.TryGetValue(e.WorldId, out var w) ? w : e.WorldId.ToString()));
|
ImGui.TextUnformatted(worldName);
|
||||||
|
|
||||||
ImGui.TableNextColumn();
|
ImGui.TableNextColumn();
|
||||||
ImGui.TextUnformatted(float.IsNaN(e.Distance) ? "-" : $"{e.Distance:0.0} m");
|
ImGui.TextUnformatted(distanceText);
|
||||||
|
|
||||||
ImGui.TableNextColumn();
|
ImGui.TableNextColumn();
|
||||||
bool alreadyPaired = IsAlreadyPairedByUidOrAlias(e);
|
|
||||||
string status = alreadyPaired ? "Paired" : (string.IsNullOrEmpty(e.Token) ? "Requests disabled" : "On Umbra");
|
|
||||||
ImGui.TextUnformatted(status);
|
ImGui.TextUnformatted(status);
|
||||||
|
|
||||||
ImGui.TableNextColumn();
|
ImGui.TableNextColumn();
|
||||||
using (ImRaii.Disabled(alreadyPaired || string.IsNullOrEmpty(e.Token)))
|
using (ImRaii.PushId(i))
|
||||||
{
|
{
|
||||||
if (alreadyPaired)
|
if (canRequest && !overDistance)
|
||||||
{
|
{
|
||||||
ImGui.Button($"Already sync##{e.Name}");
|
if (ImGui.Button("Envoyer invitation"))
|
||||||
|
{
|
||||||
|
_ = _requestService.SendRequestAsync(entry.Token!, entry.Uid, entry.DisplayName);
|
||||||
}
|
}
|
||||||
else if (string.IsNullOrEmpty(e.Token))
|
UiSharedService.AttachToolTip("Envoie une demande d'appairage via AutoDetect.");
|
||||||
{
|
|
||||||
ImGui.Button($"Requests disabled##{e.Name}");
|
|
||||||
}
|
}
|
||||||
else if (ImGui.Button($"Send request##{e.Name}"))
|
else
|
||||||
{
|
{
|
||||||
_ = _requestService.SendRequestAsync(e.Token!);
|
string reason = alreadyPaired
|
||||||
|
? "Vous êtes déjà appairé avec ce joueur."
|
||||||
|
: overDistance
|
||||||
|
? $"Ce joueur est au-delà de la distance maximale configurée ({maxDist} m)."
|
||||||
|
: !entry.AcceptPairRequests
|
||||||
|
? "Ce joueur a désactivé la réception automatique des invitations."
|
||||||
|
: string.IsNullOrEmpty(entry.Token)
|
||||||
|
? "Impossible d'obtenir un jeton d'invitation pour ce joueur."
|
||||||
|
: string.Empty;
|
||||||
|
|
||||||
|
ImGui.TextDisabled(status);
|
||||||
|
if (!string.IsNullOrEmpty(reason))
|
||||||
|
{
|
||||||
|
UiSharedService.AttachToolTip(reason);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui.EndTable();
|
ImGui.EndTable();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task JoinSyncshellAsync(SyncshellDiscoveryEntryDto entry)
|
||||||
|
{
|
||||||
|
if (!_syncshellJoinInFlight.Add(entry.GID))
|
||||||
|
{
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void OnOpen()
|
try
|
||||||
{
|
{
|
||||||
base.OnOpen();
|
var joined = await _syncshellDiscoveryService.JoinAsync(entry.GID, CancellationToken.None).ConfigureAwait(false);
|
||||||
Mediator.Subscribe<Services.Mediator.DiscoveryListUpdated>(this, OnDiscoveryUpdated);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void OnClose()
|
private void DrawSyncshellTab()
|
||||||
{
|
{
|
||||||
Mediator.Unsubscribe<Services.Mediator.DiscoveryListUpdated>(this);
|
if (!_syncshellInitialized)
|
||||||
base.OnClose();
|
{
|
||||||
|
_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)
|
||||||
@@ -134,24 +451,9 @@ public class AutoDetectUi : WindowMediatorSubscriberBase
|
|||||||
_entries = msg.Entries;
|
_entries = msg.Entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Services.Mediator.NearbyEntry> BuildLocalSnapshot(int maxDist)
|
private void OnSyncshellDiscoveryUpdated(SyncshellDiscoveryUpdated msg)
|
||||||
{
|
{
|
||||||
var list = new List<Services.Mediator.NearbyEntry>();
|
_syncshellEntries = msg.Entries;
|
||||||
var local = _dalamud.GetPlayerCharacter();
|
|
||||||
var localPos = local?.Position ?? Vector3.Zero;
|
|
||||||
for (int i = 0; i < 200; i += 2)
|
|
||||||
{
|
|
||||||
var obj = _objectTable[i];
|
|
||||||
if (obj == null || obj.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player) continue;
|
|
||||||
if (local != null && obj.Address == local.Address) continue;
|
|
||||||
float dist = local == null ? float.NaN : Vector3.Distance(localPos, obj.Position);
|
|
||||||
if (!float.IsNaN(dist) && dist > maxDist) continue;
|
|
||||||
string name = obj.Name.ToString();
|
|
||||||
ushort worldId = 0;
|
|
||||||
if (obj is IPlayerCharacter pc) worldId = (ushort)pc.HomeWorld.RowId;
|
|
||||||
list.Add(new Services.Mediator.NearbyEntry(name, worldId, dist, false, null, null, null));
|
|
||||||
}
|
|
||||||
return list;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsAlreadyPairedByUidOrAlias(Services.Mediator.NearbyEntry e)
|
private bool IsAlreadyPairedByUidOrAlias(Services.Mediator.NearbyEntry e)
|
||||||
@@ -194,4 +496,37 @@ public class AutoDetectUi : WindowMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
return sb.ToString();
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void TriggerAccept(string uid)
|
||||||
|
{
|
||||||
|
if (!_acceptInFlight.Add(uid)) return;
|
||||||
|
|
||||||
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
bool ok = await _pendingService.AcceptAsync(uid).ConfigureAwait(false);
|
||||||
|
if (!ok)
|
||||||
|
{
|
||||||
|
Mediator.Publish(new NotificationMessage("AutoDetect", $"Impossible d'accepter l'invitation {uid}.", NotificationType.Warning, TimeSpan.FromSeconds(5)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_acceptInFlight.Remove(uid);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatDuration(TimeSpan span)
|
||||||
|
{
|
||||||
|
if (span.TotalMinutes >= 1)
|
||||||
|
{
|
||||||
|
var minutes = Math.Max(1, (int)Math.Round(span.TotalMinutes));
|
||||||
|
return minutes == 1 ? "1 minute" : $"{minutes} minutes";
|
||||||
|
}
|
||||||
|
|
||||||
|
var seconds = Math.Max(1, (int)Math.Round(span.TotalSeconds));
|
||||||
|
return seconds == 1 ? "1 seconde" : $"{seconds} secondes";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
245
MareSynchronos/UI/ChangelogUi.cs
Normal file
245
MareSynchronos/UI/ChangelogUi.cs
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Numerics;
|
||||||
|
using System.Reflection;
|
||||||
|
using Dalamud.Bindings.ImGui;
|
||||||
|
using Dalamud.Interface;
|
||||||
|
using Dalamud.Interface.Colors;
|
||||||
|
using Dalamud.Interface.Utility.Raii;
|
||||||
|
using MareSynchronos.MareConfiguration;
|
||||||
|
using MareSynchronos.Services;
|
||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace MareSynchronos.UI;
|
||||||
|
|
||||||
|
public sealed class ChangelogUi : WindowMediatorSubscriberBase
|
||||||
|
{
|
||||||
|
private const int AlwaysExpandedEntryCount = 2;
|
||||||
|
|
||||||
|
private readonly MareConfigService _configService;
|
||||||
|
private readonly UiSharedService _uiShared;
|
||||||
|
private readonly Version _currentVersion;
|
||||||
|
private readonly string _currentVersionLabel;
|
||||||
|
private readonly IReadOnlyList<ChangelogEntry> _entries;
|
||||||
|
|
||||||
|
private bool _showAllEntries;
|
||||||
|
private bool _hasAcknowledgedVersion;
|
||||||
|
|
||||||
|
public ChangelogUi(ILogger<ChangelogUi> logger, UiSharedService uiShared, MareConfigService configService,
|
||||||
|
MareMediator mediator, PerformanceCollectorService performanceCollectorService)
|
||||||
|
: base(logger, mediator, "Umbra Sync - Notes de version", performanceCollectorService)
|
||||||
|
{
|
||||||
|
_uiShared = uiShared;
|
||||||
|
_configService = configService;
|
||||||
|
_currentVersion = Assembly.GetExecutingAssembly().GetName().Version ?? new Version(0, 0, 0, 0);
|
||||||
|
_currentVersionLabel = _currentVersion.ToString();
|
||||||
|
_entries = BuildEntries();
|
||||||
|
_hasAcknowledgedVersion = string.Equals(_configService.Current.LastChangelogVersionSeen, _currentVersionLabel, StringComparison.Ordinal);
|
||||||
|
|
||||||
|
RespectCloseHotkey = true;
|
||||||
|
SizeConstraints = new()
|
||||||
|
{
|
||||||
|
MinimumSize = new(520, 360),
|
||||||
|
MaximumSize = new(900, 1200)
|
||||||
|
};
|
||||||
|
Flags |= ImGuiWindowFlags.NoResize;
|
||||||
|
ShowCloseButton = true;
|
||||||
|
|
||||||
|
if (!string.Equals(_configService.Current.LastChangelogVersionSeen, _currentVersionLabel, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
IsOpen = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnClose()
|
||||||
|
{
|
||||||
|
MarkCurrentVersionAsReadIfNeeded();
|
||||||
|
base.OnClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void DrawInternal()
|
||||||
|
{
|
||||||
|
_ = _uiShared.DrawOtherPluginState();
|
||||||
|
|
||||||
|
DrawHeader();
|
||||||
|
DrawEntries();
|
||||||
|
DrawFooter();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawHeader()
|
||||||
|
{
|
||||||
|
using (_uiShared.UidFont.Push())
|
||||||
|
{
|
||||||
|
ImGui.TextUnformatted("Notes de version");
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.TextColored(ImGuiColors.DalamudGrey, $"Version chargée : {_currentVersionLabel}");
|
||||||
|
ImGui.Separator();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawEntries()
|
||||||
|
{
|
||||||
|
bool expandedOldVersions = false;
|
||||||
|
for (int index = 0; index < _entries.Count; index++)
|
||||||
|
{
|
||||||
|
var entry = _entries[index];
|
||||||
|
if (!_showAllEntries && index >= AlwaysExpandedEntryCount)
|
||||||
|
{
|
||||||
|
if (!expandedOldVersions)
|
||||||
|
{
|
||||||
|
expandedOldVersions = ImGui.CollapsingHeader("Historique complet");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!expandedOldVersions)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DrawEntry(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawEntry(ChangelogEntry entry)
|
||||||
|
{
|
||||||
|
using (ImRaii.PushId(entry.VersionLabel))
|
||||||
|
{
|
||||||
|
ImGui.Spacing();
|
||||||
|
UiSharedService.ColorText(entry.VersionLabel, entry.Version == _currentVersion
|
||||||
|
? ImGuiColors.HealerGreen
|
||||||
|
: ImGuiColors.DalamudWhite);
|
||||||
|
|
||||||
|
ImGui.Spacing();
|
||||||
|
|
||||||
|
foreach (var line in entry.Lines)
|
||||||
|
{
|
||||||
|
DrawLine(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.Separator();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DrawLine(ChangelogLine line)
|
||||||
|
{
|
||||||
|
using var indent = line.IndentLevel > 0 ? ImRaii.PushIndent(line.IndentLevel) : null;
|
||||||
|
if (line.Color != null)
|
||||||
|
{
|
||||||
|
ImGui.TextColored(line.Color.Value, $"- {line.Text}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ImGui.TextUnformatted($"- {line.Text}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawFooter()
|
||||||
|
{
|
||||||
|
ImGui.Spacing();
|
||||||
|
if (!_showAllEntries && _entries.Count > AlwaysExpandedEntryCount)
|
||||||
|
{
|
||||||
|
if (ImGui.Button("Tout afficher"))
|
||||||
|
{
|
||||||
|
_showAllEntries = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.SameLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui.Button("Marquer comme lu"))
|
||||||
|
{
|
||||||
|
MarkCurrentVersionAsReadIfNeeded();
|
||||||
|
IsOpen = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void MarkCurrentVersionAsReadIfNeeded()
|
||||||
|
{
|
||||||
|
if (_hasAcknowledgedVersion)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_configService.Current.LastChangelogVersionSeen = _currentVersionLabel;
|
||||||
|
_configService.Save();
|
||||||
|
_hasAcknowledgedVersion = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyList<ChangelogEntry> BuildEntries()
|
||||||
|
{
|
||||||
|
return new List<ChangelogEntry>
|
||||||
|
{
|
||||||
|
new(new Version(0, 1, 9, 5), "0.1.9.5", new List<ChangelogLine>
|
||||||
|
{
|
||||||
|
new("Fix l'affichage de la bulle dans la liste du groupe."),
|
||||||
|
new("Amélioration de l'ajout des utilisateurs via le bouton +."),
|
||||||
|
new("Possibilité de mettre en pause individuellement des utilisateurs d'une syncshell."),
|
||||||
|
new("Amélioration de la stabilité du plugin en cas de petite connexion / petite configuration."),
|
||||||
|
new("Divers fix de l'interface."),
|
||||||
|
}),
|
||||||
|
new(new Version(0, 1, 9, 4), "0.1.9.4", new List<ChangelogLine>
|
||||||
|
{
|
||||||
|
new("Réécriture complète de la bulle de frappe avec la possibilité de choisir la taille de la bulle."),
|
||||||
|
new("Désactivation de l'AutoDetect en zone instanciée."),
|
||||||
|
new("Réécriture interface AutoDetect pour acceuillir les invitations en attente et préparer les synchsells publiques."),
|
||||||
|
new("Amélioration de la compréhension des activations / désactivations des préférences de synchronisation par défaut."),
|
||||||
|
new("Mise en avant du Self Analyse avec une alerte lorsqu'un seuil de donnée a été atteint."),
|
||||||
|
new("Ajout de l'alerte de la non-compatibilité du plugin Chat2."),
|
||||||
|
new("Divers fix de l'interface."),
|
||||||
|
}),
|
||||||
|
new(new Version(0, 1, 9, 3), "0.1.9.3", new List<ChangelogLine>
|
||||||
|
{
|
||||||
|
new("Correctif de l'affichage de la bulle de frappe quand l'interface est à + de 100%."),
|
||||||
|
}),
|
||||||
|
new(new Version(0, 1, 9, 2), "0.1.9.2", new List<ChangelogLine>
|
||||||
|
{
|
||||||
|
new("Correctif de l'affichage de la bulle de frappe."),
|
||||||
|
}),
|
||||||
|
new(new Version(0, 1, 9, 1), "0.1.9.1", new List<ChangelogLine>
|
||||||
|
{
|
||||||
|
new("Début correctif pour la bulle de frappe."),
|
||||||
|
new("Les bascules de synchronisation n'affichent plus qu'une seule notification résumée."),
|
||||||
|
|
||||||
|
}),
|
||||||
|
new(new Version(0, 1, 9, 0), "0.1.9.0", new List<ChangelogLine>
|
||||||
|
{
|
||||||
|
new("Il est désormais possible de configurer par défaut nos choix de synchronisation (VFX, Music, Animation)."),
|
||||||
|
new("La catégorie 'En attente' ne s'affiche uniquement que si une invitation est en attente"),
|
||||||
|
new("(EN PRÉ VERSION) Il est désormais possible de voir quand une personne appairée est en train d'écrire avec une bulle qui s'affiche."),
|
||||||
|
new("(EN PRÉ VERSION) La bulle de frappe s'affiche également sur votre propre plaque de nom lorsque vous écrivez."),
|
||||||
|
new("Les bascules de synchronisation n'affichent plus qu'une seule notification résumée."),
|
||||||
|
new("Correctif : Désormais, les invitation entrantes ne s'affichent qu'une seule fois au lieu de deux."),
|
||||||
|
}),
|
||||||
|
new(new Version(0, 1, 8, 2), "0.1.8.2", new List<ChangelogLine>
|
||||||
|
{
|
||||||
|
new("Détection Nearby : la liste rapide ne montre plus que les joueurs réellement invitables."),
|
||||||
|
new("Sont filtrés automatiquement les personnes refusées ou déjà appairées."),
|
||||||
|
new("Invitations Nearby : anti-spam de 5 minutes par personne, blocage 15 minutes après trois refus."),
|
||||||
|
new("Affichage : Correction de l'affichage des notes par défaut plutôt que de l'ID si disponible."),
|
||||||
|
new("Les notifications de blocage sont envoyées directement dans le tchat."),
|
||||||
|
new("Overlay DTR : affiche le nombre d'invitations Nearby disponibles dans le titre et l'infobulle."),
|
||||||
|
new("Poses Nearby : le filtre re-fonctionne avec vos notes locales pour retrouver les entrées correspondantes."),
|
||||||
|
}),
|
||||||
|
new(new Version(0, 1, 8, 1), "0.1.8.1", new List<ChangelogLine>
|
||||||
|
{
|
||||||
|
new("Correctif 'Vu sous' : l'infobulle affiche désormais le dernier personnage observé."),
|
||||||
|
new("Invitations AutoDetect : triées en tête de liste pour mieux les repérer."),
|
||||||
|
new("Invitations AutoDetect : conservées entre les redémarrages du plugin ou du jeu."),
|
||||||
|
new("Barre de statut serveur : couleur violette adoptée par défaut."),
|
||||||
|
}),
|
||||||
|
new(new Version(0, 1, 8, 0), "0.1.8.0", new List<ChangelogLine>
|
||||||
|
{
|
||||||
|
new("AutoDetect : détection automatique des joueurs Umbra autour de vous et propositions d'appairage."),
|
||||||
|
new("AutoDetect : désactivé par défaut pour préserver la confidentialité.", 1, ImGuiColors.DalamudGrey),
|
||||||
|
new("AutoDetect : activez-le dans 'Transfers' avec les options Nearby detection et Allow pair requests.", 1, ImGuiColors.DalamudGrey),
|
||||||
|
new("Syncshell temporaire : durée configurable de 1 h à 7 jours, expiration automatique."),
|
||||||
|
new("Syncshell permanente : possibilité de nommer et d'organiser vos groupes sur la durée."),
|
||||||
|
new("Interface : palette UmbraSync harmonisée et menus allégés pour l'usage RP."),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly record struct ChangelogEntry(Version Version, string VersionLabel, IReadOnlyList<ChangelogLine> Lines);
|
||||||
|
|
||||||
|
private readonly record struct ChangelogLine(string Text, int IndentLevel = 0, System.Numerics.Vector4? Color = null);
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ using System.Text;
|
|||||||
|
|
||||||
namespace MareSynchronos.UI;
|
namespace MareSynchronos.UI;
|
||||||
|
|
||||||
internal sealed partial class CharaDataHubUi
|
public sealed partial class CharaDataHubUi
|
||||||
{
|
{
|
||||||
private static string GetAccessTypeString(AccessTypeDto dto) => dto switch
|
private static string GetAccessTypeString(AccessTypeDto dto) => dto switch
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ using MareSynchronos.Services.CharaData.Models;
|
|||||||
|
|
||||||
namespace MareSynchronos.UI;
|
namespace MareSynchronos.UI;
|
||||||
|
|
||||||
internal sealed partial class CharaDataHubUi
|
public sealed partial class CharaDataHubUi
|
||||||
{
|
{
|
||||||
private string _joinLobbyId = string.Empty;
|
private string _joinLobbyId = string.Empty;
|
||||||
private void DrawGposeTogether()
|
private void DrawGposeTogether()
|
||||||
@@ -15,14 +15,14 @@ internal sealed partial class CharaDataHubUi
|
|||||||
if (!_charaDataManager.BrioAvailable)
|
if (!_charaDataManager.BrioAvailable)
|
||||||
{
|
{
|
||||||
ImGuiHelpers.ScaledDummy(5);
|
ImGuiHelpers.ScaledDummy(5);
|
||||||
UiSharedService.DrawGroupedCenteredColorText("BRIO IS MANDATORY FOR GPOSE TOGETHER.", ImGuiColors.DalamudRed);
|
UiSharedService.DrawGroupedCenteredColorText("BRIO IS MANDATORY FOR GPOSE TOGETHER.", UiSharedService.AccentColor);
|
||||||
ImGuiHelpers.ScaledDummy(5);
|
ImGuiHelpers.ScaledDummy(5);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_uiSharedService.ApiController.IsConnected)
|
if (!_uiSharedService.ApiController.IsConnected)
|
||||||
{
|
{
|
||||||
ImGuiHelpers.ScaledDummy(5);
|
ImGuiHelpers.ScaledDummy(5);
|
||||||
UiSharedService.DrawGroupedCenteredColorText("CANNOT USE GPOSE TOGETHER WHILE DISCONNECTED FROM THE SERVER.", ImGuiColors.DalamudRed);
|
UiSharedService.DrawGroupedCenteredColorText("CANNOT USE GPOSE TOGETHER WHILE DISCONNECTED FROM THE SERVER.", UiSharedService.AccentColor);
|
||||||
ImGuiHelpers.ScaledDummy(5);
|
ImGuiHelpers.ScaledDummy(5);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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
|
||||||
{
|
{
|
||||||
@@ -165,7 +165,7 @@ internal sealed partial class CharaDataHubUi
|
|||||||
UiSharedService.AttachToolTip(user.WorldDataDescriptor + UiSharedService.TooltipSeparator);
|
UiSharedService.AttachToolTip(user.WorldDataDescriptor + UiSharedService.TooltipSeparator);
|
||||||
|
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
_uiSharedService.IconText(FontAwesomeIcon.Map, sameMapAndServer.SameMap ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed);
|
_uiSharedService.IconText(FontAwesomeIcon.Map, sameMapAndServer.SameMap ? ImGuiColors.ParsedGreen : UiSharedService.AccentColor);
|
||||||
if (ImGui.IsItemClicked(ImGuiMouseButton.Left) && user.WorldData != null)
|
if (ImGui.IsItemClicked(ImGuiMouseButton.Left) && user.WorldData != null)
|
||||||
{
|
{
|
||||||
_dalamudUtilService.SetMarkerAndOpenMap(new(user.WorldData.Value.PositionX, user.WorldData.Value.PositionY, user.WorldData.Value.PositionZ), user.Map);
|
_dalamudUtilService.SetMarkerAndOpenMap(new(user.WorldData.Value.PositionX, user.WorldData.Value.PositionY, user.WorldData.Value.PositionZ), user.Map);
|
||||||
@@ -175,12 +175,12 @@ internal sealed partial class CharaDataHubUi
|
|||||||
+ "Note: For GPose synchronization to work properly, you must be on the same map.");
|
+ "Note: For GPose synchronization to work properly, you must be on the same map.");
|
||||||
|
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
_uiSharedService.IconText(FontAwesomeIcon.Globe, sameMapAndServer.SameServer ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed);
|
_uiSharedService.IconText(FontAwesomeIcon.Globe, sameMapAndServer.SameServer ? ImGuiColors.ParsedGreen : UiSharedService.AccentColor);
|
||||||
UiSharedService.AttachToolTip((sameMapAndServer.SameMap ? "You are on the same server." : "You are not on the same server.") + UiSharedService.TooltipSeparator
|
UiSharedService.AttachToolTip((sameMapAndServer.SameMap ? "You are on the same server." : "You are not on the same server.") + UiSharedService.TooltipSeparator
|
||||||
+ "Note: GPose synchronization is not dependent on the current server, but you will have to spawn a character for the other lobby users.");
|
+ "Note: GPose synchronization is not dependent on the current server, but you will have to spawn a character for the other lobby users.");
|
||||||
|
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
_uiSharedService.IconText(FontAwesomeIcon.Running, sameMapAndServer.SameEverything ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed);
|
_uiSharedService.IconText(FontAwesomeIcon.Running, sameMapAndServer.SameEverything ? ImGuiColors.ParsedGreen : UiSharedService.AccentColor);
|
||||||
UiSharedService.AttachToolTip(sameMapAndServer.SameEverything ? "You are in the same instanced area." : "You are not the same instanced area." + UiSharedService.TooltipSeparator +
|
UiSharedService.AttachToolTip(sameMapAndServer.SameEverything ? "You are in the same instanced area." : "You are not the same instanced area." + UiSharedService.TooltipSeparator +
|
||||||
"Note: Users not in your instance, but on the same map, will be drawn as floating wisps." + Environment.NewLine
|
"Note: Users not in your instance, but on the same map, will be drawn as floating wisps." + Environment.NewLine
|
||||||
+ "Note: GPose synchronization is not dependent on the current instance, but you will have to spawn a character for the other lobby users.");
|
+ "Note: GPose synchronization is not dependent on the current instance, but you will have to spawn a character for the other lobby users.");
|
||||||
@@ -217,7 +217,7 @@ internal sealed partial class CharaDataHubUi
|
|||||||
if (_uiSharedService.IsInGpose && user.Address == nint.Zero)
|
if (_uiSharedService.IsInGpose && user.Address == nint.Zero)
|
||||||
{
|
{
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
_uiSharedService.IconText(FontAwesomeIcon.ExclamationTriangle, ImGuiColors.DalamudRed);
|
_uiSharedService.IconText(FontAwesomeIcon.ExclamationTriangle, UiSharedService.AccentColor);
|
||||||
UiSharedService.AttachToolTip("No valid character assigned for this user. Pose data will not be applied.");
|
UiSharedService.AttachToolTip("No valid character assigned for this user. Pose data will not be applied.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ internal sealed partial class CharaDataHubUi
|
|||||||
if (canUpdate)
|
if (canUpdate)
|
||||||
{
|
{
|
||||||
ImGui.AlignTextToFramePadding();
|
ImGui.AlignTextToFramePadding();
|
||||||
UiSharedService.ColorTextWrapped("Warning: You have unsaved changes!", ImGuiColors.DalamudRed);
|
UiSharedService.ColorTextWrapped("Warning: You have unsaved changes!", UiSharedService.AccentColor);
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
using (ImRaii.Disabled(_charaDataManager.CharaUpdateTask != null && !_charaDataManager.CharaUpdateTask.IsCompleted))
|
using (ImRaii.Disabled(_charaDataManager.CharaUpdateTask != null && !_charaDataManager.CharaUpdateTask.IsCompleted))
|
||||||
{
|
{
|
||||||
@@ -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"))
|
||||||
{
|
{
|
||||||
@@ -216,7 +216,7 @@ internal sealed partial class CharaDataHubUi
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
UiSharedService.ColorTextWrapped($"{dataDto.MissingFiles.DistinctBy(k => k.HashOrFileSwap).Count()} files to download this character data are missing on the server", ImGuiColors.DalamudRed);
|
UiSharedService.ColorTextWrapped($"{dataDto.MissingFiles.DistinctBy(k => k.HashOrFileSwap).Count()} files to download this character data are missing on the server", UiSharedService.AccentColor);
|
||||||
ImGui.NewLine();
|
ImGui.NewLine();
|
||||||
ImGui.SameLine(pos);
|
ImGui.SameLine(pos);
|
||||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowCircleUp, "Attempt to upload missing files and restore Character Data"))
|
if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowCircleUp, "Attempt to upload missing files and restore Character Data"))
|
||||||
@@ -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,13 +395,13 @@ 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)
|
||||||
{
|
{
|
||||||
ImGuiHelpers.ScaledDummy(5);
|
ImGuiHelpers.ScaledDummy(5);
|
||||||
UiSharedService.DrawGroupedCenteredColorText("To attach pose and world data Brio requires to be installed.", ImGuiColors.DalamudRed);
|
UiSharedService.DrawGroupedCenteredColorText("To attach pose and world data Brio requires to be installed.", UiSharedService.AccentColor);
|
||||||
ImGuiHelpers.ScaledDummy(5);
|
ImGuiHelpers.ScaledDummy(5);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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
|
||||||
{
|
{
|
||||||
@@ -544,7 +544,8 @@ internal sealed partial class CharaDataHubUi
|
|||||||
{
|
{
|
||||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowCircleDown, "Download your Online Character Data from Server"))
|
if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowCircleDown, "Download your Online Character Data from Server"))
|
||||||
{
|
{
|
||||||
_ = _charaDataManager.GetAllData(_disposalCts.Token);
|
var cts = EnsureFreshCts(ref _disposalCts);
|
||||||
|
_ = _charaDataManager.GetAllData(cts.Token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (_charaDataManager.DataGetTimeoutTask != null && !_charaDataManager.DataGetTimeoutTask.IsCompleted)
|
if (_charaDataManager.DataGetTimeoutTask != null && !_charaDataManager.DataGetTimeoutTask.IsCompleted)
|
||||||
@@ -585,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
|
||||||
@@ -640,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)
|
||||||
{
|
{
|
||||||
@@ -654,7 +655,8 @@ internal sealed partial class CharaDataHubUi
|
|||||||
{
|
{
|
||||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Plus, "New Character Data Entry"))
|
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Plus, "New Character Data Entry"))
|
||||||
{
|
{
|
||||||
_charaDataManager.CreateCharaDataEntry(_closalCts.Token);
|
var cts = EnsureFreshCts(ref _closalCts);
|
||||||
|
_charaDataManager.CreateCharaDataEntry(cts.Token);
|
||||||
_selectNewEntry = true;
|
_selectNewEntry = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -675,17 +677,17 @@ 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)
|
||||||
{
|
{
|
||||||
var color = _charaDataManager.DataCreationTask.Result.Success ? ImGuiColors.HealerGreen : ImGuiColors.DalamudRed;
|
var color = _charaDataManager.DataCreationTask.Result.Success ? ImGuiColors.HealerGreen : UiSharedService.AccentColor;
|
||||||
UiSharedService.ColorTextWrapped(_charaDataManager.DataCreationTask.Result.Output, color);
|
UiSharedService.ColorTextWrapped(_charaDataManager.DataCreationTask.Result.Output, color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
{
|
{
|
||||||
@@ -57,6 +57,14 @@ internal partial class CharaDataHubUi
|
|||||||
_configService.Save();
|
_configService.Save();
|
||||||
}
|
}
|
||||||
_uiSharedService.DrawHelpText("Draw floating wisps where other's poses are in the world.");
|
_uiSharedService.DrawHelpText("Draw floating wisps where other's poses are in the world.");
|
||||||
|
int maxWisps = _configService.Current.NearbyMaxWisps;
|
||||||
|
ImGui.SetNextItemWidth(140);
|
||||||
|
if (ImGui.SliderInt("Maximum wisps", ref maxWisps, 0, 200))
|
||||||
|
{
|
||||||
|
_configService.Current.NearbyMaxWisps = maxWisps;
|
||||||
|
_configService.Save();
|
||||||
|
}
|
||||||
|
_uiSharedService.DrawHelpText("Limit how many wisps are active at once. Set to 0 to disable wisps even when enabled above.");
|
||||||
int poseDetectionDistance = _configService.Current.NearbyDistanceFilter;
|
int poseDetectionDistance = _configService.Current.NearbyDistanceFilter;
|
||||||
ImGui.SetNextItemWidth(100);
|
ImGui.SetNextItemWidth(100);
|
||||||
if (ImGui.SliderInt("Detection Distance", ref poseDetectionDistance, 5, 1000))
|
if (ImGui.SliderInt("Detection Distance", ref poseDetectionDistance, 5, 1000))
|
||||||
@@ -78,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,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;
|
||||||
@@ -196,7 +204,8 @@ internal partial class CharaDataHubUi
|
|||||||
{
|
{
|
||||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowCircleDown, "Update Data Shared With You"))
|
if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowCircleDown, "Update Data Shared With You"))
|
||||||
{
|
{
|
||||||
_ = _charaDataManager.GetAllSharedData(_disposalCts.Token).ContinueWith(u => UpdateFilteredItems());
|
var cts = EnsureFreshCts(ref _disposalCts);
|
||||||
|
_ = _charaDataManager.GetAllSharedData(cts.Token).ContinueWith(u => UpdateFilteredItems());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (_charaDataManager.GetSharedWithYouTimeoutTask != null && !_charaDataManager.GetSharedWithYouTimeoutTask.IsCompleted)
|
if (_charaDataManager.GetSharedWithYouTimeoutTask != null && !_charaDataManager.GetSharedWithYouTimeoutTask.IsCompleted)
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
using Dalamud.Bindings.ImGui;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Dalamud.Bindings.ImGui;
|
||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
using Dalamud.Interface.Colors;
|
using Dalamud.Interface.Colors;
|
||||||
using Dalamud.Interface.ImGuiFileDialog;
|
using Dalamud.Interface.ImGuiFileDialog;
|
||||||
@@ -15,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;
|
||||||
@@ -30,9 +35,10 @@ 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 CancellationTokenSource _closalCts = new();
|
private readonly McdfShareManager _mcdfShareManager;
|
||||||
|
private CancellationTokenSource? _closalCts = new();
|
||||||
private bool _disableUI = false;
|
private bool _disableUI = false;
|
||||||
private CancellationTokenSource _disposalCts = new();
|
private CancellationTokenSource? _disposalCts = new();
|
||||||
private string _exportDescription = string.Empty;
|
private string _exportDescription = string.Empty;
|
||||||
private string _filterCodeNote = string.Empty;
|
private string _filterCodeNote = string.Empty;
|
||||||
private string _filterDescription = string.Empty;
|
private string _filterDescription = string.Empty;
|
||||||
@@ -62,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;
|
||||||
@@ -73,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();
|
||||||
@@ -92,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) =>
|
||||||
{
|
{
|
||||||
@@ -123,7 +148,14 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_closalCts.Cancel();
|
try
|
||||||
|
{
|
||||||
|
_closalCts?.Cancel();
|
||||||
|
}
|
||||||
|
catch (ObjectDisposedException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
EnsureFreshCts(ref _closalCts);
|
||||||
SelectedDtoId = string.Empty;
|
SelectedDtoId = string.Empty;
|
||||||
_filteredDict = null;
|
_filteredDict = null;
|
||||||
_sharedWithYouOwnerFilter = string.Empty;
|
_sharedWithYouOwnerFilter = string.Empty;
|
||||||
@@ -135,21 +167,34 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
public override void OnOpen()
|
public override void OnOpen()
|
||||||
{
|
{
|
||||||
_closalCts = _closalCts.CancelRecreate();
|
EnsureFreshCts(ref _closalCts);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Dispose(bool disposing)
|
protected override void Dispose(bool disposing)
|
||||||
{
|
{
|
||||||
if (disposing)
|
if (disposing)
|
||||||
{
|
{
|
||||||
_closalCts.CancelDispose();
|
CancelAndDispose(ref _closalCts);
|
||||||
_disposalCts.CancelDispose();
|
CancelAndDispose(ref _disposalCts);
|
||||||
}
|
}
|
||||||
|
|
||||||
base.Dispose(disposing);
|
base.Dispose(disposing);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void DrawInternal()
|
protected override void DrawInternal()
|
||||||
|
{
|
||||||
|
DrawHubContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DrawInline()
|
||||||
|
{
|
||||||
|
using (ImRaii.PushId("CharaDataHubInline"))
|
||||||
|
{
|
||||||
|
DrawHubContent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawHubContent()
|
||||||
{
|
{
|
||||||
if (!_comboHybridUsedLastFrame)
|
if (!_comboHybridUsedLastFrame)
|
||||||
{
|
{
|
||||||
@@ -170,7 +215,7 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
|
|||||||
if (!_charaDataManager.BrioAvailable)
|
if (!_charaDataManager.BrioAvailable)
|
||||||
{
|
{
|
||||||
ImGuiHelpers.ScaledDummy(3);
|
ImGuiHelpers.ScaledDummy(3);
|
||||||
UiSharedService.DrawGroupedCenteredColorText("To utilize any features related to posing or spawning characters you require to have Brio installed.", ImGuiColors.DalamudRed);
|
UiSharedService.DrawGroupedCenteredColorText("To utilize any features related to posing or spawning characters you require to have Brio installed.", UiSharedService.AccentColor);
|
||||||
UiSharedService.DistanceSeparator();
|
UiSharedService.DistanceSeparator();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,18 +235,22 @@ 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)
|
||||||
{
|
{
|
||||||
UiSharedService.ColorTextWrapped("WARNING: During the data application avoid interacting with this actor to prevent potential crashes.", ImGuiColors.DalamudRed);
|
UiSharedService.ColorTextWrapped("WARNING: During the data application avoid interacting with this actor to prevent potential crashes.", UiSharedService.AccentColor);
|
||||||
ImGuiHelpers.ScaledDummy(5);
|
ImGuiHelpers.ScaledDummy(5);
|
||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
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;
|
||||||
@@ -221,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))
|
||||||
@@ -262,6 +315,7 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_charaDataNearbyManager.ComputeNearbyData = false;
|
_charaDataNearbyManager.ComputeNearbyData = false;
|
||||||
@@ -280,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");
|
||||||
|
|
||||||
@@ -306,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.");
|
||||||
@@ -436,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"))
|
||||||
@@ -595,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -644,11 +718,11 @@ 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)
|
||||||
{
|
{
|
||||||
UiSharedService.ColorTextWrapped(_charaDataManager.DownloadMetaInfoTask.Result.Result, ImGuiColors.DalamudRed);
|
UiSharedService.ColorTextWrapped(_charaDataManager.DownloadMetaInfoTask.Result.Result, UiSharedService.AccentColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
using (ImRaii.Disabled(_charaDataManager.LastDownloadedMetaInfo == null))
|
using (ImRaii.Disabled(_charaDataManager.LastDownloadedMetaInfo == null))
|
||||||
@@ -689,7 +763,8 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowCircleDown, "Download your Character Data"))
|
if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowCircleDown, "Download your Character Data"))
|
||||||
{
|
{
|
||||||
_ = _charaDataManager.GetAllData(_disposalCts.Token);
|
var cts = EnsureFreshCts(ref _disposalCts);
|
||||||
|
_ = _charaDataManager.GetAllData(cts.Token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (_charaDataManager.DataGetTimeoutTask != null && !_charaDataManager.DataGetTimeoutTask.IsCompleted)
|
if (_charaDataManager.DataGetTimeoutTask != null && !_charaDataManager.DataGetTimeoutTask.IsCompleted)
|
||||||
@@ -848,14 +923,15 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
|
|||||||
if ((_charaDataManager.LoadedMcdfHeader?.IsFaulted ?? false) || (_charaDataManager.McdfApplicationTask?.IsFaulted ?? false))
|
if ((_charaDataManager.LoadedMcdfHeader?.IsFaulted ?? false) || (_charaDataManager.McdfApplicationTask?.IsFaulted ?? false))
|
||||||
{
|
{
|
||||||
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.",
|
||||||
ImGuiColors.DalamudRed);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -883,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;
|
||||||
@@ -896,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);
|
||||||
@@ -1104,4 +1586,26 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
|
|||||||
drawAction();
|
drawAction();
|
||||||
if (_disableUI) ImGui.BeginDisabled();
|
if (_disableUI) ImGui.BeginDisabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static CancellationTokenSource EnsureFreshCts(ref CancellationTokenSource? cts)
|
||||||
|
{
|
||||||
|
CancelAndDispose(ref cts);
|
||||||
|
cts = new CancellationTokenSource();
|
||||||
|
return cts;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CancelAndDispose(ref CancellationTokenSource? cts)
|
||||||
|
{
|
||||||
|
if (cts == null) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
cts.Cancel();
|
||||||
|
}
|
||||||
|
catch (ObjectDisposedException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
cts.Dispose();
|
||||||
|
cts = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,15 +1,21 @@
|
|||||||
using System.Numerics;
|
using System;
|
||||||
|
using System.Numerics;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Dalamud.Game.Text.SeStringHandling;
|
||||||
using Dalamud.Bindings.ImGui;
|
using Dalamud.Bindings.ImGui;
|
||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
using Dalamud.Interface.Colors;
|
using Dalamud.Interface.Colors;
|
||||||
using Dalamud.Interface.Utility;
|
using Dalamud.Interface.Utility;
|
||||||
|
using MareSynchronos.API.Data;
|
||||||
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.API.Dto.User;
|
|
||||||
using MareSynchronos.PlayerData.Pairs;
|
using MareSynchronos.PlayerData.Pairs;
|
||||||
using MareSynchronos.Services;
|
using MareSynchronos.Services;
|
||||||
|
using MareSynchronos.Services.AutoDetect;
|
||||||
using MareSynchronos.Services.Mediator;
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using MareSynchronos.Services.ServerConfiguration;
|
||||||
using MareSynchronos.UI.Handlers;
|
using MareSynchronos.UI.Handlers;
|
||||||
using MareSynchronos.WebAPI;
|
using MareSynchronos.WebAPI;
|
||||||
|
|
||||||
@@ -21,16 +27,59 @@ public class DrawGroupPair : DrawPairBase
|
|||||||
private readonly GroupPairFullInfoDto _fullInfoDto;
|
private readonly GroupPairFullInfoDto _fullInfoDto;
|
||||||
private readonly GroupFullInfoDto _group;
|
private readonly GroupFullInfoDto _group;
|
||||||
private readonly CharaDataManager _charaDataManager;
|
private readonly CharaDataManager _charaDataManager;
|
||||||
|
private readonly AutoDetectRequestService _autoDetectRequestService;
|
||||||
|
private readonly ServerConfigurationManager _serverConfigurationManager;
|
||||||
|
private const string ManualPairInvitePrefix = "[UmbraPairInvite|";
|
||||||
|
|
||||||
public DrawGroupPair(string id, Pair entry, ApiController apiController,
|
public DrawGroupPair(string id, Pair entry, ApiController apiController,
|
||||||
MareMediator mareMediator, GroupFullInfoDto group, GroupPairFullInfoDto fullInfoDto,
|
MareMediator mareMediator, GroupFullInfoDto group, GroupPairFullInfoDto fullInfoDto,
|
||||||
UidDisplayHandler handler, UiSharedService uiSharedService, CharaDataManager charaDataManager)
|
UidDisplayHandler handler, UiSharedService uiSharedService, CharaDataManager charaDataManager,
|
||||||
|
AutoDetectRequestService autoDetectRequestService, ServerConfigurationManager serverConfigurationManager)
|
||||||
: base(id, entry, apiController, handler, uiSharedService)
|
: base(id, entry, apiController, handler, uiSharedService)
|
||||||
{
|
{
|
||||||
_group = group;
|
_group = group;
|
||||||
_fullInfoDto = fullInfoDto;
|
_fullInfoDto = fullInfoDto;
|
||||||
_mediator = mareMediator;
|
_mediator = mareMediator;
|
||||||
_charaDataManager = charaDataManager;
|
_charaDataManager = charaDataManager;
|
||||||
|
_autoDetectRequestService = autoDetectRequestService;
|
||||||
|
_serverConfigurationManager = serverConfigurationManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override float GetRightSideExtraWidth()
|
||||||
|
{
|
||||||
|
float width = 0f;
|
||||||
|
float spacing = ImGui.GetStyle().ItemSpacing.X;
|
||||||
|
|
||||||
|
var soundsDisabled = _fullInfoDto.GroupUserPermissions.IsDisableSounds();
|
||||||
|
var animDisabled = _fullInfoDto.GroupUserPermissions.IsDisableAnimations();
|
||||||
|
var vfxDisabled = _fullInfoDto.GroupUserPermissions.IsDisableVFX();
|
||||||
|
var individualSoundsDisabled = (_pair.UserPair?.OwnPermissions.IsDisableSounds() ?? false) || (_pair.UserPair?.OtherPermissions.IsDisableSounds() ?? false);
|
||||||
|
var individualAnimDisabled = (_pair.UserPair?.OwnPermissions.IsDisableAnimations() ?? false) || (_pair.UserPair?.OtherPermissions.IsDisableAnimations() ?? false);
|
||||||
|
var individualVFXDisabled = (_pair.UserPair?.OwnPermissions.IsDisableVFX() ?? false) || (_pair.UserPair?.OtherPermissions.IsDisableVFX() ?? false);
|
||||||
|
|
||||||
|
bool showInfo = individualAnimDisabled || individualSoundsDisabled || individualVFXDisabled || animDisabled || soundsDisabled || vfxDisabled;
|
||||||
|
bool showShared = _charaDataManager.SharedWithYouData.TryGetValue(_pair.UserData, out var sharedData);
|
||||||
|
bool showPlus = _pair.UserPair == null && _pair.IsOnline;
|
||||||
|
|
||||||
|
if (showShared)
|
||||||
|
{
|
||||||
|
width += _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Running).X + spacing;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showInfo)
|
||||||
|
{
|
||||||
|
var icon = (individualAnimDisabled || individualSoundsDisabled || individualVFXDisabled)
|
||||||
|
? FontAwesomeIcon.ExclamationTriangle
|
||||||
|
: FontAwesomeIcon.InfoCircle;
|
||||||
|
width += UiSharedService.GetIconSize(icon).X + spacing;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showPlus)
|
||||||
|
{
|
||||||
|
width += _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Plus).X + spacing;
|
||||||
|
}
|
||||||
|
|
||||||
|
return width;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void DrawLeftSide(float textPosY, float originalY)
|
protected override void DrawLeftSide(float textPosY, float originalY)
|
||||||
@@ -152,28 +201,56 @@ public class DrawGroupPair : DrawPairBase
|
|||||||
var individualVFXDisabled = (_pair.UserPair?.OwnPermissions.IsDisableVFX() ?? false) || (_pair.UserPair?.OtherPermissions.IsDisableVFX() ?? false);
|
var individualVFXDisabled = (_pair.UserPair?.OwnPermissions.IsDisableVFX() ?? false) || (_pair.UserPair?.OtherPermissions.IsDisableVFX() ?? false);
|
||||||
|
|
||||||
bool showShared = _charaDataManager.SharedWithYouData.TryGetValue(_pair.UserData, out var sharedData);
|
bool showShared = _charaDataManager.SharedWithYouData.TryGetValue(_pair.UserData, out var sharedData);
|
||||||
bool showInfo = (individualAnimDisabled || individualSoundsDisabled || animDisabled || soundsDisabled);
|
bool showInfo = (individualAnimDisabled || individualSoundsDisabled || individualVFXDisabled || animDisabled || soundsDisabled || vfxDisabled);
|
||||||
bool showPlus = _pair.UserPair == null;
|
bool showPlus = _pair.UserPair == null && _pair.IsOnline;
|
||||||
bool showBars = (userIsOwner || (userIsModerator && !entryIsMod && !entryIsOwner)) || !_pair.IsPaused;
|
bool showBars = (userIsOwner || (userIsModerator && !entryIsMod && !entryIsOwner)) || !_pair.IsPaused;
|
||||||
|
bool showPause = true;
|
||||||
|
|
||||||
var spacing = ImGui.GetStyle().ItemSpacing.X;
|
var spacing = ImGui.GetStyle().ItemSpacing.X;
|
||||||
var permIcon = (individualAnimDisabled || individualSoundsDisabled || individualVFXDisabled) ? FontAwesomeIcon.ExclamationTriangle
|
var permIcon = (individualAnimDisabled || individualSoundsDisabled || individualVFXDisabled) ? FontAwesomeIcon.ExclamationTriangle
|
||||||
: ((soundsDisabled || animDisabled || vfxDisabled) ? FontAwesomeIcon.InfoCircle : FontAwesomeIcon.None);
|
: ((soundsDisabled || animDisabled || vfxDisabled) ? FontAwesomeIcon.InfoCircle : FontAwesomeIcon.None);
|
||||||
var runningIconWidth = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Running).X;
|
var runningIconWidth = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Running).X;
|
||||||
var infoIconWidth = UiSharedService.GetIconSize(permIcon).X;
|
var infoIconWidth = showInfo && permIcon != FontAwesomeIcon.None ? UiSharedService.GetIconSize(permIcon).X : 0f;
|
||||||
var plusButtonWidth = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Plus).X;
|
var plusButtonWidth = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Plus).X;
|
||||||
|
var pauseIcon = _fullInfoDto.GroupUserPermissions.IsPaused() ? FontAwesomeIcon.Play : FontAwesomeIcon.Pause;
|
||||||
|
var pauseButtonWidth = _uiSharedService.GetIconButtonSize(pauseIcon).X;
|
||||||
var barButtonWidth = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Bars).X;
|
var barButtonWidth = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Bars).X;
|
||||||
|
|
||||||
var pos = ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth() + spacing
|
float totalWidth = 0f;
|
||||||
- (showShared ? (runningIconWidth + spacing) : 0)
|
void Accumulate(bool condition, float width)
|
||||||
- (showInfo ? (infoIconWidth + spacing) : 0)
|
{
|
||||||
- (showPlus ? (plusButtonWidth + spacing) : 0)
|
if (!condition || width <= 0f) return;
|
||||||
- (showBars ? (barButtonWidth + spacing) : 0);
|
if (totalWidth > 0f) totalWidth += spacing;
|
||||||
|
totalWidth += width;
|
||||||
|
}
|
||||||
|
|
||||||
ImGui.SameLine(pos);
|
Accumulate(showShared, runningIconWidth);
|
||||||
|
Accumulate(showInfo && infoIconWidth > 0f, infoIconWidth);
|
||||||
|
Accumulate(showPlus, plusButtonWidth);
|
||||||
|
Accumulate(showPause, pauseButtonWidth);
|
||||||
|
if (showBars)
|
||||||
|
{
|
||||||
|
if (totalWidth > 0f) totalWidth += spacing;
|
||||||
|
totalWidth += barButtonWidth;
|
||||||
|
}
|
||||||
|
if (showPause && showBars)
|
||||||
|
{
|
||||||
|
totalWidth -= spacing * 0.5f;
|
||||||
|
if (totalWidth < 0f) totalWidth = 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
float cardPaddingX = UiSharedService.GetCardContentPaddingX();
|
||||||
|
float rightMargin = cardPaddingX + 6f * ImGuiHelpers.GlobalScale;
|
||||||
|
float baseX = MathF.Max(ImGui.GetCursorPosX(),
|
||||||
|
ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth() - rightMargin - totalWidth);
|
||||||
|
float currentX = baseX;
|
||||||
|
|
||||||
|
ImGui.SameLine();
|
||||||
|
ImGui.SetCursorPosX(baseX);
|
||||||
|
|
||||||
if (showShared)
|
if (showShared)
|
||||||
{
|
{
|
||||||
|
ImGui.SetCursorPosY(textPosY);
|
||||||
_uiSharedService.IconText(FontAwesomeIcon.Running);
|
_uiSharedService.IconText(FontAwesomeIcon.Running);
|
||||||
|
|
||||||
UiSharedService.AttachToolTip($"This user has shared {sharedData!.Count} Character Data Sets with you." + UiSharedService.TooltipSeparator
|
UiSharedService.AttachToolTip($"This user has shared {sharedData!.Count} Character Data Sets with you." + UiSharedService.TooltipSeparator
|
||||||
@@ -183,12 +260,15 @@ public class DrawGroupPair : DrawPairBase
|
|||||||
{
|
{
|
||||||
_mediator.Publish(new OpenCharaDataHubWithFilterMessage(_pair.UserData));
|
_mediator.Publish(new OpenCharaDataHubWithFilterMessage(_pair.UserData));
|
||||||
}
|
}
|
||||||
ImGui.SameLine();
|
currentX += runningIconWidth + spacing;
|
||||||
|
ImGui.SetCursorPosX(currentX);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (individualAnimDisabled || individualSoundsDisabled)
|
if (showInfo && infoIconWidth > 0f)
|
||||||
{
|
{
|
||||||
ImGui.SetCursorPosY(textPosY);
|
ImGui.SetCursorPosY(textPosY);
|
||||||
|
if (individualAnimDisabled || individualSoundsDisabled || individualVFXDisabled)
|
||||||
|
{
|
||||||
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudYellow);
|
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudYellow);
|
||||||
_uiSharedService.IconText(permIcon);
|
_uiSharedService.IconText(permIcon);
|
||||||
ImGui.PopStyleColor();
|
ImGui.PopStyleColor();
|
||||||
@@ -201,7 +281,7 @@ public class DrawGroupPair : DrawPairBase
|
|||||||
if (individualSoundsDisabled)
|
if (individualSoundsDisabled)
|
||||||
{
|
{
|
||||||
var userSoundsText = "Sound sync disabled with " + _pair.UserData.AliasOrUID;
|
var userSoundsText = "Sound sync disabled with " + _pair.UserData.AliasOrUID;
|
||||||
_uiSharedService.IconText(FontAwesomeIcon.VolumeOff);
|
_uiSharedService.IconText(FontAwesomeIcon.VolumeMute);
|
||||||
ImGui.SameLine(40 * ImGuiHelpers.GlobalScale);
|
ImGui.SameLine(40 * ImGuiHelpers.GlobalScale);
|
||||||
ImGui.TextUnformatted(userSoundsText);
|
ImGui.TextUnformatted(userSoundsText);
|
||||||
ImGui.NewLine();
|
ImGui.NewLine();
|
||||||
@@ -212,7 +292,7 @@ public class DrawGroupPair : DrawPairBase
|
|||||||
if (individualAnimDisabled)
|
if (individualAnimDisabled)
|
||||||
{
|
{
|
||||||
var userAnimText = "Animation sync disabled with " + _pair.UserData.AliasOrUID;
|
var userAnimText = "Animation sync disabled with " + _pair.UserData.AliasOrUID;
|
||||||
_uiSharedService.IconText(FontAwesomeIcon.Stop);
|
_uiSharedService.IconText(FontAwesomeIcon.WindowClose);
|
||||||
ImGui.SameLine(40 * ImGuiHelpers.GlobalScale);
|
ImGui.SameLine(40 * ImGuiHelpers.GlobalScale);
|
||||||
ImGui.TextUnformatted(userAnimText);
|
ImGui.TextUnformatted(userAnimText);
|
||||||
ImGui.NewLine();
|
ImGui.NewLine();
|
||||||
@@ -223,7 +303,7 @@ public class DrawGroupPair : DrawPairBase
|
|||||||
if (individualVFXDisabled)
|
if (individualVFXDisabled)
|
||||||
{
|
{
|
||||||
var userVFXText = "VFX sync disabled with " + _pair.UserData.AliasOrUID;
|
var userVFXText = "VFX sync disabled with " + _pair.UserData.AliasOrUID;
|
||||||
_uiSharedService.IconText(FontAwesomeIcon.Circle);
|
_uiSharedService.IconText(FontAwesomeIcon.TimesCircle);
|
||||||
ImGui.SameLine(40 * ImGuiHelpers.GlobalScale);
|
ImGui.SameLine(40 * ImGuiHelpers.GlobalScale);
|
||||||
ImGui.TextUnformatted(userVFXText);
|
ImGui.TextUnformatted(userVFXText);
|
||||||
ImGui.NewLine();
|
ImGui.NewLine();
|
||||||
@@ -233,11 +313,9 @@ public class DrawGroupPair : DrawPairBase
|
|||||||
|
|
||||||
ImGui.EndTooltip();
|
ImGui.EndTooltip();
|
||||||
}
|
}
|
||||||
ImGui.SameLine();
|
|
||||||
}
|
}
|
||||||
else if ((animDisabled || soundsDisabled))
|
else
|
||||||
{
|
{
|
||||||
ImGui.SetCursorPosY(textPosY);
|
|
||||||
_uiSharedService.IconText(permIcon);
|
_uiSharedService.IconText(permIcon);
|
||||||
if (ImGui.IsItemHovered())
|
if (ImGui.IsItemHovered())
|
||||||
{
|
{
|
||||||
@@ -248,7 +326,7 @@ public class DrawGroupPair : DrawPairBase
|
|||||||
if (soundsDisabled)
|
if (soundsDisabled)
|
||||||
{
|
{
|
||||||
var userSoundsText = "Sound sync disabled by " + _pair.UserData.AliasOrUID;
|
var userSoundsText = "Sound sync disabled by " + _pair.UserData.AliasOrUID;
|
||||||
_uiSharedService.IconText(FontAwesomeIcon.VolumeOff);
|
_uiSharedService.IconText(FontAwesomeIcon.VolumeMute);
|
||||||
ImGui.SameLine(40 * ImGuiHelpers.GlobalScale);
|
ImGui.SameLine(40 * ImGuiHelpers.GlobalScale);
|
||||||
ImGui.TextUnformatted(userSoundsText);
|
ImGui.TextUnformatted(userSoundsText);
|
||||||
}
|
}
|
||||||
@@ -256,7 +334,7 @@ public class DrawGroupPair : DrawPairBase
|
|||||||
if (animDisabled)
|
if (animDisabled)
|
||||||
{
|
{
|
||||||
var userAnimText = "Animation sync disabled by " + _pair.UserData.AliasOrUID;
|
var userAnimText = "Animation sync disabled by " + _pair.UserData.AliasOrUID;
|
||||||
_uiSharedService.IconText(FontAwesomeIcon.Stop);
|
_uiSharedService.IconText(FontAwesomeIcon.WindowClose);
|
||||||
ImGui.SameLine(40 * ImGuiHelpers.GlobalScale);
|
ImGui.SameLine(40 * ImGuiHelpers.GlobalScale);
|
||||||
ImGui.TextUnformatted(userAnimText);
|
ImGui.TextUnformatted(userAnimText);
|
||||||
}
|
}
|
||||||
@@ -264,14 +342,17 @@ public class DrawGroupPair : DrawPairBase
|
|||||||
if (vfxDisabled)
|
if (vfxDisabled)
|
||||||
{
|
{
|
||||||
var userVFXText = "VFX sync disabled by " + _pair.UserData.AliasOrUID;
|
var userVFXText = "VFX sync disabled by " + _pair.UserData.AliasOrUID;
|
||||||
_uiSharedService.IconText(FontAwesomeIcon.Circle);
|
_uiSharedService.IconText(FontAwesomeIcon.TimesCircle);
|
||||||
ImGui.SameLine(40 * ImGuiHelpers.GlobalScale);
|
ImGui.SameLine(40 * ImGuiHelpers.GlobalScale);
|
||||||
ImGui.TextUnformatted(userVFXText);
|
ImGui.TextUnformatted(userVFXText);
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui.EndTooltip();
|
ImGui.EndTooltip();
|
||||||
}
|
}
|
||||||
ImGui.SameLine();
|
}
|
||||||
|
|
||||||
|
currentX += infoIconWidth + spacing;
|
||||||
|
ImGui.SetCursorPosX(currentX);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showPlus)
|
if (showPlus)
|
||||||
@@ -280,22 +361,43 @@ public class DrawGroupPair : DrawPairBase
|
|||||||
|
|
||||||
if (_uiSharedService.IconButton(FontAwesomeIcon.Plus))
|
if (_uiSharedService.IconButton(FontAwesomeIcon.Plus))
|
||||||
{
|
{
|
||||||
_ = _apiController.UserAddPair(new UserDto(new(_pair.UserData.UID)));
|
var targetUid = _pair.UserData.UID;
|
||||||
|
if (!string.IsNullOrEmpty(targetUid))
|
||||||
|
{
|
||||||
|
_ = SendGroupPairInviteAsync(targetUid, entryUID);
|
||||||
}
|
}
|
||||||
UiSharedService.AttachToolTip("Pair with " + entryUID + " individually");
|
}
|
||||||
ImGui.SameLine();
|
UiSharedService.AttachToolTip(AppendSeenInfo("Send pairing invite to " + entryUID));
|
||||||
|
currentX += plusButtonWidth + spacing;
|
||||||
|
ImGui.SetCursorPosX(currentX);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showPause)
|
||||||
|
{
|
||||||
|
float gapToBars = showBars ? spacing * 0.5f : spacing;
|
||||||
|
ImGui.SetCursorPosY(originalY);
|
||||||
|
if (_uiSharedService.IconButton(pauseIcon))
|
||||||
|
{
|
||||||
|
var newPermissions = _fullInfoDto.GroupUserPermissions ^ GroupUserPermissions.Paused;
|
||||||
|
_fullInfoDto.GroupUserPermissions = newPermissions;
|
||||||
|
_ = _apiController.GroupChangeIndividualPermissionState(new GroupPairUserPermissionDto(_group.Group, _pair.UserData, newPermissions));
|
||||||
|
}
|
||||||
|
|
||||||
|
UiSharedService.AttachToolTip(AppendSeenInfo((_fullInfoDto.GroupUserPermissions.IsPaused() ? "Resume" : "Pause") + " syncing with " + entryUID));
|
||||||
|
currentX += pauseButtonWidth + gapToBars;
|
||||||
|
ImGui.SetCursorPosX(currentX);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showBars)
|
if (showBars)
|
||||||
{
|
{
|
||||||
ImGui.SetCursorPosY(originalY);
|
ImGui.SetCursorPosY(originalY);
|
||||||
|
|
||||||
if (_uiSharedService.IconButton(FontAwesomeIcon.Bars))
|
if (_uiSharedService.IconButton(FontAwesomeIcon.Bars))
|
||||||
{
|
{
|
||||||
ImGui.OpenPopup("Popup");
|
ImGui.OpenPopup("Syncshell Flyout Menu");
|
||||||
}
|
}
|
||||||
|
currentX += barButtonWidth;
|
||||||
|
ImGui.SetCursorPosX(currentX);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ImGui.BeginPopup("Popup"))
|
if (ImGui.BeginPopup("Popup"))
|
||||||
{
|
{
|
||||||
if ((userIsModerator || userIsOwner) && !(entryIsMod || entryIsOwner))
|
if ((userIsModerator || userIsOwner) && !(entryIsMod || entryIsOwner))
|
||||||
@@ -381,6 +483,76 @@ public class DrawGroupPair : DrawPairBase
|
|||||||
ImGui.EndPopup();
|
ImGui.EndPopup();
|
||||||
}
|
}
|
||||||
|
|
||||||
return pos - spacing;
|
return baseX - spacing;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string AppendSeenInfo(string tooltip)
|
||||||
|
{
|
||||||
|
if (_pair.IsVisible) return tooltip;
|
||||||
|
|
||||||
|
var lastSeen = _serverConfigurationManager.GetNameForUid(_pair.UserData.UID);
|
||||||
|
if (string.IsNullOrWhiteSpace(lastSeen)) return tooltip;
|
||||||
|
|
||||||
|
return tooltip + " (Vu sous : " + lastSeen + ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SendGroupPairInviteAsync(string targetUid, string displayName)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var ok = await _autoDetectRequestService.SendDirectUidRequestAsync(targetUid, displayName).ConfigureAwait(false);
|
||||||
|
if (!ok) return;
|
||||||
|
|
||||||
|
await SendManualInviteSignalAsync(targetUid, displayName).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// errors are logged within the request service; ignore here
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SendManualInviteSignalAsync(string targetUid, string displayName)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(_apiController.UID)) return;
|
||||||
|
|
||||||
|
var senderAliasRaw = string.IsNullOrEmpty(_apiController.DisplayName) ? _apiController.UID : _apiController.DisplayName;
|
||||||
|
var senderAlias = EncodeInviteField(senderAliasRaw);
|
||||||
|
var targetDisplay = EncodeInviteField(displayName);
|
||||||
|
var inviteId = Guid.NewGuid().ToString("N");
|
||||||
|
var payloadText = new StringBuilder()
|
||||||
|
.Append(ManualPairInvitePrefix)
|
||||||
|
.Append(_apiController.UID)
|
||||||
|
.Append('|')
|
||||||
|
.Append(senderAlias)
|
||||||
|
.Append('|')
|
||||||
|
.Append(targetUid)
|
||||||
|
.Append('|')
|
||||||
|
.Append(targetDisplay)
|
||||||
|
.Append('|')
|
||||||
|
.Append(inviteId)
|
||||||
|
.Append(']')
|
||||||
|
.ToString();
|
||||||
|
|
||||||
|
var payload = new SeStringBuilder().AddText(payloadText).Build().Encode();
|
||||||
|
var chatMessage = new ChatMessage
|
||||||
|
{
|
||||||
|
SenderName = senderAlias,
|
||||||
|
PayloadContent = payload
|
||||||
|
};
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _apiController.GroupChatSendMsg(new GroupDto(_group.Group), chatMessage).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignore - invite remains tracked locally even if group chat signal fails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string EncodeInviteField(string value)
|
||||||
|
{
|
||||||
|
var bytes = Encoding.UTF8.GetBytes(value ?? string.Empty);
|
||||||
|
return Convert.ToBase64String(bytes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
using Dalamud.Bindings.ImGui;
|
using System;
|
||||||
|
using System.Numerics;
|
||||||
|
using Dalamud.Bindings.ImGui;
|
||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
|
using Dalamud.Interface.Utility;
|
||||||
using MareSynchronos.PlayerData.Pairs;
|
using MareSynchronos.PlayerData.Pairs;
|
||||||
using MareSynchronos.UI.Handlers;
|
using MareSynchronos.UI.Handlers;
|
||||||
using MareSynchronos.WebAPI;
|
using MareSynchronos.WebAPI;
|
||||||
@@ -28,36 +31,75 @@ public abstract class DrawPairBase
|
|||||||
|
|
||||||
public void DrawPairedClient()
|
public void DrawPairedClient()
|
||||||
{
|
{
|
||||||
var originalY = ImGui.GetCursorPosY();
|
var style = ImGui.GetStyle();
|
||||||
var pauseIconSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Play);
|
var padding = style.FramePadding;
|
||||||
var textSize = ImGui.CalcTextSize(_pair.UserData.AliasOrUID);
|
var spacing = style.ItemSpacing;
|
||||||
|
var rowStartCursor = ImGui.GetCursorPos();
|
||||||
|
var rowStartScreen = ImGui.GetCursorScreenPos();
|
||||||
|
|
||||||
var startPos = ImGui.GetCursorStartPos();
|
var pauseButtonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Pause);
|
||||||
|
var playButtonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Play);
|
||||||
|
var menuButtonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Bars);
|
||||||
|
|
||||||
var framePadding = ImGui.GetStyle().FramePadding;
|
float pauseClusterWidth = Math.Max(pauseButtonSize.X, playButtonSize.X);
|
||||||
var lineHeight = textSize.Y + framePadding.Y * 2;
|
float pauseClusterHeight = Math.Max(pauseButtonSize.Y, playButtonSize.Y);
|
||||||
|
float reservedSpacing = style.ItemSpacing.X * 2.4f;
|
||||||
|
float rightButtonWidth =
|
||||||
|
menuButtonSize.X +
|
||||||
|
pauseClusterWidth +
|
||||||
|
reservedSpacing +
|
||||||
|
GetRightSideExtraWidth();
|
||||||
|
|
||||||
var off = startPos.Y;
|
float availableWidth = Math.Max(ImGui.GetContentRegionAvail().X - rightButtonWidth, 1f);
|
||||||
var height = UiSharedService.GetWindowContentRegionHeight();
|
float textHeight = ImGui.GetFontSize();
|
||||||
|
var presenceIconSize = UiSharedService.GetIconSize(FontAwesomeIcon.Moon);
|
||||||
|
float iconHeight = presenceIconSize.Y;
|
||||||
|
float contentHeight = Math.Max(textHeight, Math.Max(iconHeight, pauseClusterHeight));
|
||||||
|
float rowHeight = contentHeight + padding.Y * 2f;
|
||||||
|
float totalHeight = rowHeight + spacing.Y;
|
||||||
|
|
||||||
if ((originalY + off) < -lineHeight || (originalY + off) > height)
|
var origin = ImGui.GetCursorStartPos();
|
||||||
|
var top = origin.Y + rowStartCursor.Y;
|
||||||
|
var bottom = top + totalHeight;
|
||||||
|
var visibleHeight = UiSharedService.GetWindowContentRegionHeight();
|
||||||
|
if (bottom < 0 || top > visibleHeight)
|
||||||
{
|
{
|
||||||
ImGui.Dummy(new System.Numerics.Vector2(0f, lineHeight));
|
ImGui.SetCursorPos(new Vector2(rowStartCursor.X, rowStartCursor.Y + totalHeight));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var textPosY = originalY + pauseIconSize.Y / 2 - textSize.Y / 2;
|
var drawList = ImGui.GetWindowDrawList();
|
||||||
DrawLeftSide(textPosY, originalY);
|
var backgroundColor = new Vector4(0.10f, 0.10f, 0.14f, 0.95f);
|
||||||
|
var borderColor = new Vector4(0f, 0f, 0f, 0.9f);
|
||||||
|
float rounding = Math.Max(style.FrameRounding, 7f * ImGuiHelpers.GlobalScale);
|
||||||
|
|
||||||
|
var panelMin = rowStartScreen + new Vector2(0f, spacing.Y * 0.15f);
|
||||||
|
var panelMax = panelMin + new Vector2(availableWidth, rowHeight - spacing.Y * 0.3f);
|
||||||
|
drawList.AddRectFilled(panelMin, panelMax, ImGui.ColorConvertFloat4ToU32(backgroundColor), rounding);
|
||||||
|
drawList.AddRect(panelMin, panelMax, ImGui.ColorConvertFloat4ToU32(borderColor), rounding);
|
||||||
|
|
||||||
|
float iconTop = rowStartCursor.Y + (rowHeight - iconHeight) / 2f;
|
||||||
|
float textTop = rowStartCursor.Y + (rowHeight - textHeight) / 2f - padding.Y * 0.6f;
|
||||||
|
float buttonTop = rowStartCursor.Y + (rowHeight - pauseClusterHeight) / 2f;
|
||||||
|
|
||||||
|
ImGui.SetCursorPos(new Vector2(rowStartCursor.X + padding.X, iconTop));
|
||||||
|
DrawLeftSide(iconTop, iconTop);
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
|
ImGui.SetCursorPosY(textTop);
|
||||||
var posX = ImGui.GetCursorPosX();
|
var posX = ImGui.GetCursorPosX();
|
||||||
var rightSide = DrawRightSide(textPosY, originalY);
|
var rightSide = DrawRightSide(buttonTop, buttonTop);
|
||||||
DrawName(originalY, posX, rightSide);
|
DrawName(textTop + padding.Y * 0.15f, posX, rightSide);
|
||||||
|
|
||||||
|
ImGui.SetCursorPos(new Vector2(rowStartCursor.X, rowStartCursor.Y + totalHeight));
|
||||||
|
ImGui.SetCursorPosX(rowStartCursor.X);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract void DrawLeftSide(float textPosY, float originalY);
|
protected abstract void DrawLeftSide(float textPosY, float originalY);
|
||||||
|
|
||||||
protected abstract float DrawRightSide(float textPosY, float originalY);
|
protected abstract float DrawRightSide(float textPosY, float originalY);
|
||||||
|
|
||||||
|
protected virtual float GetRightSideExtraWidth() => 0f;
|
||||||
|
|
||||||
private void DrawName(float originalY, float leftSide, float rightSide)
|
private void DrawName(float originalY, float leftSide, float rightSide)
|
||||||
{
|
{
|
||||||
_displayHandler.DrawPairText(_id, _pair, leftSide, originalY, () => rightSide - leftSide);
|
_displayHandler.DrawPairText(_id, _pair, leftSide, originalY, () => rightSide - leftSide);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ using MareSynchronos.Services.Mediator;
|
|||||||
using MareSynchronos.UI.Handlers;
|
using MareSynchronos.UI.Handlers;
|
||||||
using MareSynchronos.WebAPI;
|
using MareSynchronos.WebAPI;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
using MareSynchronos.Services.ServerConfiguration;
|
||||||
|
|
||||||
namespace MareSynchronos.UI.Components;
|
namespace MareSynchronos.UI.Components;
|
||||||
|
|
||||||
@@ -20,10 +21,12 @@ public class DrawUserPair : DrawPairBase
|
|||||||
protected readonly MareMediator _mediator;
|
protected readonly MareMediator _mediator;
|
||||||
private readonly SelectGroupForPairUi _selectGroupForPairUi;
|
private readonly SelectGroupForPairUi _selectGroupForPairUi;
|
||||||
private readonly CharaDataManager _charaDataManager;
|
private readonly CharaDataManager _charaDataManager;
|
||||||
|
private readonly ServerConfigurationManager _serverConfigurationManager;
|
||||||
|
|
||||||
public DrawUserPair(string id, Pair entry, UidDisplayHandler displayHandler, ApiController apiController,
|
public DrawUserPair(string id, Pair entry, UidDisplayHandler displayHandler, ApiController apiController,
|
||||||
MareMediator mareMediator, SelectGroupForPairUi selectGroupForPairUi,
|
MareMediator mareMediator, SelectGroupForPairUi selectGroupForPairUi,
|
||||||
UiSharedService uiSharedService, CharaDataManager charaDataManager)
|
UiSharedService uiSharedService, CharaDataManager charaDataManager,
|
||||||
|
ServerConfigurationManager serverConfigurationManager)
|
||||||
: base(id, entry, apiController, displayHandler, uiSharedService)
|
: base(id, entry, apiController, displayHandler, uiSharedService)
|
||||||
{
|
{
|
||||||
if (_pair.UserPair == null) throw new ArgumentException("Pair must be UserPair", nameof(entry));
|
if (_pair.UserPair == null) throw new ArgumentException("Pair must be UserPair", nameof(entry));
|
||||||
@@ -31,12 +34,35 @@ public class DrawUserPair : DrawPairBase
|
|||||||
_selectGroupForPairUi = selectGroupForPairUi;
|
_selectGroupForPairUi = selectGroupForPairUi;
|
||||||
_mediator = mareMediator;
|
_mediator = mareMediator;
|
||||||
_charaDataManager = charaDataManager;
|
_charaDataManager = charaDataManager;
|
||||||
|
_serverConfigurationManager = serverConfigurationManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsOnline => _pair.IsOnline;
|
public bool IsOnline => _pair.IsOnline;
|
||||||
public bool IsVisible => _pair.IsVisible;
|
public bool IsVisible => _pair.IsVisible;
|
||||||
public UserPairDto UserPair => _pair.UserPair!;
|
public UserPairDto UserPair => _pair.UserPair!;
|
||||||
|
|
||||||
|
protected override float GetRightSideExtraWidth()
|
||||||
|
{
|
||||||
|
float width = 0f;
|
||||||
|
var spacingX = ImGui.GetStyle().ItemSpacing.X;
|
||||||
|
|
||||||
|
var individualSoundsDisabled = (_pair.UserPair?.OwnPermissions.IsDisableSounds() ?? false) || (_pair.UserPair?.OtherPermissions.IsDisableSounds() ?? false);
|
||||||
|
var individualAnimDisabled = (_pair.UserPair?.OwnPermissions.IsDisableAnimations() ?? false) || (_pair.UserPair?.OtherPermissions.IsDisableAnimations() ?? false);
|
||||||
|
var individualVFXDisabled = (_pair.UserPair?.OwnPermissions.IsDisableVFX() ?? false) || (_pair.UserPair?.OtherPermissions.IsDisableVFX() ?? false);
|
||||||
|
|
||||||
|
if (individualSoundsDisabled || individualAnimDisabled || individualVFXDisabled)
|
||||||
|
{
|
||||||
|
width += _uiSharedService.GetIconButtonSize(FontAwesomeIcon.ExclamationTriangle).X + spacingX * 0.5f;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_charaDataManager.SharedWithYouData.TryGetValue(_pair.UserData, out var sharedData))
|
||||||
|
{
|
||||||
|
width += _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Running).X + spacingX * 0.5f;
|
||||||
|
}
|
||||||
|
|
||||||
|
return width;
|
||||||
|
}
|
||||||
|
|
||||||
protected override void DrawLeftSide(float textPosY, float originalY)
|
protected override void DrawLeftSide(float textPosY, float originalY)
|
||||||
{
|
{
|
||||||
var online = _pair.IsOnline;
|
var online = _pair.IsOnline;
|
||||||
@@ -54,7 +80,7 @@ public class DrawUserPair : DrawPairBase
|
|||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
ImGui.SetCursorPosY(textPosY);
|
ImGui.SetCursorPosY(textPosY);
|
||||||
ImGui.PushFont(UiBuilder.IconFont);
|
ImGui.PushFont(UiBuilder.IconFont);
|
||||||
UiSharedService.ColorText(FontAwesomeIcon.ArrowUp.ToIconString(), ImGuiColors.DalamudRed);
|
UiSharedService.ColorText(FontAwesomeIcon.ArrowUp.ToIconString(), UiSharedService.AccentColor);
|
||||||
ImGui.PopFont();
|
ImGui.PopFont();
|
||||||
UiSharedService.AttachToolTip(_pair.UserData.AliasOrUID + " has not added you back");
|
UiSharedService.AttachToolTip(_pair.UserData.AliasOrUID + " has not added you back");
|
||||||
}
|
}
|
||||||
@@ -106,7 +132,8 @@ public class DrawUserPair : DrawPairBase
|
|||||||
var barButtonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Bars);
|
var barButtonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Bars);
|
||||||
var entryUID = _pair.UserData.AliasOrUID;
|
var entryUID = _pair.UserData.AliasOrUID;
|
||||||
var spacingX = ImGui.GetStyle().ItemSpacing.X;
|
var spacingX = ImGui.GetStyle().ItemSpacing.X;
|
||||||
var windowEndX = ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth();
|
var edgePadding = UiSharedService.GetCardContentPaddingX() + 6f * ImGuiHelpers.GlobalScale;
|
||||||
|
var windowEndX = ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth() - edgePadding;
|
||||||
var rightSidePos = windowEndX - barButtonSize.X;
|
var rightSidePos = windowEndX - barButtonSize.X;
|
||||||
|
|
||||||
// Flyout Menu
|
// Flyout Menu
|
||||||
@@ -135,9 +162,9 @@ public class DrawUserPair : DrawPairBase
|
|||||||
perm.SetPaused(!perm.IsPaused());
|
perm.SetPaused(!perm.IsPaused());
|
||||||
_ = _apiController.UserSetPairPermissions(new(_pair.UserData, perm));
|
_ = _apiController.UserSetPairPermissions(new(_pair.UserData, perm));
|
||||||
}
|
}
|
||||||
UiSharedService.AttachToolTip(!_pair.UserPair!.OwnPermissions.IsPaused()
|
UiSharedService.AttachToolTip(AppendSeenInfo(!_pair.UserPair!.OwnPermissions.IsPaused()
|
||||||
? "Pause pairing with " + entryUID
|
? "Pause pairing with " + entryUID
|
||||||
: "Resume pairing with " + entryUID);
|
: "Resume pairing with " + entryUID));
|
||||||
|
|
||||||
|
|
||||||
var individualSoundsDisabled = (_pair.UserPair?.OwnPermissions.IsDisableSounds() ?? false) || (_pair.UserPair?.OtherPermissions.IsDisableSounds() ?? false);
|
var individualSoundsDisabled = (_pair.UserPair?.OwnPermissions.IsDisableSounds() ?? false) || (_pair.UserPair?.OtherPermissions.IsDisableSounds() ?? false);
|
||||||
@@ -163,7 +190,7 @@ public class DrawUserPair : DrawPairBase
|
|||||||
if (individualSoundsDisabled)
|
if (individualSoundsDisabled)
|
||||||
{
|
{
|
||||||
var userSoundsText = "Sound sync disabled with " + _pair.UserData.AliasOrUID;
|
var userSoundsText = "Sound sync disabled with " + _pair.UserData.AliasOrUID;
|
||||||
_uiSharedService.IconText(FontAwesomeIcon.VolumeOff);
|
_uiSharedService.IconText(FontAwesomeIcon.VolumeMute);
|
||||||
ImGui.SameLine(40 * ImGuiHelpers.GlobalScale);
|
ImGui.SameLine(40 * ImGuiHelpers.GlobalScale);
|
||||||
ImGui.TextUnformatted(userSoundsText);
|
ImGui.TextUnformatted(userSoundsText);
|
||||||
ImGui.NewLine();
|
ImGui.NewLine();
|
||||||
@@ -174,7 +201,7 @@ public class DrawUserPair : DrawPairBase
|
|||||||
if (individualAnimDisabled)
|
if (individualAnimDisabled)
|
||||||
{
|
{
|
||||||
var userAnimText = "Animation sync disabled with " + _pair.UserData.AliasOrUID;
|
var userAnimText = "Animation sync disabled with " + _pair.UserData.AliasOrUID;
|
||||||
_uiSharedService.IconText(FontAwesomeIcon.Stop);
|
_uiSharedService.IconText(FontAwesomeIcon.WindowClose);
|
||||||
ImGui.SameLine(40 * ImGuiHelpers.GlobalScale);
|
ImGui.SameLine(40 * ImGuiHelpers.GlobalScale);
|
||||||
ImGui.TextUnformatted(userAnimText);
|
ImGui.TextUnformatted(userAnimText);
|
||||||
ImGui.NewLine();
|
ImGui.NewLine();
|
||||||
@@ -185,7 +212,7 @@ public class DrawUserPair : DrawPairBase
|
|||||||
if (individualVFXDisabled)
|
if (individualVFXDisabled)
|
||||||
{
|
{
|
||||||
var userVFXText = "VFX sync disabled with " + _pair.UserData.AliasOrUID;
|
var userVFXText = "VFX sync disabled with " + _pair.UserData.AliasOrUID;
|
||||||
_uiSharedService.IconText(FontAwesomeIcon.Circle);
|
_uiSharedService.IconText(FontAwesomeIcon.TimesCircle);
|
||||||
ImGui.SameLine(40 * ImGuiHelpers.GlobalScale);
|
ImGui.SameLine(40 * ImGuiHelpers.GlobalScale);
|
||||||
ImGui.TextUnformatted(userVFXText);
|
ImGui.TextUnformatted(userVFXText);
|
||||||
ImGui.NewLine();
|
ImGui.NewLine();
|
||||||
@@ -263,35 +290,38 @@ public class DrawUserPair : DrawPairBase
|
|||||||
{
|
{
|
||||||
_selectGroupForPairUi.Open(entry);
|
_selectGroupForPairUi.Open(entry);
|
||||||
}
|
}
|
||||||
UiSharedService.AttachToolTip("Choose pair groups for " + entryUID);
|
UiSharedService.AttachToolTip(AppendSeenInfo("Choose pair groups for " + entryUID));
|
||||||
|
|
||||||
var isDisableSounds = entry.UserPair!.OwnPermissions.IsDisableSounds();
|
var isDisableSounds = entry.UserPair!.OwnPermissions.IsDisableSounds();
|
||||||
string disableSoundsText = isDisableSounds ? "Enable sound sync" : "Disable sound sync";
|
string disableSoundsText = isDisableSounds ? "Enable sound sync" : "Disable sound sync";
|
||||||
var disableSoundsIcon = isDisableSounds ? FontAwesomeIcon.VolumeUp : FontAwesomeIcon.VolumeMute;
|
var disableSoundsIcon = isDisableSounds ? FontAwesomeIcon.VolumeMute : FontAwesomeIcon.VolumeUp;
|
||||||
if (_uiSharedService.IconTextButton(disableSoundsIcon, disableSoundsText))
|
if (_uiSharedService.IconTextButton(disableSoundsIcon, disableSoundsText))
|
||||||
{
|
{
|
||||||
var permissions = entry.UserPair.OwnPermissions;
|
var permissions = entry.UserPair.OwnPermissions;
|
||||||
permissions.SetDisableSounds(!isDisableSounds);
|
permissions.SetDisableSounds(!isDisableSounds);
|
||||||
|
_mediator.Publish(new PairSyncOverrideChanged(entry.UserData.UID, permissions.IsDisableSounds(), null, null));
|
||||||
_ = _apiController.UserSetPairPermissions(new UserPermissionsDto(entry.UserData, permissions));
|
_ = _apiController.UserSetPairPermissions(new UserPermissionsDto(entry.UserData, permissions));
|
||||||
}
|
}
|
||||||
|
|
||||||
var isDisableAnims = entry.UserPair!.OwnPermissions.IsDisableAnimations();
|
var isDisableAnims = entry.UserPair!.OwnPermissions.IsDisableAnimations();
|
||||||
string disableAnimsText = isDisableAnims ? "Enable animation sync" : "Disable animation sync";
|
string disableAnimsText = isDisableAnims ? "Enable animation sync" : "Disable animation sync";
|
||||||
var disableAnimsIcon = isDisableAnims ? FontAwesomeIcon.Running : FontAwesomeIcon.Stop;
|
var disableAnimsIcon = isDisableAnims ? FontAwesomeIcon.WindowClose : FontAwesomeIcon.Running;
|
||||||
if (_uiSharedService.IconTextButton(disableAnimsIcon, disableAnimsText))
|
if (_uiSharedService.IconTextButton(disableAnimsIcon, disableAnimsText))
|
||||||
{
|
{
|
||||||
var permissions = entry.UserPair.OwnPermissions;
|
var permissions = entry.UserPair.OwnPermissions;
|
||||||
permissions.SetDisableAnimations(!isDisableAnims);
|
permissions.SetDisableAnimations(!isDisableAnims);
|
||||||
|
_mediator.Publish(new PairSyncOverrideChanged(entry.UserData.UID, null, permissions.IsDisableAnimations(), null));
|
||||||
_ = _apiController.UserSetPairPermissions(new UserPermissionsDto(entry.UserData, permissions));
|
_ = _apiController.UserSetPairPermissions(new UserPermissionsDto(entry.UserData, permissions));
|
||||||
}
|
}
|
||||||
|
|
||||||
var isDisableVFX = entry.UserPair!.OwnPermissions.IsDisableVFX();
|
var isDisableVFX = entry.UserPair!.OwnPermissions.IsDisableVFX();
|
||||||
string disableVFXText = isDisableVFX ? "Enable VFX sync" : "Disable VFX sync";
|
string disableVFXText = isDisableVFX ? "Enable VFX sync" : "Disable VFX sync";
|
||||||
var disableVFXIcon = isDisableVFX ? FontAwesomeIcon.Sun : FontAwesomeIcon.Circle;
|
var disableVFXIcon = isDisableVFX ? FontAwesomeIcon.TimesCircle : FontAwesomeIcon.Sun;
|
||||||
if (_uiSharedService.IconTextButton(disableVFXIcon, disableVFXText))
|
if (_uiSharedService.IconTextButton(disableVFXIcon, disableVFXText))
|
||||||
{
|
{
|
||||||
var permissions = entry.UserPair.OwnPermissions;
|
var permissions = entry.UserPair.OwnPermissions;
|
||||||
permissions.SetDisableVFX(!isDisableVFX);
|
permissions.SetDisableVFX(!isDisableVFX);
|
||||||
|
_mediator.Publish(new PairSyncOverrideChanged(entry.UserData.UID, null, null, permissions.IsDisableVFX()));
|
||||||
_ = _apiController.UserSetPairPermissions(new UserPermissionsDto(entry.UserData, permissions));
|
_ = _apiController.UserSetPairPermissions(new UserPermissionsDto(entry.UserData, permissions));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,6 +329,16 @@ public class DrawUserPair : DrawPairBase
|
|||||||
{
|
{
|
||||||
_ = _apiController.UserRemovePair(new(entry.UserData));
|
_ = _apiController.UserRemovePair(new(entry.UserData));
|
||||||
}
|
}
|
||||||
UiSharedService.AttachToolTip("Hold CTRL and click to unpair permanently from " + entryUID);
|
UiSharedService.AttachToolTip(AppendSeenInfo("Hold CTRL and click to unpair permanently from " + entryUID));
|
||||||
|
}
|
||||||
|
|
||||||
|
private string AppendSeenInfo(string tooltip)
|
||||||
|
{
|
||||||
|
if (_pair.IsVisible) return tooltip;
|
||||||
|
|
||||||
|
var lastSeen = _serverConfigurationManager.GetNameForUid(_pair.UserData.UID);
|
||||||
|
if (string.IsNullOrWhiteSpace(lastSeen)) return tooltip;
|
||||||
|
|
||||||
|
return tooltip + " (Vu sous : " + lastSeen + ")";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,11 +12,13 @@ using MareSynchronos.API.Dto.Group;
|
|||||||
using MareSynchronos.MareConfiguration;
|
using MareSynchronos.MareConfiguration;
|
||||||
using MareSynchronos.PlayerData.Pairs;
|
using MareSynchronos.PlayerData.Pairs;
|
||||||
using MareSynchronos.Services;
|
using MareSynchronos.Services;
|
||||||
|
using MareSynchronos.Services.AutoDetect;
|
||||||
using MareSynchronos.Services.Mediator;
|
using MareSynchronos.Services.Mediator;
|
||||||
using MareSynchronos.Services.ServerConfiguration;
|
using MareSynchronos.Services.ServerConfiguration;
|
||||||
using MareSynchronos.UI.Components;
|
using MareSynchronos.UI.Components;
|
||||||
using MareSynchronos.UI.Handlers;
|
using MareSynchronos.UI.Handlers;
|
||||||
using MareSynchronos.WebAPI;
|
using MareSynchronos.WebAPI;
|
||||||
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
|
||||||
@@ -28,9 +30,9 @@ internal sealed class GroupPanel
|
|||||||
private readonly CompactUi _mainUi;
|
private readonly CompactUi _mainUi;
|
||||||
private readonly PairManager _pairManager;
|
private readonly PairManager _pairManager;
|
||||||
private readonly ChatService _chatService;
|
private readonly ChatService _chatService;
|
||||||
private readonly MareConfigService _mareConfig;
|
|
||||||
private readonly ServerConfigurationManager _serverConfigurationManager;
|
private readonly ServerConfigurationManager _serverConfigurationManager;
|
||||||
private readonly CharaDataManager _charaDataManager;
|
private readonly CharaDataManager _charaDataManager;
|
||||||
|
private readonly AutoDetectRequestService _autoDetectRequestService;
|
||||||
private readonly Dictionary<string, bool> _showGidForEntry = new(StringComparer.Ordinal);
|
private readonly Dictionary<string, bool> _showGidForEntry = new(StringComparer.Ordinal);
|
||||||
private readonly UidDisplayHandler _uidDisplayHandler;
|
private readonly UidDisplayHandler _uidDisplayHandler;
|
||||||
private readonly UiSharedService _uiShared;
|
private readonly UiSharedService _uiShared;
|
||||||
@@ -40,6 +42,7 @@ internal sealed class GroupPanel
|
|||||||
private string _editGroupComment = string.Empty;
|
private string _editGroupComment = string.Empty;
|
||||||
private string _editGroupEntry = string.Empty;
|
private string _editGroupEntry = string.Empty;
|
||||||
private bool _errorGroupCreate = false;
|
private bool _errorGroupCreate = false;
|
||||||
|
private string _errorGroupCreateMessage = string.Empty;
|
||||||
private bool _errorGroupJoin;
|
private bool _errorGroupJoin;
|
||||||
private bool _isPasswordValid;
|
private bool _isPasswordValid;
|
||||||
private GroupPasswordDto? _lastCreatedGroup = null;
|
private GroupPasswordDto? _lastCreatedGroup = null;
|
||||||
@@ -53,27 +56,42 @@ internal sealed class GroupPanel
|
|||||||
private bool _showModalCreateGroup;
|
private bool _showModalCreateGroup;
|
||||||
private bool _showModalEnterPassword;
|
private bool _showModalEnterPassword;
|
||||||
private string _newSyncShellAlias = string.Empty;
|
private string _newSyncShellAlias = string.Empty;
|
||||||
|
private bool _createIsTemporary = false;
|
||||||
|
private int _tempSyncshellDurationHours = 24;
|
||||||
|
private readonly int[] _temporaryDurationOptions = new[]
|
||||||
|
{
|
||||||
|
1,
|
||||||
|
12,
|
||||||
|
24,
|
||||||
|
48,
|
||||||
|
72,
|
||||||
|
96,
|
||||||
|
120,
|
||||||
|
144,
|
||||||
|
168
|
||||||
|
};
|
||||||
private string _syncShellPassword = string.Empty;
|
private string _syncShellPassword = string.Empty;
|
||||||
private string _syncShellToJoin = string.Empty;
|
private string _syncShellToJoin = string.Empty;
|
||||||
|
|
||||||
public GroupPanel(CompactUi mainUi, UiSharedService uiShared, PairManager pairManager, ChatService chatServivce,
|
public GroupPanel(CompactUi mainUi, UiSharedService uiShared, PairManager pairManager, ChatService chatServivce,
|
||||||
UidDisplayHandler uidDisplayHandler, MareConfigService mareConfig, ServerConfigurationManager serverConfigurationManager,
|
UidDisplayHandler uidDisplayHandler, ServerConfigurationManager serverConfigurationManager,
|
||||||
CharaDataManager charaDataManager)
|
CharaDataManager charaDataManager, AutoDetectRequestService autoDetectRequestService)
|
||||||
{
|
{
|
||||||
_mainUi = mainUi;
|
_mainUi = mainUi;
|
||||||
_uiShared = uiShared;
|
_uiShared = uiShared;
|
||||||
_pairManager = pairManager;
|
_pairManager = pairManager;
|
||||||
_chatService = chatServivce;
|
_chatService = chatServivce;
|
||||||
_uidDisplayHandler = uidDisplayHandler;
|
_uidDisplayHandler = uidDisplayHandler;
|
||||||
_mareConfig = mareConfig;
|
|
||||||
_serverConfigurationManager = serverConfigurationManager;
|
_serverConfigurationManager = serverConfigurationManager;
|
||||||
_charaDataManager = charaDataManager;
|
_charaDataManager = charaDataManager;
|
||||||
|
_autoDetectRequestService = autoDetectRequestService;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ApiController ApiController => _uiShared.ApiController;
|
private ApiController ApiController => _uiShared.ApiController;
|
||||||
|
|
||||||
public void DrawSyncshells()
|
public void DrawSyncshells()
|
||||||
{
|
{
|
||||||
|
using var fontScale = UiSharedService.PushFontScale(UiSharedService.ContentFontScale);
|
||||||
using (ImRaii.PushId("addsyncshell")) DrawAddSyncshell();
|
using (ImRaii.PushId("addsyncshell")) DrawAddSyncshell();
|
||||||
using (ImRaii.PushId("syncshelllist")) DrawSyncshellList();
|
using (ImRaii.PushId("syncshelllist")) DrawSyncshellList();
|
||||||
_mainUi.TransferPartHeight = ImGui.GetCursorPosY();
|
_mainUi.TransferPartHeight = ImGui.GetCursorPosY();
|
||||||
@@ -110,6 +128,9 @@ internal sealed class GroupPanel
|
|||||||
_lastCreatedGroup = null;
|
_lastCreatedGroup = null;
|
||||||
_errorGroupCreate = false;
|
_errorGroupCreate = false;
|
||||||
_newSyncShellAlias = string.Empty;
|
_newSyncShellAlias = string.Empty;
|
||||||
|
_createIsTemporary = false;
|
||||||
|
_tempSyncshellDurationHours = 24;
|
||||||
|
_errorGroupCreateMessage = string.Empty;
|
||||||
_showModalCreateGroup = true;
|
_showModalCreateGroup = true;
|
||||||
ImGui.OpenPopup("Create Syncshell");
|
ImGui.OpenPopup("Create Syncshell");
|
||||||
}
|
}
|
||||||
@@ -151,15 +172,77 @@ internal sealed class GroupPanel
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (ImGui.BeginPopupModal("Create Syncshell", ref _showModalCreateGroup, UiSharedService.PopupWindowFlags))
|
if (ImGui.BeginPopupModal("Create Syncshell", ref _showModalCreateGroup, UiSharedService.PopupWindowFlags))
|
||||||
|
{
|
||||||
|
UiSharedService.TextWrapped("Choisissez le type de Syncshell à créer.");
|
||||||
|
bool showPermanent = !_createIsTemporary;
|
||||||
|
if (ImGui.RadioButton("Permanente", showPermanent))
|
||||||
|
{
|
||||||
|
_createIsTemporary = false;
|
||||||
|
}
|
||||||
|
ImGui.SameLine();
|
||||||
|
if (ImGui.RadioButton("Temporaire", _createIsTemporary))
|
||||||
|
{
|
||||||
|
_createIsTemporary = true;
|
||||||
|
_newSyncShellAlias = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_createIsTemporary)
|
||||||
{
|
{
|
||||||
UiSharedService.TextWrapped("Donnez un nom à votre Syncshell (optionnel) puis créez-la.");
|
UiSharedService.TextWrapped("Donnez un nom à votre Syncshell (optionnel) puis créez-la.");
|
||||||
ImGui.SetNextItemWidth(-1);
|
ImGui.SetNextItemWidth(-1);
|
||||||
ImGui.InputTextWithHint("##syncshellalias", "Nom du Syncshell", ref _newSyncShellAlias, 50);
|
ImGui.InputTextWithHint("##syncshellalias", "Nom du Syncshell", ref _newSyncShellAlias, 50);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_newSyncShellAlias = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_createIsTemporary)
|
||||||
|
{
|
||||||
|
UiSharedService.TextWrapped("Durée maximale d'une Syncshell temporaire : 7 jours.");
|
||||||
|
if (_tempSyncshellDurationHours > 168) _tempSyncshellDurationHours = 168;
|
||||||
|
for (int i = 0; i < _temporaryDurationOptions.Length; i++)
|
||||||
|
{
|
||||||
|
var option = _temporaryDurationOptions[i];
|
||||||
|
var isSelected = _tempSyncshellDurationHours == option;
|
||||||
|
string label = option switch
|
||||||
|
{
|
||||||
|
>= 24 when option % 24 == 0 => option == 24 ? "24h" : $"{option / 24}j",
|
||||||
|
_ => option + "h"
|
||||||
|
};
|
||||||
|
|
||||||
|
if (ImGui.RadioButton(label, isSelected))
|
||||||
|
{
|
||||||
|
_tempSyncshellDurationHours = option;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start a new line after every 3 buttons
|
||||||
|
if ((i + 1) % 3 == 0)
|
||||||
|
{
|
||||||
|
ImGui.NewLine();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ImGui.SameLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var expiresLocal = DateTime.Now.AddHours(_tempSyncshellDurationHours);
|
||||||
|
UiSharedService.TextWrapped($"Expiration le {expiresLocal:g} (heure locale).");
|
||||||
|
}
|
||||||
|
|
||||||
UiSharedService.TextWrapped("Appuyez sur le bouton ci-dessous pour créer une nouvelle Syncshell.");
|
UiSharedService.TextWrapped("Appuyez sur le bouton ci-dessous pour créer une nouvelle Syncshell.");
|
||||||
ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale);
|
ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale);
|
||||||
if (ImGui.Button("Create Syncshell"))
|
if (ImGui.Button("Create Syncshell"))
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
{
|
||||||
|
if (_createIsTemporary)
|
||||||
|
{
|
||||||
|
var expiresAtUtc = DateTime.UtcNow.AddHours(_tempSyncshellDurationHours);
|
||||||
|
_lastCreatedGroup = ApiController.GroupCreateTemporary(expiresAtUtc).Result;
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
var aliasInput = string.IsNullOrWhiteSpace(_newSyncShellAlias) ? null : _newSyncShellAlias.Trim();
|
var aliasInput = string.IsNullOrWhiteSpace(_newSyncShellAlias) ? null : _newSyncShellAlias.Trim();
|
||||||
_lastCreatedGroup = ApiController.GroupCreate(aliasInput).Result;
|
_lastCreatedGroup = ApiController.GroupCreate(aliasInput).Result;
|
||||||
@@ -168,10 +251,19 @@ internal sealed class GroupPanel
|
|||||||
_newSyncShellAlias = string.Empty;
|
_newSyncShellAlias = string.Empty;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch
|
}
|
||||||
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_lastCreatedGroup = null;
|
_lastCreatedGroup = null;
|
||||||
_errorGroupCreate = true;
|
_errorGroupCreate = true;
|
||||||
|
if (ex.Message.Contains("name is already in use", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
_errorGroupCreateMessage = "Le nom de la Syncshell est déjà utilisé.";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_errorGroupCreateMessage = ex.Message;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,6 +271,7 @@ internal sealed class GroupPanel
|
|||||||
{
|
{
|
||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
_errorGroupCreate = false;
|
_errorGroupCreate = false;
|
||||||
|
_errorGroupCreateMessage = string.Empty;
|
||||||
if (!string.IsNullOrWhiteSpace(_lastCreatedGroup.Group.Alias))
|
if (!string.IsNullOrWhiteSpace(_lastCreatedGroup.Group.Alias))
|
||||||
{
|
{
|
||||||
ImGui.TextUnformatted("Syncshell Name: " + _lastCreatedGroup.Group.Alias);
|
ImGui.TextUnformatted("Syncshell Name: " + _lastCreatedGroup.Group.Alias);
|
||||||
@@ -192,12 +285,19 @@ internal sealed class GroupPanel
|
|||||||
ImGui.SetClipboardText(_lastCreatedGroup.Password);
|
ImGui.SetClipboardText(_lastCreatedGroup.Password);
|
||||||
}
|
}
|
||||||
UiSharedService.TextWrapped("You can change the Syncshell password later at any time.");
|
UiSharedService.TextWrapped("You can change the Syncshell password later at any time.");
|
||||||
|
if (_lastCreatedGroup.IsTemporary && _lastCreatedGroup.ExpiresAt != null)
|
||||||
|
{
|
||||||
|
var expiresLocal = _lastCreatedGroup.ExpiresAt.Value.ToLocalTime();
|
||||||
|
UiSharedService.TextWrapped($"Cette Syncshell expirera le {expiresLocal:g} (heure locale).");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_errorGroupCreate)
|
if (_errorGroupCreate)
|
||||||
{
|
{
|
||||||
UiSharedService.ColorTextWrapped("You are already owner of the maximum amount of Syncshells (3) or joined the maximum amount of Syncshells (6). Relinquish ownership of your own Syncshells to someone else or leave existing Syncshells.",
|
var msg = string.IsNullOrWhiteSpace(_errorGroupCreateMessage)
|
||||||
new Vector4(1, 0, 0, 1));
|
? "You are already owner of the maximum amount of Syncshells (3) or joined the maximum amount of Syncshells (6). Relinquish ownership of your own Syncshells to someone else or leave existing Syncshells."
|
||||||
|
: _errorGroupCreateMessage;
|
||||||
|
UiSharedService.ColorTextWrapped(msg, new Vector4(1, 0, 0, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
UiSharedService.SetScaledWindowSize(350);
|
UiSharedService.SetScaledWindowSize(350);
|
||||||
@@ -212,19 +312,20 @@ internal sealed class GroupPanel
|
|||||||
int shellNumber = _serverConfigurationManager.GetShellNumberForGid(groupDto.GID);
|
int shellNumber = _serverConfigurationManager.GetShellNumberForGid(groupDto.GID);
|
||||||
|
|
||||||
var name = groupDto.Group.Alias ?? groupDto.GID;
|
var name = groupDto.Group.Alias ?? groupDto.GID;
|
||||||
if (!_expandedGroupState.TryGetValue(groupDto.GID, out bool isExpanded))
|
if (!_expandedGroupState.ContainsKey(groupDto.GID))
|
||||||
{
|
{
|
||||||
isExpanded = false;
|
_expandedGroupState[groupDto.GID] = false;
|
||||||
_expandedGroupState.Add(groupDto.GID, isExpanded);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var icon = isExpanded ? FontAwesomeIcon.CaretSquareDown : FontAwesomeIcon.CaretSquareRight;
|
UiSharedService.DrawCard($"syncshell-card-{groupDto.GID}", () =>
|
||||||
_uiShared.IconText(icon);
|
|
||||||
if (ImGui.IsItemClicked(ImGuiMouseButton.Left))
|
|
||||||
{
|
{
|
||||||
_expandedGroupState[groupDto.GID] = !_expandedGroupState[groupDto.GID];
|
bool expandedState = _expandedGroupState[groupDto.GID];
|
||||||
|
UiSharedService.DrawArrowToggle(ref expandedState, $"##syncshell-toggle-{groupDto.GID}");
|
||||||
|
if (expandedState != _expandedGroupState[groupDto.GID])
|
||||||
|
{
|
||||||
|
_expandedGroupState[groupDto.GID] = expandedState;
|
||||||
}
|
}
|
||||||
ImGui.SameLine();
|
ImGui.SameLine(0f, 6f * ImGuiHelpers.GlobalScale);
|
||||||
|
|
||||||
var textIsGid = true;
|
var textIsGid = true;
|
||||||
string groupName = groupDto.GroupAliasOrGID;
|
string groupName = groupDto.GroupAliasOrGID;
|
||||||
@@ -257,11 +358,13 @@ internal sealed class GroupPanel
|
|||||||
if (!string.Equals(_editGroupEntry, groupDto.GID, StringComparison.Ordinal))
|
if (!string.Equals(_editGroupEntry, groupDto.GID, StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
var shellConfig = _serverConfigurationManager.GetShellConfigForGid(groupDto.GID);
|
var shellConfig = _serverConfigurationManager.GetShellConfigForGid(groupDto.GID);
|
||||||
if (!_mareConfig.Current.DisableSyncshellChat && shellConfig.Enabled)
|
var totalMembers = pairsInGroup.Count + 1;
|
||||||
{
|
var connectedMembers = pairsInGroup.Count(p => p.IsOnline) + 1;
|
||||||
ImGui.TextUnformatted($"[{shellNumber}]");
|
var maxCapacity = ApiController.ServerInfo.MaxGroupUserCount;
|
||||||
UiSharedService.AttachToolTip("Chat command prefix: /ss" + shellNumber);
|
ImGui.TextUnformatted($"{connectedMembers}/{totalMembers}");
|
||||||
}
|
UiSharedService.AttachToolTip("Membres connectés / membres totaux" + Environment.NewLine +
|
||||||
|
$"Capacité maximale : {maxCapacity}" + Environment.NewLine +
|
||||||
|
"Syncshell ID: " + groupDto.Group.GID);
|
||||||
if (textIsGid) ImGui.PushFont(UiBuilder.MonoFont);
|
if (textIsGid) ImGui.PushFont(UiBuilder.MonoFont);
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
ImGui.TextUnformatted(groupName);
|
ImGui.TextUnformatted(groupName);
|
||||||
@@ -269,6 +372,20 @@ internal sealed class GroupPanel
|
|||||||
UiSharedService.AttachToolTip("Left click to switch between GID display and comment" + Environment.NewLine +
|
UiSharedService.AttachToolTip("Left click to switch between GID display and comment" + Environment.NewLine +
|
||||||
"Right click to change comment for " + groupName + Environment.NewLine + Environment.NewLine
|
"Right click to change comment for " + groupName + Environment.NewLine + Environment.NewLine
|
||||||
+ "Users: " + (pairsInGroup.Count + 1) + ", Owner: " + groupDto.OwnerAliasOrUID);
|
+ "Users: " + (pairsInGroup.Count + 1) + ", Owner: " + groupDto.OwnerAliasOrUID);
|
||||||
|
if (groupDto.IsTemporary)
|
||||||
|
{
|
||||||
|
ImGui.SameLine();
|
||||||
|
UiSharedService.ColorText("(Temp)", ImGuiColors.DalamudOrange);
|
||||||
|
if (groupDto.ExpiresAt != null)
|
||||||
|
{
|
||||||
|
var tempExpireLocal = groupDto.ExpiresAt.Value.ToLocalTime();
|
||||||
|
UiSharedService.AttachToolTip($"Expire le {tempExpireLocal:g}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
UiSharedService.AttachToolTip("Syncshell temporaire");
|
||||||
|
}
|
||||||
|
}
|
||||||
if (ImGui.IsItemClicked(ImGuiMouseButton.Left))
|
if (ImGui.IsItemClicked(ImGuiMouseButton.Left))
|
||||||
{
|
{
|
||||||
var prevState = textIsGid;
|
var prevState = textIsGid;
|
||||||
@@ -430,7 +547,7 @@ internal sealed class GroupPanel
|
|||||||
bool hideOfflineUsers = pairsInGroup.Count > 1000;
|
bool hideOfflineUsers = pairsInGroup.Count > 1000;
|
||||||
|
|
||||||
ImGui.Indent(20);
|
ImGui.Indent(20);
|
||||||
if (_expandedGroupState[groupDto.GID])
|
if (expandedState)
|
||||||
{
|
{
|
||||||
var sortedPairs = pairsInGroup
|
var sortedPairs = pairsInGroup
|
||||||
.OrderByDescending(u => string.Equals(u.UserData.UID, groupDto.OwnerUID, StringComparison.Ordinal))
|
.OrderByDescending(u => string.Equals(u.UserData.UID, groupDto.OwnerUID, StringComparison.Ordinal))
|
||||||
@@ -452,7 +569,9 @@ internal sealed class GroupPanel
|
|||||||
).Value,
|
).Value,
|
||||||
_uidDisplayHandler,
|
_uidDisplayHandler,
|
||||||
_uiShared,
|
_uiShared,
|
||||||
_charaDataManager);
|
_charaDataManager,
|
||||||
|
_autoDetectRequestService,
|
||||||
|
_serverConfigurationManager);
|
||||||
|
|
||||||
if (pair.IsVisible)
|
if (pair.IsVisible)
|
||||||
visibleUsers.Add(drawPair);
|
visibleUsers.Add(drawPair);
|
||||||
@@ -495,6 +614,9 @@ internal sealed class GroupPanel
|
|||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
}
|
}
|
||||||
ImGui.Unindent(20);
|
ImGui.Unindent(20);
|
||||||
|
}, stretchWidth: true);
|
||||||
|
|
||||||
|
ImGuiHelpers.ScaledDummy(4f);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawSyncShellButtons(GroupFullInfoDto groupDto, List<Pair> groupPairs)
|
private void DrawSyncShellButtons(GroupFullInfoDto groupDto, List<Pair> groupPairs)
|
||||||
@@ -513,19 +635,20 @@ internal sealed class GroupPanel
|
|||||||
bool showInfoIcon = !invitesEnabled || soundsDisabled || animDisabled || vfxDisabled || userSoundsDisabled || userAnimDisabled || userVFXDisabled;
|
bool showInfoIcon = !invitesEnabled || soundsDisabled || animDisabled || vfxDisabled || userSoundsDisabled || userAnimDisabled || userVFXDisabled;
|
||||||
|
|
||||||
var lockedIcon = invitesEnabled ? FontAwesomeIcon.LockOpen : FontAwesomeIcon.Lock;
|
var lockedIcon = invitesEnabled ? FontAwesomeIcon.LockOpen : FontAwesomeIcon.Lock;
|
||||||
var animIcon = animDisabled ? FontAwesomeIcon.Stop : FontAwesomeIcon.Running;
|
var animIcon = animDisabled ? FontAwesomeIcon.WindowClose : FontAwesomeIcon.Running;
|
||||||
var soundsIcon = soundsDisabled ? FontAwesomeIcon.VolumeOff : FontAwesomeIcon.VolumeUp;
|
var soundsIcon = soundsDisabled ? FontAwesomeIcon.VolumeMute : FontAwesomeIcon.VolumeUp;
|
||||||
var vfxIcon = vfxDisabled ? FontAwesomeIcon.Circle : FontAwesomeIcon.Sun;
|
var vfxIcon = vfxDisabled ? FontAwesomeIcon.TimesCircle : FontAwesomeIcon.Sun;
|
||||||
var userAnimIcon = userAnimDisabled ? FontAwesomeIcon.Stop : FontAwesomeIcon.Running;
|
var userAnimIcon = userAnimDisabled ? FontAwesomeIcon.WindowClose : FontAwesomeIcon.Running;
|
||||||
var userSoundsIcon = userSoundsDisabled ? FontAwesomeIcon.VolumeOff : FontAwesomeIcon.VolumeUp;
|
var userSoundsIcon = userSoundsDisabled ? FontAwesomeIcon.VolumeMute : FontAwesomeIcon.VolumeUp;
|
||||||
var userVFXIcon = userVFXDisabled ? FontAwesomeIcon.Circle : FontAwesomeIcon.Sun;
|
var userVFXIcon = userVFXDisabled ? FontAwesomeIcon.TimesCircle : FontAwesomeIcon.Sun;
|
||||||
|
|
||||||
var iconSize = UiSharedService.GetIconSize(infoIcon);
|
var iconSize = UiSharedService.GetIconSize(infoIcon);
|
||||||
var barbuttonSize = _uiShared.GetIconButtonSize(FontAwesomeIcon.Bars);
|
var barbuttonSize = _uiShared.GetIconButtonSize(FontAwesomeIcon.Bars);
|
||||||
var isOwner = string.Equals(groupDto.OwnerUID, ApiController.UID, StringComparison.Ordinal);
|
var isOwner = string.Equals(groupDto.OwnerUID, ApiController.UID, StringComparison.Ordinal);
|
||||||
|
|
||||||
var spacingX = ImGui.GetStyle().ItemSpacing.X;
|
var spacingX = ImGui.GetStyle().ItemSpacing.X;
|
||||||
var windowEndX = ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth();
|
var cardPaddingX = UiSharedService.GetCardContentPaddingX();
|
||||||
|
var windowEndX = ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth() - cardPaddingX - 6f * ImGuiHelpers.GlobalScale;
|
||||||
var pauseIcon = groupDto.GroupUserPermissions.IsPaused() ? FontAwesomeIcon.Play : FontAwesomeIcon.Pause;
|
var pauseIcon = groupDto.GroupUserPermissions.IsPaused() ? FontAwesomeIcon.Play : FontAwesomeIcon.Pause;
|
||||||
var pauseIconSize = _uiShared.GetIconButtonSize(pauseIcon);
|
var pauseIconSize = _uiShared.GetIconButtonSize(pauseIcon);
|
||||||
|
|
||||||
@@ -655,6 +778,7 @@ internal sealed class GroupPanel
|
|||||||
ImGui.CloseCurrentPopup();
|
ImGui.CloseCurrentPopup();
|
||||||
var perm = groupDto.GroupUserPermissions;
|
var perm = groupDto.GroupUserPermissions;
|
||||||
perm.SetDisableSounds(!perm.IsDisableSounds());
|
perm.SetDisableSounds(!perm.IsDisableSounds());
|
||||||
|
_mainUi.Mediator.Publish(new GroupSyncOverrideChanged(groupDto.Group.GID, perm.IsDisableSounds(), null, null));
|
||||||
_ = ApiController.GroupChangeIndividualPermissionState(new(groupDto.Group, new UserData(ApiController.UID), perm));
|
_ = ApiController.GroupChangeIndividualPermissionState(new(groupDto.Group, new UserData(ApiController.UID), perm));
|
||||||
}
|
}
|
||||||
UiSharedService.AttachToolTip("Sets your allowance for sound synchronization for users of this syncshell."
|
UiSharedService.AttachToolTip("Sets your allowance for sound synchronization for users of this syncshell."
|
||||||
@@ -668,6 +792,7 @@ internal sealed class GroupPanel
|
|||||||
ImGui.CloseCurrentPopup();
|
ImGui.CloseCurrentPopup();
|
||||||
var perm = groupDto.GroupUserPermissions;
|
var perm = groupDto.GroupUserPermissions;
|
||||||
perm.SetDisableAnimations(!perm.IsDisableAnimations());
|
perm.SetDisableAnimations(!perm.IsDisableAnimations());
|
||||||
|
_mainUi.Mediator.Publish(new GroupSyncOverrideChanged(groupDto.Group.GID, null, perm.IsDisableAnimations(), null));
|
||||||
_ = ApiController.GroupChangeIndividualPermissionState(new(groupDto.Group, new UserData(ApiController.UID), perm));
|
_ = ApiController.GroupChangeIndividualPermissionState(new(groupDto.Group, new UserData(ApiController.UID), perm));
|
||||||
}
|
}
|
||||||
UiSharedService.AttachToolTip("Sets your allowance for animations synchronization for users of this syncshell."
|
UiSharedService.AttachToolTip("Sets your allowance for animations synchronization for users of this syncshell."
|
||||||
@@ -682,6 +807,7 @@ internal sealed class GroupPanel
|
|||||||
ImGui.CloseCurrentPopup();
|
ImGui.CloseCurrentPopup();
|
||||||
var perm = groupDto.GroupUserPermissions;
|
var perm = groupDto.GroupUserPermissions;
|
||||||
perm.SetDisableVFX(!perm.IsDisableVFX());
|
perm.SetDisableVFX(!perm.IsDisableVFX());
|
||||||
|
_mainUi.Mediator.Publish(new GroupSyncOverrideChanged(groupDto.Group.GID, null, null, perm.IsDisableVFX()));
|
||||||
_ = ApiController.GroupChangeIndividualPermissionState(new(groupDto.Group, new UserData(ApiController.UID), perm));
|
_ = ApiController.GroupChangeIndividualPermissionState(new(groupDto.Group, new UserData(ApiController.UID), perm));
|
||||||
}
|
}
|
||||||
UiSharedService.AttachToolTip("Sets your allowance for VFX synchronization for users of this syncshell."
|
UiSharedService.AttachToolTip("Sets your allowance for VFX synchronization for users of this syncshell."
|
||||||
@@ -706,9 +832,22 @@ internal sealed class GroupPanel
|
|||||||
|
|
||||||
private void DrawSyncshellList()
|
private void DrawSyncshellList()
|
||||||
{
|
{
|
||||||
var ySize = _mainUi.TransferPartHeight == 0
|
float availableHeight = ImGui.GetContentRegionAvail().Y;
|
||||||
? 1
|
float ySize;
|
||||||
: (ImGui.GetWindowContentRegionMax().Y - ImGui.GetWindowContentRegionMin().Y) - _mainUi.TransferPartHeight - ImGui.GetCursorPosY();
|
if (_mainUi.TransferPartHeight <= 0)
|
||||||
|
{
|
||||||
|
float reserve = ImGui.GetFrameHeightWithSpacing() * 2f;
|
||||||
|
ySize = availableHeight - reserve;
|
||||||
|
if (ySize <= 0)
|
||||||
|
{
|
||||||
|
ySize = System.Math.Max(availableHeight, 1f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ySize = (ImGui.GetWindowContentRegionMax().Y - ImGui.GetWindowContentRegionMin().Y) - _mainUi.TransferPartHeight - ImGui.GetCursorPosY();
|
||||||
|
}
|
||||||
|
|
||||||
ImGui.BeginChild("list", new Vector2(_mainUi.WindowContentWidth, ySize), border: false);
|
ImGui.BeginChild("list", new Vector2(_mainUi.WindowContentWidth, ySize), border: false);
|
||||||
foreach (var entry in _pairManager.GroupPairs.OrderBy(g => g.Key.Group.AliasOrGID, StringComparer.OrdinalIgnoreCase).ToList())
|
foreach (var entry in _pairManager.GroupPairs.OrderBy(g => g.Key.Group.AliasOrGID, StringComparer.OrdinalIgnoreCase).ToList())
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
using Dalamud.Bindings.ImGui;
|
using Dalamud.Bindings.ImGui;
|
||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
|
using Dalamud.Interface.Utility;
|
||||||
using Dalamud.Interface.Utility.Raii;
|
using Dalamud.Interface.Utility.Raii;
|
||||||
using MareSynchronos.API.Data.Extensions;
|
using MareSynchronos.API.Data.Extensions;
|
||||||
using MareSynchronos.MareConfiguration;
|
using MareSynchronos.MareConfiguration;
|
||||||
using MareSynchronos.UI.Handlers;
|
using MareSynchronos.UI.Handlers;
|
||||||
using MareSynchronos.WebAPI;
|
using MareSynchronos.WebAPI;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
namespace MareSynchronos.UI.Components;
|
namespace MareSynchronos.UI.Components;
|
||||||
|
|
||||||
@@ -28,7 +32,7 @@ public class PairGroupsUi
|
|||||||
_uiSharedService = uiSharedService;
|
_uiSharedService = uiSharedService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Draw<T>(List<T> visibleUsers, List<T> onlineUsers, List<T> offlineUsers) where T : DrawPairBase
|
public void Draw<T>(List<T> visibleUsers, List<T> onlineUsers, List<T> offlineUsers, Action? drawVisibleExtras = null) where T : DrawPairBase
|
||||||
{
|
{
|
||||||
// Only render those tags that actually have pairs in them, otherwise
|
// Only render those tags that actually have pairs in them, otherwise
|
||||||
// we can end up with a bunch of useless pair groups
|
// we can end up with a bunch of useless pair groups
|
||||||
@@ -36,7 +40,7 @@ public class PairGroupsUi
|
|||||||
var allUsers = onlineUsers.Concat(offlineUsers).ToList();
|
var allUsers = onlineUsers.Concat(offlineUsers).ToList();
|
||||||
if (typeof(T) == typeof(DrawUserPair))
|
if (typeof(T) == typeof(DrawUserPair))
|
||||||
{
|
{
|
||||||
DrawUserPairs(tagsWithPairsInThem, allUsers.Cast<DrawUserPair>().ToList(), visibleUsers.Cast<DrawUserPair>(), onlineUsers.Cast<DrawUserPair>(), offlineUsers.Cast<DrawUserPair>());
|
DrawUserPairs(tagsWithPairsInThem, allUsers.Cast<DrawUserPair>().ToList(), visibleUsers.Cast<DrawUserPair>(), onlineUsers.Cast<DrawUserPair>(), offlineUsers.Cast<DrawUserPair>(), drawVisibleExtras);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,14 +48,15 @@ public class PairGroupsUi
|
|||||||
{
|
{
|
||||||
var allArePaused = availablePairsInThisTag.All(pair => pair.UserPair!.OwnPermissions.IsPaused());
|
var allArePaused = availablePairsInThisTag.All(pair => pair.UserPair!.OwnPermissions.IsPaused());
|
||||||
var pauseButton = allArePaused ? FontAwesomeIcon.Play : FontAwesomeIcon.Pause;
|
var pauseButton = allArePaused ? FontAwesomeIcon.Play : FontAwesomeIcon.Pause;
|
||||||
var flyoutMenuX = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Bars).X;
|
var flyoutMenuSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Bars);
|
||||||
var pauseButtonX = _uiSharedService.GetIconButtonSize(pauseButton).X;
|
var pauseButtonSize = _uiSharedService.GetIconButtonSize(pauseButton);
|
||||||
var windowX = ImGui.GetWindowContentRegionMin().X;
|
|
||||||
var windowWidth = UiSharedService.GetWindowContentRegionWidth();
|
|
||||||
var spacingX = ImGui.GetStyle().ItemSpacing.X;
|
var spacingX = ImGui.GetStyle().ItemSpacing.X;
|
||||||
|
var currentX = ImGui.GetCursorPosX();
|
||||||
|
var availableWidth = ImGui.GetContentRegionAvail().X;
|
||||||
|
var buttonsWidth = pauseButtonSize.X + flyoutMenuSize.X + spacingX;
|
||||||
|
var pauseStart = Math.Max(currentX, currentX + availableWidth - buttonsWidth);
|
||||||
|
|
||||||
var buttonPauseOffset = windowX + windowWidth - flyoutMenuX - spacingX - pauseButtonX;
|
ImGui.SameLine(pauseStart);
|
||||||
ImGui.SameLine(buttonPauseOffset);
|
|
||||||
if (_uiSharedService.IconButton(pauseButton))
|
if (_uiSharedService.IconButton(pauseButton))
|
||||||
{
|
{
|
||||||
if (allArePaused)
|
if (allArePaused)
|
||||||
@@ -72,8 +77,8 @@ public class PairGroupsUi
|
|||||||
UiSharedService.AttachToolTip($"Pause pairing with all pairs in {tag}");
|
UiSharedService.AttachToolTip($"Pause pairing with all pairs in {tag}");
|
||||||
}
|
}
|
||||||
|
|
||||||
var buttonDeleteOffset = windowX + windowWidth - flyoutMenuX;
|
var menuStart = Math.Max(pauseStart + pauseButtonSize.X + spacingX, currentX);
|
||||||
ImGui.SameLine(buttonDeleteOffset);
|
ImGui.SameLine(menuStart);
|
||||||
if (_uiSharedService.IconButton(FontAwesomeIcon.Bars))
|
if (_uiSharedService.IconButton(FontAwesomeIcon.Bars))
|
||||||
{
|
{
|
||||||
ImGui.OpenPopup("Group Flyout Menu");
|
ImGui.OpenPopup("Group Flyout Menu");
|
||||||
@@ -86,7 +91,7 @@ public class PairGroupsUi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawCategory(string tag, IEnumerable<DrawPairBase> onlineUsers, IEnumerable<DrawPairBase> allUsers, IEnumerable<DrawPairBase>? visibleUsers = null)
|
private void DrawCategory(string tag, IEnumerable<DrawPairBase> onlineUsers, IEnumerable<DrawPairBase> allUsers, IEnumerable<DrawPairBase>? visibleUsers = null, Action? drawExtraContent = null)
|
||||||
{
|
{
|
||||||
IEnumerable<DrawPairBase> usersInThisTag;
|
IEnumerable<DrawPairBase> usersInThisTag;
|
||||||
HashSet<string>? otherUidsTaggedWithTag = null;
|
HashSet<string>? otherUidsTaggedWithTag = null;
|
||||||
@@ -108,26 +113,25 @@ public class PairGroupsUi
|
|||||||
|
|
||||||
if (isSpecialTag && !usersInThisTag.Any()) return;
|
if (isSpecialTag && !usersInThisTag.Any()) return;
|
||||||
|
|
||||||
|
UiSharedService.DrawCard($"pair-group-{tag}", () =>
|
||||||
|
{
|
||||||
DrawName(tag, isSpecialTag, visibleInThisTag, usersInThisTag.Count(), otherUidsTaggedWithTag?.Count);
|
DrawName(tag, isSpecialTag, visibleInThisTag, usersInThisTag.Count(), otherUidsTaggedWithTag?.Count);
|
||||||
if (!isSpecialTag)
|
if (!isSpecialTag)
|
||||||
{
|
{
|
||||||
using (ImRaii.PushId($"group-{tag}-buttons")) DrawButtons(tag, allUsers.Cast<DrawUserPair>().Where(p => otherUidsTaggedWithTag!.Contains(p.UID)).ToList());
|
using (ImRaii.PushId($"group-{tag}-buttons")) DrawButtons(tag, allUsers.Cast<DrawUserPair>().Where(p => otherUidsTaggedWithTag!.Contains(p.UID)).ToList());
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
if (!_tagHandler.IsTagOpen(tag))
|
|
||||||
{
|
|
||||||
var size = ImGui.CalcTextSize("").Y + ImGui.GetStyle().FramePadding.Y * 2f;
|
|
||||||
ImGui.SameLine();
|
|
||||||
ImGui.Dummy(new(size, size));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_tagHandler.IsTagOpen(tag)) return;
|
if (!_tagHandler.IsTagOpen(tag)) return;
|
||||||
|
|
||||||
ImGui.Indent(20);
|
ImGuiHelpers.ScaledDummy(4f);
|
||||||
|
var indent = 18f * ImGuiHelpers.GlobalScale;
|
||||||
|
ImGui.Indent(indent);
|
||||||
DrawPairs(tag, usersInThisTag);
|
DrawPairs(tag, usersInThisTag);
|
||||||
ImGui.Unindent(20);
|
drawExtraContent?.Invoke();
|
||||||
|
ImGui.Unindent(indent);
|
||||||
|
}, stretchWidth: true);
|
||||||
|
|
||||||
|
ImGuiHelpers.ScaledDummy(4f);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawGroupMenu(string tag)
|
private void DrawGroupMenu(string tag)
|
||||||
@@ -157,17 +161,21 @@ public class PairGroupsUi
|
|||||||
};
|
};
|
||||||
|
|
||||||
string resultFolderName = !isSpecialTag ? $"{displayedName} ({visible}/{online}/{total} Pairs)" : $"{displayedName} ({online} Pairs)";
|
string resultFolderName = !isSpecialTag ? $"{displayedName} ({visible}/{online}/{total} Pairs)" : $"{displayedName} ({online} Pairs)";
|
||||||
var icon = _tagHandler.IsTagOpen(tag) ? FontAwesomeIcon.CaretSquareDown : FontAwesomeIcon.CaretSquareRight;
|
bool isOpen = _tagHandler.IsTagOpen(tag);
|
||||||
_uiSharedService.IconText(icon);
|
bool previousState = isOpen;
|
||||||
if (ImGui.IsItemClicked(ImGuiMouseButton.Left))
|
UiSharedService.DrawArrowToggle(ref isOpen, $"##group-toggle-{tag}");
|
||||||
|
if (isOpen != previousState)
|
||||||
{
|
{
|
||||||
ToggleTagOpen(tag);
|
_tagHandler.SetTagOpen(tag, isOpen);
|
||||||
}
|
}
|
||||||
ImGui.SameLine();
|
ImGui.SameLine(0f, 6f * ImGuiHelpers.GlobalScale);
|
||||||
|
ImGui.AlignTextToFramePadding();
|
||||||
ImGui.TextUnformatted(resultFolderName);
|
ImGui.TextUnformatted(resultFolderName);
|
||||||
if (ImGui.IsItemClicked(ImGuiMouseButton.Left))
|
if (ImGui.IsItemClicked(ImGuiMouseButton.Left))
|
||||||
{
|
{
|
||||||
ToggleTagOpen(tag);
|
bool newState = !_tagHandler.IsTagOpen(tag);
|
||||||
|
_tagHandler.SetTagOpen(tag, newState);
|
||||||
|
isOpen = newState;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isSpecialTag && ImGui.IsItemHovered())
|
if (!isSpecialTag && ImGui.IsItemHovered())
|
||||||
@@ -186,15 +194,11 @@ public class PairGroupsUi
|
|||||||
{
|
{
|
||||||
// These are all the OtherUIDs that are tagged with this tag
|
// These are all the OtherUIDs that are tagged with this tag
|
||||||
_uidDisplayHandler.RenderPairList(availablePairsInThisCategory);
|
_uidDisplayHandler.RenderPairList(availablePairsInThisCategory);
|
||||||
ImGui.Separator();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawUserPairs(List<string> tagsWithPairsInThem, List<DrawUserPair> allUsers, IEnumerable<DrawUserPair> visibleUsers, IEnumerable<DrawUserPair> onlineUsers, IEnumerable<DrawUserPair> offlineUsers)
|
private void DrawUserPairs(List<string> tagsWithPairsInThem, List<DrawUserPair> allUsers, IEnumerable<DrawUserPair> visibleUsers, IEnumerable<DrawUserPair> onlineUsers, IEnumerable<DrawUserPair> offlineUsers, Action? drawVisibleExtras)
|
||||||
{
|
{
|
||||||
if (_mareConfig.Current.ShowVisibleUsersSeparately)
|
// Visible section intentionally omitted for Individual Pairs view.
|
||||||
{
|
|
||||||
using (ImRaii.PushId("$group-VisibleCustomTag")) DrawCategory(TagHandler.CustomVisibleTag, visibleUsers, allUsers);
|
|
||||||
}
|
|
||||||
foreach (var tag in tagsWithPairsInThem)
|
foreach (var tag in tagsWithPairsInThem)
|
||||||
{
|
{
|
||||||
if (_mareConfig.Current.ShowOfflineUsersSeparately)
|
if (_mareConfig.Current.ShowOfflineUsersSeparately)
|
||||||
@@ -242,9 +246,4 @@ public class PairGroupsUi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ToggleTagOpen(string tag)
|
|
||||||
{
|
|
||||||
bool open = !_tagHandler.IsTagOpen(tag);
|
|
||||||
_tagHandler.SetTagOpen(tag, open);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -35,7 +35,7 @@ internal class ReportPopupHandler : IPopupHandler
|
|||||||
UiSharedService.TextWrapped($"Note: Sending a report will disable the offending profile globally.{Environment.NewLine}" +
|
UiSharedService.TextWrapped($"Note: Sending a report will disable the offending profile globally.{Environment.NewLine}" +
|
||||||
$"The report will be sent to the team of your currently connected server.{Environment.NewLine}" +
|
$"The report will be sent to the team of your currently connected server.{Environment.NewLine}" +
|
||||||
$"Depending on the severity of the offense the users profile or account can be permanently disabled or banned.");
|
$"Depending on the severity of the offense the users profile or account can be permanently disabled or banned.");
|
||||||
UiSharedService.ColorTextWrapped("Report spam and wrong reports will not be tolerated and can lead to permanent account suspension.", ImGuiColors.DalamudRed);
|
UiSharedService.ColorTextWrapped("Report spam and wrong reports will not be tolerated and can lead to permanent account suspension.", UiSharedService.AccentColor);
|
||||||
UiSharedService.ColorTextWrapped("This is not for reporting misbehavior but solely for the actual profile. " +
|
UiSharedService.ColorTextWrapped("This is not for reporting misbehavior but solely for the actual profile. " +
|
||||||
"Reports that are not solely for the profile will be ignored.", ImGuiColors.DalamudYellow);
|
"Reports that are not solely for the profile will be ignored.", ImGuiColors.DalamudYellow);
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ using MareSynchronos.Services.Mediator;
|
|||||||
using MareSynchronos.Utils;
|
using MareSynchronos.Utils;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
using System;
|
||||||
|
|
||||||
namespace MareSynchronos.UI;
|
namespace MareSynchronos.UI;
|
||||||
|
|
||||||
@@ -20,7 +21,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
|||||||
private readonly UiSharedService _uiSharedService;
|
private readonly UiSharedService _uiSharedService;
|
||||||
private readonly Dictionary<string, string[]> _texturesToConvert = new(StringComparer.Ordinal);
|
private readonly Dictionary<string, string[]> _texturesToConvert = new(StringComparer.Ordinal);
|
||||||
private Dictionary<ObjectKind, Dictionary<string, CharacterAnalyzer.FileDataEntry>>? _cachedAnalysis;
|
private Dictionary<ObjectKind, Dictionary<string, CharacterAnalyzer.FileDataEntry>>? _cachedAnalysis;
|
||||||
private CancellationTokenSource _conversionCancellationTokenSource = new();
|
private CancellationTokenSource? _conversionCancellationTokenSource = new();
|
||||||
private string _conversionCurrentFileName = string.Empty;
|
private string _conversionCurrentFileName = string.Empty;
|
||||||
private int _conversionCurrentFileProgress = 0;
|
private int _conversionCurrentFileProgress = 0;
|
||||||
private Task? _conversionTask;
|
private Task? _conversionTask;
|
||||||
@@ -64,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)
|
||||||
{
|
{
|
||||||
@@ -74,7 +88,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
|||||||
UiSharedService.TextWrapped("Current file: " + _conversionCurrentFileName);
|
UiSharedService.TextWrapped("Current file: " + _conversionCurrentFileName);
|
||||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.StopCircle, "Cancel conversion"))
|
if (_uiSharedService.IconTextButton(FontAwesomeIcon.StopCircle, "Cancel conversion"))
|
||||||
{
|
{
|
||||||
_conversionCancellationTokenSource.Cancel();
|
TryCancel(_conversionCancellationTokenSource);
|
||||||
}
|
}
|
||||||
UiSharedService.SetScaledWindowSize(500);
|
UiSharedService.SetScaledWindowSize(500);
|
||||||
ImGui.EndPopup();
|
ImGui.EndPopup();
|
||||||
@@ -115,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();
|
||||||
@@ -126,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);
|
||||||
@@ -165,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)
|
||||||
@@ -179,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)
|
||||||
{
|
{
|
||||||
@@ -212,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)
|
||||||
@@ -242,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);
|
||||||
@@ -283,19 +303,19 @@ 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!", ImGuiColors.DalamudRed);
|
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." +
|
||||||
Environment.NewLine + "- Some textures, especially ones utilizing colorsets, might not be suited for BC7 conversion and might produce visual artifacts." +
|
Environment.NewLine + "- Some textures, especially ones utilizing colorsets, might not be suited for BC7 conversion and might produce visual artifacts." +
|
||||||
Environment.NewLine + "- Before converting textures, make sure to have the original files of the mod you are converting so you can reimport it in case of issues." +
|
Environment.NewLine + "- 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)"))
|
||||||
{
|
{
|
||||||
_conversionCancellationTokenSource = _conversionCancellationTokenSource.CancelRecreate();
|
var conversionCts = EnsureFreshCts(ref _conversionCancellationTokenSource);
|
||||||
_conversionTask = _ipcManager.Penumbra.ConvertTextureFiles(_logger, _texturesToConvert, _conversionProgress, _conversionCancellationTokenSource.Token);
|
_conversionTask = _ipcManager.Penumbra.ConvertTextureFiles(_logger, _texturesToConvert, _conversionProgress, conversionCts.Token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -305,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))
|
||||||
{
|
{
|
||||||
@@ -354,10 +377,15 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
protected override void Dispose(bool disposing)
|
protected override void Dispose(bool disposing)
|
||||||
{
|
{
|
||||||
base.Dispose(disposing);
|
if (disposing)
|
||||||
|
{
|
||||||
|
CancelAndDispose(ref _conversionCancellationTokenSource);
|
||||||
_conversionProgress.ProgressChanged -= ConversionProgress_ProgressChanged;
|
_conversionProgress.ProgressChanged -= ConversionProgress_ProgressChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
base.Dispose(disposing);
|
||||||
|
}
|
||||||
|
|
||||||
private void ConversionProgress_ProgressChanged(object? sender, (string, int) e)
|
private void ConversionProgress_ProgressChanged(object? sender, (string, int) e)
|
||||||
{
|
{
|
||||||
_conversionCurrentFileName = e.Item1;
|
_conversionCurrentFileName = e.Item1;
|
||||||
@@ -434,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;
|
||||||
@@ -449,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))
|
||||||
@@ -489,4 +517,31 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static CancellationTokenSource EnsureFreshCts(ref CancellationTokenSource? cts)
|
||||||
|
{
|
||||||
|
CancelAndDispose(ref cts);
|
||||||
|
cts = new CancellationTokenSource();
|
||||||
|
return cts;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CancelAndDispose(ref CancellationTokenSource? cts)
|
||||||
|
{
|
||||||
|
if (cts == null) return;
|
||||||
|
TryCancel(cts);
|
||||||
|
cts.Dispose();
|
||||||
|
cts = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TryCancel(CancellationTokenSource? cts)
|
||||||
|
{
|
||||||
|
if (cts == null) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
cts.Cancel();
|
||||||
|
}
|
||||||
|
catch (ObjectDisposedException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -2,9 +2,11 @@
|
|||||||
using Dalamud.Game.Text.SeStringHandling;
|
using Dalamud.Game.Text.SeStringHandling;
|
||||||
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
|
using Dalamud.Interface;
|
||||||
using MareSynchronos.MareConfiguration;
|
using MareSynchronos.MareConfiguration;
|
||||||
using MareSynchronos.MareConfiguration.Configurations;
|
using MareSynchronos.MareConfiguration.Configurations;
|
||||||
using MareSynchronos.PlayerData.Pairs;
|
using MareSynchronos.PlayerData.Pairs;
|
||||||
|
using MareSynchronos.Services.AutoDetect;
|
||||||
using MareSynchronos.Services.Mediator;
|
using MareSynchronos.Services.Mediator;
|
||||||
using MareSynchronos.WebAPI;
|
using MareSynchronos.WebAPI;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
@@ -15,6 +17,7 @@ namespace MareSynchronos.UI;
|
|||||||
|
|
||||||
public sealed class DtrEntry : IDisposable, IHostedService
|
public sealed class DtrEntry : IDisposable, IHostedService
|
||||||
{
|
{
|
||||||
|
public const string DefaultGlyph = "\u25CB";
|
||||||
private enum DtrStyle
|
private enum DtrStyle
|
||||||
{
|
{
|
||||||
Default,
|
Default,
|
||||||
@@ -39,12 +42,13 @@ public sealed class DtrEntry : IDisposable, IHostedService
|
|||||||
private readonly ILogger<DtrEntry> _logger;
|
private readonly ILogger<DtrEntry> _logger;
|
||||||
private readonly MareMediator _mareMediator;
|
private readonly MareMediator _mareMediator;
|
||||||
private readonly PairManager _pairManager;
|
private readonly PairManager _pairManager;
|
||||||
|
private readonly NearbyPendingService _nearbyPendingService;
|
||||||
private Task? _runTask;
|
private Task? _runTask;
|
||||||
private string? _text;
|
private string? _text;
|
||||||
private string? _tooltip;
|
private string? _tooltip;
|
||||||
private Colors _colors;
|
private Colors _colors;
|
||||||
|
|
||||||
public DtrEntry(ILogger<DtrEntry> logger, IDtrBar dtrBar, MareConfigService configService, MareMediator mareMediator, PairManager pairManager, ApiController apiController)
|
public DtrEntry(ILogger<DtrEntry> logger, IDtrBar dtrBar, MareConfigService configService, MareMediator mareMediator, PairManager pairManager, ApiController apiController, NearbyPendingService nearbyPendingService)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_dtrBar = dtrBar;
|
_dtrBar = dtrBar;
|
||||||
@@ -53,6 +57,7 @@ public sealed class DtrEntry : IDisposable, IHostedService
|
|||||||
_mareMediator = mareMediator;
|
_mareMediator = mareMediator;
|
||||||
_pairManager = pairManager;
|
_pairManager = pairManager;
|
||||||
_apiController = apiController;
|
_apiController = apiController;
|
||||||
|
_nearbyPendingService = nearbyPendingService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
@@ -145,8 +150,9 @@ public sealed class DtrEntry : IDisposable, IHostedService
|
|||||||
if (_apiController.IsConnected)
|
if (_apiController.IsConnected)
|
||||||
{
|
{
|
||||||
var pairCount = _pairManager.GetVisibleUserCount();
|
var pairCount = _pairManager.GetVisibleUserCount();
|
||||||
|
var baseText = RenderDtrStyle(_configService.Current.DtrStyle, pairCount.ToString());
|
||||||
text = RenderDtrStyle(_configService.Current.DtrStyle, pairCount.ToString());
|
var pendingNearby = _nearbyPendingService.Pending.Count;
|
||||||
|
text = pendingNearby > 0 ? $"{baseText} ({pendingNearby})" : baseText;
|
||||||
if (pairCount > 0)
|
if (pairCount > 0)
|
||||||
{
|
{
|
||||||
IEnumerable<string> visiblePairs;
|
IEnumerable<string> visiblePairs;
|
||||||
@@ -163,12 +169,21 @@ public sealed class DtrEntry : IDisposable, IHostedService
|
|||||||
.Select(x => string.Format("{0}", _configService.Current.PreferNoteInDtrTooltip ? x.GetNoteOrName() : x.PlayerName));
|
.Select(x => string.Format("{0}", _configService.Current.PreferNoteInDtrTooltip ? x.GetNoteOrName() : x.PlayerName));
|
||||||
}
|
}
|
||||||
|
|
||||||
tooltip = $"Umbra: Connected{Environment.NewLine}----------{Environment.NewLine}{string.Join(Environment.NewLine, visiblePairs)}";
|
tooltip = $"Umbra: Connected";
|
||||||
|
if (pendingNearby > 0)
|
||||||
|
{
|
||||||
|
tooltip += $"{Environment.NewLine}Invitation en attente : {pendingNearby}";
|
||||||
|
}
|
||||||
|
tooltip += $"{Environment.NewLine}----------{Environment.NewLine}{string.Join(Environment.NewLine, visiblePairs)}";
|
||||||
colors = _configService.Current.DtrColorsPairsInRange;
|
colors = _configService.Current.DtrColorsPairsInRange;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
tooltip = "Umbra: Connected";
|
tooltip = "Umbra: Connected";
|
||||||
|
if (pendingNearby > 0)
|
||||||
|
{
|
||||||
|
tooltip += $"{Environment.NewLine}Invitation en attente : {pendingNearby}";
|
||||||
|
}
|
||||||
colors = _configService.Current.DtrColorsDefault;
|
colors = _configService.Current.DtrColorsDefault;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -196,7 +211,8 @@ public sealed class DtrEntry : IDisposable, IHostedService
|
|||||||
{
|
{
|
||||||
var style = (DtrStyle)styleNum;
|
var style = (DtrStyle)styleNum;
|
||||||
|
|
||||||
return style switch {
|
return style switch
|
||||||
|
{
|
||||||
DtrStyle.Style1 => $"\xE039 {text}",
|
DtrStyle.Style1 => $"\xE039 {text}",
|
||||||
DtrStyle.Style2 => $"\xE0BC {text}",
|
DtrStyle.Style2 => $"\xE0BC {text}",
|
||||||
DtrStyle.Style3 => $"\xE0BD {text}",
|
DtrStyle.Style3 => $"\xE0BD {text}",
|
||||||
@@ -206,7 +222,7 @@ public sealed class DtrEntry : IDisposable, IHostedService
|
|||||||
DtrStyle.Style7 => $"\xE05D {text}",
|
DtrStyle.Style7 => $"\xE05D {text}",
|
||||||
DtrStyle.Style8 => $"\xE03C{text}",
|
DtrStyle.Style8 => $"\xE03C{text}",
|
||||||
DtrStyle.Style9 => $"\xE040 {text} \xE041",
|
DtrStyle.Style9 => $"\xE040 {text} \xE041",
|
||||||
_ => $"\uE044 {text}"
|
_ => DefaultGlyph + " " + text
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ using MareSynchronos.Services.ServerConfiguration;
|
|||||||
using MareSynchronos.Utils;
|
using MareSynchronos.Utils;
|
||||||
using MareSynchronos.WebAPI;
|
using MareSynchronos.WebAPI;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Numerics;
|
||||||
|
|
||||||
namespace MareSynchronos.UI;
|
namespace MareSynchronos.UI;
|
||||||
|
|
||||||
@@ -21,7 +22,6 @@ public class EditProfileUi : WindowMediatorSubscriberBase
|
|||||||
private readonly FileDialogManager _fileDialogManager;
|
private readonly FileDialogManager _fileDialogManager;
|
||||||
private readonly MareProfileManager _mareProfileManager;
|
private readonly MareProfileManager _mareProfileManager;
|
||||||
private readonly UiSharedService _uiSharedService;
|
private readonly UiSharedService _uiSharedService;
|
||||||
private readonly ServerConfigurationManager _serverConfigurationManager;
|
|
||||||
private bool _adjustedForScollBarsLocalProfile = false;
|
private bool _adjustedForScollBarsLocalProfile = false;
|
||||||
private bool _adjustedForScollBarsOnlineProfile = false;
|
private bool _adjustedForScollBarsOnlineProfile = false;
|
||||||
private string _descriptionText = string.Empty;
|
private string _descriptionText = string.Empty;
|
||||||
@@ -33,7 +33,6 @@ public class EditProfileUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
public EditProfileUi(ILogger<EditProfileUi> logger, MareMediator mediator,
|
public EditProfileUi(ILogger<EditProfileUi> logger, MareMediator mediator,
|
||||||
ApiController apiController, UiSharedService uiSharedService, FileDialogManager fileDialogManager,
|
ApiController apiController, UiSharedService uiSharedService, FileDialogManager fileDialogManager,
|
||||||
ServerConfigurationManager serverConfigurationManager,
|
|
||||||
MareProfileManager mareProfileManager, PerformanceCollectorService performanceCollectorService)
|
MareProfileManager mareProfileManager, PerformanceCollectorService performanceCollectorService)
|
||||||
: base(logger, mediator, "Umbra Edit Profile###UmbraSyncEditProfileUI", performanceCollectorService)
|
: base(logger, mediator, "Umbra Edit Profile###UmbraSyncEditProfileUI", performanceCollectorService)
|
||||||
{
|
{
|
||||||
@@ -46,7 +45,6 @@ public class EditProfileUi : WindowMediatorSubscriberBase
|
|||||||
_apiController = apiController;
|
_apiController = apiController;
|
||||||
_uiSharedService = uiSharedService;
|
_uiSharedService = uiSharedService;
|
||||||
_fileDialogManager = fileDialogManager;
|
_fileDialogManager = fileDialogManager;
|
||||||
_serverConfigurationManager = serverConfigurationManager;
|
|
||||||
_mareProfileManager = mareProfileManager;
|
_mareProfileManager = mareProfileManager;
|
||||||
|
|
||||||
Mediator.Subscribe<GposeStartMessage>(this, (_) => { _wasOpen = IsOpen; IsOpen = false; });
|
Mediator.Subscribe<GposeStartMessage>(this, (_) => { _wasOpen = IsOpen; IsOpen = false; });
|
||||||
@@ -63,14 +61,25 @@ public class EditProfileUi : WindowMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected override void DrawInternal()
|
protected override void DrawInternal()
|
||||||
|
{
|
||||||
|
DrawProfileContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DrawInline()
|
||||||
|
{
|
||||||
|
DrawProfileContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawProfileContent()
|
||||||
{
|
{
|
||||||
_uiSharedService.BigText("Current Profile (as saved on server)");
|
_uiSharedService.BigText("Current Profile (as saved on server)");
|
||||||
|
ImGuiHelpers.ScaledDummy(new Vector2(0f, ImGui.GetStyle().ItemSpacing.Y / 2));
|
||||||
|
|
||||||
var profile = _mareProfileManager.GetMareProfile(new UserData(_apiController.UID));
|
var profile = _mareProfileManager.GetMareProfile(new UserData(_apiController.UID));
|
||||||
|
|
||||||
if (profile.IsFlagged)
|
if (profile.IsFlagged)
|
||||||
{
|
{
|
||||||
UiSharedService.ColorTextWrapped(profile.Description, ImGuiColors.DalamudRed);
|
UiSharedService.ColorTextWrapped(profile.Description, UiSharedService.AccentColor);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,13 +96,12 @@ public class EditProfileUi : WindowMediatorSubscriberBase
|
|||||||
_descriptionText = _profileDescription;
|
_descriptionText = _profileDescription;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var spacing = ImGui.GetStyle().ItemSpacing.X;
|
||||||
if (_pfpTextureWrap != null)
|
if (_pfpTextureWrap != null)
|
||||||
{
|
{
|
||||||
ImGui.Image(_pfpTextureWrap.Handle, ImGuiHelpers.ScaledVector2(_pfpTextureWrap.Width, _pfpTextureWrap.Height));
|
ImGui.Image(_pfpTextureWrap.Handle, ImGuiHelpers.ScaledVector2(_pfpTextureWrap.Width, _pfpTextureWrap.Height));
|
||||||
}
|
|
||||||
|
|
||||||
var spacing = ImGui.GetStyle().ItemSpacing.X;
|
|
||||||
ImGuiHelpers.ScaledRelativeSameLine(256, spacing);
|
ImGuiHelpers.ScaledRelativeSameLine(256, spacing);
|
||||||
|
}
|
||||||
using (_uiSharedService.GameFont.Push())
|
using (_uiSharedService.GameFont.Push())
|
||||||
{
|
{
|
||||||
var descriptionTextSize = ImGui.CalcTextSize(profile.Description, hideTextAfterDoubleHash: false, 256f);
|
var descriptionTextSize = ImGui.CalcTextSize(profile.Description, hideTextAfterDoubleHash: false, 256f);
|
||||||
@@ -157,7 +165,7 @@ public class EditProfileUi : WindowMediatorSubscriberBase
|
|||||||
UiSharedService.AttachToolTip("Clear your currently uploaded profile picture");
|
UiSharedService.AttachToolTip("Clear your currently uploaded profile picture");
|
||||||
if (_showFileDialogError)
|
if (_showFileDialogError)
|
||||||
{
|
{
|
||||||
UiSharedService.ColorTextWrapped("The profile picture must be a PNG file with a maximum height and width of 256px and 250KiB size", ImGuiColors.DalamudRed);
|
UiSharedService.ColorTextWrapped("The profile picture must be a PNG file with a maximum height and width of 256px and 250KiB size", UiSharedService.AccentColor);
|
||||||
}
|
}
|
||||||
var isNsfw = profile.IsNSFW;
|
var isNsfw = profile.IsNSFW;
|
||||||
if (ImGui.Checkbox("Profile is NSFW", ref isNsfw))
|
if (ImGui.Checkbox("Profile is NSFW", ref isNsfw))
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ internal class EventViewerUI : WindowMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
EventSeverity.Informational => new Vector4(),
|
EventSeverity.Informational => new Vector4(),
|
||||||
EventSeverity.Warning => ImGuiColors.DalamudYellow,
|
EventSeverity.Warning => ImGuiColors.DalamudYellow,
|
||||||
EventSeverity.Error => ImGuiColors.DalamudRed,
|
EventSeverity.Error => UiSharedService.AccentColor,
|
||||||
_ => new Vector4()
|
_ => new Vector4()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -77,9 +77,21 @@ public class UidDisplayHandler
|
|||||||
|
|
||||||
if (_popupTime > DateTime.UtcNow || !_mareConfigService.Current.ProfilesShow)
|
if (_popupTime > DateTime.UtcNow || !_mareConfigService.Current.ProfilesShow)
|
||||||
{
|
{
|
||||||
ImGui.SetTooltip("Left click to switch between UID display and nick" + Environment.NewLine
|
// Build tooltip; prepend last-seen when player is offline or not visible
|
||||||
|
string tooltip = "Left click to switch between UID display and nick" + Environment.NewLine
|
||||||
+ "Right click to change nick for " + pair.UserData.AliasOrUID + Environment.NewLine
|
+ "Right click to change nick for " + pair.UserData.AliasOrUID + Environment.NewLine
|
||||||
+ "Middle Mouse Button to open their profile in a separate window");
|
+ "Middle Mouse Button to open their profile in a separate window";
|
||||||
|
|
||||||
|
if (!pair.IsOnline || !pair.IsVisible)
|
||||||
|
{
|
||||||
|
var lastSeen = _serverManager.GetNameForUid(pair.UserData.UID);
|
||||||
|
if (!string.IsNullOrEmpty(lastSeen))
|
||||||
|
{
|
||||||
|
tooltip = "Vu sous : " + lastSeen + Environment.NewLine + tooltip;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.SetTooltip(tooltip);
|
||||||
}
|
}
|
||||||
else if (_popupTime < DateTime.UtcNow && !_popupShown)
|
else if (_popupTime < DateTime.UtcNow && !_popupShown)
|
||||||
{
|
{
|
||||||
@@ -142,10 +154,15 @@ public class UidDisplayHandler
|
|||||||
|
|
||||||
public (bool isUid, string text) GetPlayerText(Pair pair)
|
public (bool isUid, string text) GetPlayerText(Pair pair)
|
||||||
{
|
{
|
||||||
var textIsUid = true;
|
|
||||||
bool showUidInsteadOfName = ShowUidInsteadOfName(pair);
|
bool showUidInsteadOfName = ShowUidInsteadOfName(pair);
|
||||||
|
if (showUidInsteadOfName)
|
||||||
|
{
|
||||||
|
return (true, pair.UserData.UID);
|
||||||
|
}
|
||||||
|
|
||||||
|
var textIsUid = true;
|
||||||
string? playerText = _serverManager.GetNoteForUid(pair.UserData.UID);
|
string? playerText = _serverManager.GetNoteForUid(pair.UserData.UID);
|
||||||
if (!showUidInsteadOfName && playerText != null)
|
if (playerText != null)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(playerText))
|
if (string.IsNullOrEmpty(playerText))
|
||||||
{
|
{
|
||||||
@@ -161,7 +178,7 @@ public class UidDisplayHandler
|
|||||||
playerText = pair.UserData.AliasOrUID;
|
playerText = pair.UserData.AliasOrUID;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_mareConfigService.Current.ShowCharacterNames && textIsUid && !showUidInsteadOfName)
|
if (_mareConfigService.Current.ShowCharacterNames && textIsUid && pair.IsOnline && pair.IsVisible)
|
||||||
{
|
{
|
||||||
var name = pair.PlayerName;
|
var name = pair.PlayerName;
|
||||||
if (name != null)
|
if (name != null)
|
||||||
@@ -197,8 +214,11 @@ public class UidDisplayHandler
|
|||||||
|
|
||||||
private bool ShowUidInsteadOfName(Pair pair)
|
private bool ShowUidInsteadOfName(Pair pair)
|
||||||
{
|
{
|
||||||
_showUidForEntry.TryGetValue(pair.UserData.UID, out var showUidInsteadOfName);
|
if (_showUidForEntry.TryGetValue(pair.UserData.UID, out var showUidInsteadOfName))
|
||||||
|
{
|
||||||
return showUidInsteadOfName;
|
return showUidInsteadOfName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -69,17 +69,17 @@ public partial class IntroUi : WindowMediatorSubscriberBase
|
|||||||
return _uiShared.ApiController.ServerState switch
|
return _uiShared.ApiController.ServerState switch
|
||||||
{
|
{
|
||||||
ServerState.Connecting => ImGuiColors.DalamudYellow,
|
ServerState.Connecting => ImGuiColors.DalamudYellow,
|
||||||
ServerState.Reconnecting => ImGuiColors.DalamudRed,
|
ServerState.Reconnecting => UiSharedService.AccentColor,
|
||||||
ServerState.Connected => ImGuiColors.HealerGreen,
|
ServerState.Connected => ImGuiColors.HealerGreen,
|
||||||
ServerState.Disconnected => ImGuiColors.DalamudYellow,
|
ServerState.Disconnected => ImGuiColors.DalamudYellow,
|
||||||
ServerState.Disconnecting => ImGuiColors.DalamudYellow,
|
ServerState.Disconnecting => ImGuiColors.DalamudYellow,
|
||||||
ServerState.Unauthorized => ImGuiColors.DalamudRed,
|
ServerState.Unauthorized => UiSharedService.AccentColor,
|
||||||
ServerState.VersionMisMatch => ImGuiColors.DalamudRed,
|
ServerState.VersionMisMatch => UiSharedService.AccentColor,
|
||||||
ServerState.Offline => ImGuiColors.DalamudRed,
|
ServerState.Offline => UiSharedService.AccentColor,
|
||||||
ServerState.RateLimited => ImGuiColors.DalamudYellow,
|
ServerState.RateLimited => ImGuiColors.DalamudYellow,
|
||||||
ServerState.NoSecretKey => ImGuiColors.DalamudYellow,
|
ServerState.NoSecretKey => ImGuiColors.DalamudYellow,
|
||||||
ServerState.MultiChara => ImGuiColors.DalamudYellow,
|
ServerState.MultiChara => ImGuiColors.DalamudYellow,
|
||||||
_ => ImGuiColors.DalamudRed
|
_ => UiSharedService.AccentColor
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,46 +140,46 @@ public partial class IntroUi : WindowMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
using (_uiShared.UidFont.Push())
|
using (_uiShared.UidFont.Push())
|
||||||
{
|
{
|
||||||
ImGui.TextUnformatted("Agreement of Usage of Service");
|
ImGui.TextUnformatted("Conditions d'utilisation");
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
ImGui.SetWindowFontScale(1.5f);
|
UiSharedService.SetFontScale(1.5f);
|
||||||
string readThis = "READ THIS CAREFULLY";
|
string readThis = "MERCI DE LIRE ATTENTIVEMENT";
|
||||||
Vector2 textSize = ImGui.CalcTextSize(readThis);
|
Vector2 textSize = ImGui.CalcTextSize(readThis);
|
||||||
ImGui.SetCursorPosX(ImGui.GetWindowSize().X / 2 - textSize.X / 2);
|
ImGui.SetCursorPosX(ImGui.GetWindowSize().X / 2 - textSize.X / 2);
|
||||||
UiSharedService.ColorText(readThis, ImGuiColors.DalamudRed);
|
UiSharedService.ColorText(readThis, UiSharedService.AccentColor);
|
||||||
ImGui.SetWindowFontScale(1.0f);
|
UiSharedService.SetFontScale(1.0f);
|
||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
UiSharedService.TextWrapped("""
|
UiSharedService.TextWrapped("""
|
||||||
To use Umbra, you must be over the age of 18, or 21 in some jurisdictions.
|
Pour utiliser les services UmbraSync, vous devez être âgé de plus de 18 ans, où plus de 21 ans dans certaines juridictions.
|
||||||
""");
|
""");
|
||||||
UiSharedService.TextWrapped("""
|
UiSharedService.TextWrapped("""
|
||||||
All of the mod files currently active on your character as well as your current character state will be uploaded to the service you registered yourself at automatically. The plugin will exclusively upload the necessary mod files and not the whole mod.
|
Tout les mods actuellement actifs sur votre personnage et ses états associés seront automatiquement téléchargés vers le serveur UmbraSync auquel vous vous êtes inscrit.Il sera téléchargé exclusivement les fichiers nécessaires à la synchronisation et non l'intégralité du mod.
|
||||||
""");
|
""");
|
||||||
UiSharedService.TextWrapped("""
|
UiSharedService.TextWrapped("""
|
||||||
If you are on a data capped internet connection, higher fees due to data usage depending on the amount of downloaded and uploaded mod files might occur. Mod files will be compressed on up- and download to save on bandwidth usage. Due to varying up- and download speeds, changes in characters might not be visible immediately. Files present on the service that already represent your active mod files will not be uploaded again.
|
Si vous disposez d'une connexion Internet limitée, des frais supplémentaires peuvent s'appliquer en fonction du nombre de fichiers envoyés et reçus. Les fichiers seront compressés afin d'économiser la bande passante. En raison des variations de vitesse de débit, les sychronisations peuvent ne pas être visible immédiatement.
|
||||||
""");
|
""");
|
||||||
UiSharedService.TextWrapped("""
|
UiSharedService.TextWrapped("""
|
||||||
The mod files you are uploading are confidential and will not be distributed to parties other than the ones who are requesting the exact same mod files. Please think about who you are going to pair since it is unavoidable that they will receive and locally cache the necessary mod files that you have currently in use. Locally cached mod files will have arbitrary file names to discourage attempts at replicating the original mod.
|
Les fichiers téléchargés sont confidentiels et ne seront pas distribués à des solutions tierces où autres personnes. Uniquement les personnes avec qui vous êtes appairés demandent exactement les mêmes fichiers. Réfléchissez donc bien avec qui vous allez vous appairer.
|
||||||
""");
|
""");
|
||||||
UiSharedService.TextWrapped("""
|
UiSharedService.TextWrapped("""
|
||||||
The plugin creator tried their best to keep you secure. However, there is no guarantee for 100% security. Do not blindly pair your client with everyone.
|
Le gentil dev' a fait de son mieux pour assurer votre sécurité. Cependant le risque 0 n'existe pas. Ne vous appairez pas aveuglément avec n'importe qui.
|
||||||
""");
|
""");
|
||||||
UiSharedService.TextWrapped("""
|
UiSharedService.TextWrapped("""
|
||||||
Mod files that are saved on the service will remain on the service as long as there are requests for the files from clients. After a period of not being used, the mod files will be automatically deleted.
|
Après une periode d'inactivité, les mods enregistrés sur le serveur UmbraSync seront automatiquement supprimés.
|
||||||
""");
|
""");
|
||||||
UiSharedService.TextWrapped("""
|
UiSharedService.TextWrapped("""
|
||||||
Accounts that are inactive for ninety (90) days will be deleted for privacy reasons.
|
Les comptes inactifs pendant 90 jours seront supprimés pour des raisons de stockage et de confidentialité.
|
||||||
""");
|
""");
|
||||||
UiSharedService.TextWrapped("""
|
UiSharedService.TextWrapped("""
|
||||||
Umbra is operated from servers located in the European Union. You agree not to upload any content to the service that violates EU law; and more specifically, German law.
|
L'infrastructure Umbrasync est hebergé dans l'Union Européenne (Allemagne) et en Suisse. Vous acceptez alors de ne pas télécharger de contenu qui pourrait aller à l'encontre des législations de ces deux pays.
|
||||||
""");
|
""");
|
||||||
UiSharedService.TextWrapped("""
|
UiSharedService.TextWrapped("""
|
||||||
You may delete your account at any time from within the Settings panel of the plugin. Any mods unique to you will then be removed from the server within 14 days.
|
Vous pouvez supprimer votre compte à tout moment. Votre compte et toutes les données associées seront supprimés dans un délai de 14 jours.
|
||||||
""");
|
""");
|
||||||
UiSharedService.TextWrapped("""
|
UiSharedService.TextWrapped("""
|
||||||
This service is provided as-is.
|
Ce service est fourni tel quel.
|
||||||
""");
|
""");
|
||||||
|
|
||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
@@ -208,7 +208,7 @@ public partial class IntroUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
if (!_uiShared.HasValidPenumbraModPath)
|
if (!_uiShared.HasValidPenumbraModPath)
|
||||||
{
|
{
|
||||||
UiSharedService.ColorTextWrapped("You do not have a valid Penumbra path set. Open Penumbra and set up a valid path for the mod directory.", ImGuiColors.DalamudRed);
|
UiSharedService.ColorTextWrapped("You do not have a valid Penumbra path set. Open Penumbra and set up a valid path for the mod directory.", UiSharedService.AccentColor);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -330,11 +330,11 @@ public partial class IntroUi : WindowMediatorSubscriberBase
|
|||||||
ImGui.InputText("", ref _secretKey, 64);
|
ImGui.InputText("", ref _secretKey, 64);
|
||||||
if (_secretKey.Length > 0 && _secretKey.Length != 64)
|
if (_secretKey.Length > 0 && _secretKey.Length != 64)
|
||||||
{
|
{
|
||||||
UiSharedService.ColorTextWrapped("Your secret key must be exactly 64 characters long.", ImGuiColors.DalamudRed);
|
UiSharedService.ColorTextWrapped("Your secret key must be exactly 64 characters long.", UiSharedService.AccentColor);
|
||||||
}
|
}
|
||||||
else if (_secretKey.Length == 64 && !HexRegex().IsMatch(_secretKey))
|
else if (_secretKey.Length == 64 && !HexRegex().IsMatch(_secretKey))
|
||||||
{
|
{
|
||||||
UiSharedService.ColorTextWrapped("Your secret key can only contain ABCDEF and the numbers 0-9.", ImGuiColors.DalamudRed);
|
UiSharedService.ColorTextWrapped("Your secret key can only contain ABCDEF and the numbers 0-9.", UiSharedService.AccentColor);
|
||||||
}
|
}
|
||||||
else if (_secretKey.Length == 64)
|
else if (_secretKey.Length == 64)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -128,6 +128,10 @@ public class PermissionWindowUI : WindowMediatorSubscriberBase
|
|||||||
using (ImRaii.Disabled(!hasChanges))
|
using (ImRaii.Disabled(!hasChanges))
|
||||||
if (_uiSharedService.IconTextButton(Dalamud.Interface.FontAwesomeIcon.Save, "Save"))
|
if (_uiSharedService.IconTextButton(Dalamud.Interface.FontAwesomeIcon.Save, "Save"))
|
||||||
{
|
{
|
||||||
|
Mediator.Publish(new PairSyncOverrideChanged(Pair.UserData.UID,
|
||||||
|
_ownPermissions.IsDisableSounds(),
|
||||||
|
_ownPermissions.IsDisableAnimations(),
|
||||||
|
_ownPermissions.IsDisableVFX()));
|
||||||
_ = _apiController.UserSetPairPermissions(new(Pair.UserData, _ownPermissions));
|
_ = _apiController.UserSetPairPermissions(new(Pair.UserData, _ownPermissions));
|
||||||
}
|
}
|
||||||
UiSharedService.AttachToolTip("Save and apply all changes");
|
UiSharedService.AttachToolTip("Save and apply all changes");
|
||||||
@@ -148,10 +152,15 @@ public class PermissionWindowUI : WindowMediatorSubscriberBase
|
|||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
if (_uiSharedService.IconTextButton(Dalamud.Interface.FontAwesomeIcon.ArrowsSpin, "Reset to Default"))
|
if (_uiSharedService.IconTextButton(Dalamud.Interface.FontAwesomeIcon.ArrowsSpin, "Reset to Default"))
|
||||||
{
|
{
|
||||||
|
var defaults = _uiSharedService.ConfigService.Current;
|
||||||
_ownPermissions.SetPaused(false);
|
_ownPermissions.SetPaused(false);
|
||||||
_ownPermissions.SetDisableVFX(false);
|
_ownPermissions.SetDisableSounds(defaults.DefaultDisableSounds);
|
||||||
_ownPermissions.SetDisableSounds(false);
|
_ownPermissions.SetDisableAnimations(defaults.DefaultDisableAnimations);
|
||||||
_ownPermissions.SetDisableAnimations(false);
|
_ownPermissions.SetDisableVFX(defaults.DefaultDisableVfx);
|
||||||
|
Mediator.Publish(new PairSyncOverrideChanged(Pair.UserData.UID,
|
||||||
|
_ownPermissions.IsDisableSounds(),
|
||||||
|
_ownPermissions.IsDisableAnimations(),
|
||||||
|
_ownPermissions.IsDisableVFX()));
|
||||||
_ = _apiController.UserSetPairPermissions(new(Pair.UserData, _ownPermissions));
|
_ = _apiController.UserSetPairPermissions(new(Pair.UserData, _ownPermissions));
|
||||||
}
|
}
|
||||||
UiSharedService.AttachToolTip("This will set all permissions to their default setting");
|
UiSharedService.AttachToolTip("This will set all permissions to their default setting");
|
||||||
|
|||||||
@@ -16,25 +16,22 @@ namespace MareSynchronos.UI;
|
|||||||
public class PopoutProfileUi : WindowMediatorSubscriberBase
|
public class PopoutProfileUi : WindowMediatorSubscriberBase
|
||||||
{
|
{
|
||||||
private readonly MareProfileManager _mareProfileManager;
|
private readonly MareProfileManager _mareProfileManager;
|
||||||
private readonly PairManager _pairManager;
|
|
||||||
private readonly ServerConfigurationManager _serverManager;
|
private readonly ServerConfigurationManager _serverManager;
|
||||||
private readonly UiSharedService _uiSharedService;
|
private readonly UiSharedService _uiSharedService;
|
||||||
private Vector2 _lastMainPos = Vector2.Zero;
|
private Vector2 _lastMainPos = Vector2.Zero;
|
||||||
private Vector2 _lastMainSize = Vector2.Zero;
|
private Vector2 _lastMainSize = Vector2.Zero;
|
||||||
private byte[] _lastProfilePicture = [];
|
private byte[] _lastProfilePicture = [];
|
||||||
private byte[] _lastSupporterPicture = [];
|
|
||||||
private Pair? _pair;
|
private Pair? _pair;
|
||||||
private IDalamudTextureWrap? _supporterTextureWrap;
|
private IDalamudTextureWrap? _supporterTextureWrap;
|
||||||
private IDalamudTextureWrap? _textureWrap;
|
private IDalamudTextureWrap? _textureWrap;
|
||||||
|
|
||||||
public PopoutProfileUi(ILogger<PopoutProfileUi> logger, MareMediator mediator, UiSharedService uiSharedService,
|
public PopoutProfileUi(ILogger<PopoutProfileUi> logger, MareMediator mediator, UiSharedService uiSharedService,
|
||||||
ServerConfigurationManager serverManager, MareConfigService mareConfigService,
|
ServerConfigurationManager serverManager, MareConfigService mareConfigService,
|
||||||
MareProfileManager mareProfileManager, PairManager pairManager, PerformanceCollectorService performanceCollectorService) : base(logger, mediator, "###UmbraSyncPopoutProfileUI", performanceCollectorService)
|
MareProfileManager mareProfileManager, PerformanceCollectorService performanceCollectorService) : base(logger, mediator, "###UmbraSyncPopoutProfileUI", performanceCollectorService)
|
||||||
{
|
{
|
||||||
_uiSharedService = uiSharedService;
|
_uiSharedService = uiSharedService;
|
||||||
_serverManager = serverManager;
|
_serverManager = serverManager;
|
||||||
_mareProfileManager = mareProfileManager;
|
_mareProfileManager = mareProfileManager;
|
||||||
_pairManager = pairManager;
|
|
||||||
Flags = ImGuiWindowFlags.NoDecoration;
|
Flags = ImGuiWindowFlags.NoDecoration;
|
||||||
|
|
||||||
Mediator.Subscribe<ProfilePopoutToggle>(this, (msg) =>
|
Mediator.Subscribe<ProfilePopoutToggle>(this, (msg) =>
|
||||||
@@ -42,7 +39,6 @@ public class PopoutProfileUi : WindowMediatorSubscriberBase
|
|||||||
IsOpen = msg.Pair != null;
|
IsOpen = msg.Pair != null;
|
||||||
_pair = msg.Pair;
|
_pair = msg.Pair;
|
||||||
_lastProfilePicture = [];
|
_lastProfilePicture = [];
|
||||||
_lastSupporterPicture = [];
|
|
||||||
_textureWrap?.Dispose();
|
_textureWrap?.Dispose();
|
||||||
_textureWrap = null;
|
_textureWrap = null;
|
||||||
_supporterTextureWrap?.Dispose();
|
_supporterTextureWrap?.Dispose();
|
||||||
@@ -113,7 +109,7 @@ public class PopoutProfileUi : WindowMediatorSubscriberBase
|
|||||||
UiSharedService.ColorText(note, ImGuiColors.DalamudGrey);
|
UiSharedService.ColorText(note, ImGuiColors.DalamudGrey);
|
||||||
}
|
}
|
||||||
string status = _pair.IsVisible ? "Visible" : (_pair.IsOnline ? "Online" : "Offline");
|
string status = _pair.IsVisible ? "Visible" : (_pair.IsOnline ? "Online" : "Offline");
|
||||||
UiSharedService.ColorText(status, (_pair.IsVisible || _pair.IsOnline) ? ImGuiColors.HealerGreen : ImGuiColors.DalamudRed);
|
UiSharedService.ColorText(status, (_pair.IsVisible || _pair.IsOnline) ? ImGuiColors.HealerGreen : UiSharedService.AccentColor);
|
||||||
if (_pair.IsVisible)
|
if (_pair.IsVisible)
|
||||||
{
|
{
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ using MareSynchronos.MareConfiguration.Models;
|
|||||||
using MareSynchronos.PlayerData.Handlers;
|
using MareSynchronos.PlayerData.Handlers;
|
||||||
using MareSynchronos.PlayerData.Pairs;
|
using MareSynchronos.PlayerData.Pairs;
|
||||||
using MareSynchronos.Services;
|
using MareSynchronos.Services;
|
||||||
|
using MareSynchronos.Services.AutoDetect;
|
||||||
using MareSynchronos.Services.Mediator;
|
using MareSynchronos.Services.Mediator;
|
||||||
using MareSynchronos.Services.ServerConfiguration;
|
using MareSynchronos.Services.ServerConfiguration;
|
||||||
using MareSynchronos.WebAPI;
|
using MareSynchronos.WebAPI;
|
||||||
@@ -44,12 +45,14 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
private readonly PairManager _pairManager;
|
private readonly PairManager _pairManager;
|
||||||
private readonly ChatService _chatService;
|
private readonly ChatService _chatService;
|
||||||
private readonly GuiHookService _guiHookService;
|
private readonly GuiHookService _guiHookService;
|
||||||
|
private readonly AutoDetectSuppressionService _autoDetectSuppressionService;
|
||||||
private readonly PerformanceCollectorService _performanceCollector;
|
private readonly PerformanceCollectorService _performanceCollector;
|
||||||
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
|
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
|
||||||
private readonly PlayerPerformanceService _playerPerformanceService;
|
private readonly PlayerPerformanceService _playerPerformanceService;
|
||||||
private readonly AccountRegistrationService _registerService;
|
private readonly AccountRegistrationService _registerService;
|
||||||
private readonly ServerConfigurationManager _serverConfigurationManager;
|
private readonly ServerConfigurationManager _serverConfigurationManager;
|
||||||
private readonly UiSharedService _uiShared;
|
private readonly UiSharedService _uiShared;
|
||||||
|
private static readonly string DtrDefaultPreviewText = DtrEntry.DefaultGlyph + " 123";
|
||||||
private bool _deleteAccountPopupModalShown = false;
|
private bool _deleteAccountPopupModalShown = false;
|
||||||
private string _lastTab = string.Empty;
|
private string _lastTab = string.Empty;
|
||||||
private bool? _notesSuccessfullyApplied = null;
|
private bool? _notesSuccessfullyApplied = null;
|
||||||
@@ -76,7 +79,8 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
FileCacheManager fileCacheManager,
|
FileCacheManager fileCacheManager,
|
||||||
FileCompactor fileCompactor, ApiController apiController,
|
FileCompactor fileCompactor, ApiController apiController,
|
||||||
IpcManager ipcManager, IpcProvider ipcProvider, CacheMonitor cacheMonitor,
|
IpcManager ipcManager, IpcProvider ipcProvider, CacheMonitor cacheMonitor,
|
||||||
DalamudUtilService dalamudUtilService, AccountRegistrationService registerService) : base(logger, mediator, "Umbra Settings", performanceCollector)
|
DalamudUtilService dalamudUtilService, AccountRegistrationService registerService,
|
||||||
|
AutoDetectSuppressionService autoDetectSuppressionService) : base(logger, mediator, "Umbra Settings", performanceCollector)
|
||||||
{
|
{
|
||||||
_configService = configService;
|
_configService = configService;
|
||||||
_pairManager = pairManager;
|
_pairManager = pairManager;
|
||||||
@@ -95,6 +99,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
_cacheMonitor = cacheMonitor;
|
_cacheMonitor = cacheMonitor;
|
||||||
_dalamudUtilService = dalamudUtilService;
|
_dalamudUtilService = dalamudUtilService;
|
||||||
_registerService = registerService;
|
_registerService = registerService;
|
||||||
|
_autoDetectSuppressionService = autoDetectSuppressionService;
|
||||||
_fileCompactor = fileCompactor;
|
_fileCompactor = fileCompactor;
|
||||||
_uiShared = uiShared;
|
_uiShared = uiShared;
|
||||||
AllowClickthrough = false;
|
AllowClickthrough = false;
|
||||||
@@ -126,6 +131,11 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
DrawSettingsContent();
|
DrawSettingsContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void DrawInline()
|
||||||
|
{
|
||||||
|
DrawSettingsContent();
|
||||||
|
}
|
||||||
|
|
||||||
public override void OnClose()
|
public override void OnClose()
|
||||||
{
|
{
|
||||||
_uiShared.EditTrackerPosition = false;
|
_uiShared.EditTrackerPosition = false;
|
||||||
@@ -204,17 +214,32 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
ImGui.AlignTextToFramePadding();
|
ImGui.AlignTextToFramePadding();
|
||||||
ImGui.TextUnformatted("0 = No limit/infinite");
|
ImGui.TextUnformatted("0 = No limit/infinite");
|
||||||
|
|
||||||
|
bool enableDownloadQueue = _configService.Current.EnableDownloadQueue;
|
||||||
|
if (ImGui.Checkbox("Activer la file de téléchargements", ref enableDownloadQueue))
|
||||||
|
{
|
||||||
|
_configService.Current.EnableDownloadQueue = enableDownloadQueue;
|
||||||
|
_configService.Save();
|
||||||
|
}
|
||||||
|
UiSharedService.AttachToolTip("Lance les téléchargements de personnages de manière séquentielle plutôt que tous en même temps. "
|
||||||
|
+ "Quand l'option est activée, seul le nombre configuré de téléchargements fonctionne en parallèle.");
|
||||||
|
|
||||||
ImGui.SetNextItemWidth(250 * ImGuiHelpers.GlobalScale);
|
ImGui.SetNextItemWidth(250 * ImGuiHelpers.GlobalScale);
|
||||||
if (ImGui.SliderInt("Maximum Parallel Downloads", ref maxParallelDownloads, 1, 10))
|
if (ImGui.SliderInt("Maximum Parallel Downloads", ref maxParallelDownloads, 1, 10))
|
||||||
{
|
{
|
||||||
_configService.Current.ParallelDownloads = maxParallelDownloads;
|
_configService.Current.ParallelDownloads = maxParallelDownloads;
|
||||||
_configService.Save();
|
_configService.Save();
|
||||||
}
|
}
|
||||||
|
UiSharedService.AttachToolTip(enableDownloadQueue
|
||||||
|
? "Nombre maximal de téléchargements de personnages autorisés simultanément lorsque la file est activée."
|
||||||
|
: "Nombre maximal de flux de fichiers lancés en parallèle pour chaque téléchargement.");
|
||||||
|
|
||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
_uiShared.BigText("AutoDetect");
|
_uiShared.BigText("AutoDetect");
|
||||||
|
bool isAutoDetectSuppressed = _autoDetectSuppressionService?.IsSuppressed ?? false;
|
||||||
bool enableDiscovery = _configService.Current.EnableAutoDetectDiscovery;
|
bool enableDiscovery = _configService.Current.EnableAutoDetectDiscovery;
|
||||||
if (ImGui.Checkbox("Enable Nearby detection (beta)", ref enableDiscovery))
|
using (ImRaii.Disabled(isAutoDetectSuppressed))
|
||||||
|
{
|
||||||
|
if (ImGui.Checkbox("Activer l'AutoDetect", ref enableDiscovery))
|
||||||
{
|
{
|
||||||
_configService.Current.EnableAutoDetectDiscovery = enableDiscovery;
|
_configService.Current.EnableAutoDetectDiscovery = enableDiscovery;
|
||||||
_configService.Save();
|
_configService.Save();
|
||||||
@@ -230,12 +255,17 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
Mediator.Publish(new AllowPairRequestsToggled(false));
|
Mediator.Publish(new AllowPairRequestsToggled(false));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (isAutoDetectSuppressed && ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
|
||||||
|
{
|
||||||
|
UiSharedService.AttachToolTip("AutoDetect est temporairement désactivé dans cette zone instanciée.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Allow Pair Requests is disabled when Nearby is OFF
|
// Allow Pair Requests is disabled when Nearby is OFF
|
||||||
using (ImRaii.Disabled(!enableDiscovery))
|
using (ImRaii.Disabled(isAutoDetectSuppressed || !enableDiscovery))
|
||||||
{
|
{
|
||||||
bool allowRequests = _configService.Current.AllowAutoDetectPairRequests;
|
bool allowRequests = _configService.Current.AllowAutoDetectPairRequests;
|
||||||
if (ImGui.Checkbox("Allow pair requests", ref allowRequests))
|
if (ImGui.Checkbox("Activer les invitations entrantes", ref allowRequests))
|
||||||
{
|
{
|
||||||
_configService.Current.AllowAutoDetectPairRequests = allowRequests;
|
_configService.Current.AllowAutoDetectPairRequests = allowRequests;
|
||||||
_configService.Save();
|
_configService.Save();
|
||||||
@@ -245,26 +275,34 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
// user-facing info toast
|
// user-facing info toast
|
||||||
Mediator.Publish(new NotificationMessage(
|
Mediator.Publish(new NotificationMessage(
|
||||||
"Nearby Detection",
|
"AutoDetect",
|
||||||
allowRequests ? "Pair requests enabled: others can invite you." : "Pair requests disabled: others cannot invite you.",
|
allowRequests ? "Invitations entrantes autorisées : les autres peuvent vous inviter." : "Invitations entrantes désactivées : les autres ne peuvent pas vous inviter.",
|
||||||
NotificationType.Info,
|
NotificationType.Info,
|
||||||
default));
|
default));
|
||||||
}
|
}
|
||||||
|
if (isAutoDetectSuppressed && ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
|
||||||
|
{
|
||||||
|
UiSharedService.AttachToolTip("AutoDetect est temporairement désactivé dans cette zone instanciée.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Radius only available when both Nearby and Allow Pair Requests are ON
|
// Radius only available when both Nearby and Allow Pair Requests are ON
|
||||||
if (enableDiscovery && _configService.Current.AllowAutoDetectPairRequests)
|
if (!isAutoDetectSuppressed && enableDiscovery && _configService.Current.AllowAutoDetectPairRequests)
|
||||||
{
|
{
|
||||||
ImGui.Indent();
|
ImGui.Indent();
|
||||||
int maxMeters = _configService.Current.AutoDetectMaxDistanceMeters;
|
int maxMeters = _configService.Current.AutoDetectMaxDistanceMeters;
|
||||||
ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale);
|
ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale);
|
||||||
if (ImGui.SliderInt("Max distance (meters)", ref maxMeters, 5, 100))
|
if (ImGui.SliderInt("Distance max (en mètre)", ref maxMeters, 5, 100))
|
||||||
{
|
{
|
||||||
_configService.Current.AutoDetectMaxDistanceMeters = maxMeters;
|
_configService.Current.AutoDetectMaxDistanceMeters = maxMeters;
|
||||||
_configService.Save();
|
_configService.Save();
|
||||||
}
|
}
|
||||||
ImGui.Unindent();
|
ImGui.Unindent();
|
||||||
}
|
}
|
||||||
|
else if (isAutoDetectSuppressed)
|
||||||
|
{
|
||||||
|
UiSharedService.ColorTextWrapped("AutoDetect est verrouillé tant que vous restez dans une zone instanciée.", ImGuiColors.DalamudYellow);
|
||||||
|
}
|
||||||
|
|
||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
_uiShared.BigText("Transfer UI");
|
_uiShared.BigText("Transfer UI");
|
||||||
@@ -496,9 +534,9 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
}, globalChatTypeIdx);
|
}, globalChatTypeIdx);
|
||||||
_uiShared.DrawHelpText("FFXIV chat channel to output chat messages on.");
|
_uiShared.DrawHelpText("FFXIV chat channel to output chat messages on.");
|
||||||
|
|
||||||
ImGui.SetWindowFontScale(0.6f);
|
UiSharedService.SetFontScale(0.6f);
|
||||||
_uiShared.BigText("\"Chat 2\" Plugin Integration");
|
_uiShared.BigText("\"Chat 2\" Plugin Integration");
|
||||||
ImGui.SetWindowFontScale(1.0f);
|
UiSharedService.SetFontScale(1.0f);
|
||||||
|
|
||||||
var extraChatTags = _configService.Current.ExtraChatTags;
|
var extraChatTags = _configService.Current.ExtraChatTags;
|
||||||
if (ImGui.Checkbox("Tag messages as ExtraChat", ref extraChatTags))
|
if (ImGui.Checkbox("Tag messages as ExtraChat", ref extraChatTags))
|
||||||
@@ -539,9 +577,9 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
if (shellEnabled)
|
if (shellEnabled)
|
||||||
shellName = $"[{shellNumber}] {shellName}";
|
shellName = $"[{shellNumber}] {shellName}";
|
||||||
|
|
||||||
ImGui.SetWindowFontScale(0.6f);
|
UiSharedService.SetFontScale(0.6f);
|
||||||
_uiShared.BigText(shellName);
|
_uiShared.BigText(shellName);
|
||||||
ImGui.SetWindowFontScale(1.0f);
|
UiSharedService.SetFontScale(1.0f);
|
||||||
|
|
||||||
using var pushIndent = ImRaii.PushIndent();
|
using var pushIndent = ImRaii.PushIndent();
|
||||||
|
|
||||||
@@ -656,7 +694,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
else if (_ipcProvider.MarePluginEnabled)
|
else if (_ipcProvider.MarePluginEnabled)
|
||||||
UiSharedService.ColorTextWrapped("Umbra API inactive: Umbra plugin is loaded", ImGuiColors.DalamudYellow);
|
UiSharedService.ColorTextWrapped("Umbra API inactive: Umbra plugin is loaded", ImGuiColors.DalamudYellow);
|
||||||
else
|
else
|
||||||
UiSharedService.ColorTextWrapped("Umbra API inactive: Unknown reason", ImGuiColors.DalamudRed);
|
UiSharedService.ColorTextWrapped("Umbra API inactive: Unknown reason", UiSharedService.AccentColor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -990,7 +1028,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
else if (_notesSuccessfullyApplied.HasValue && !_notesSuccessfullyApplied.Value)
|
else if (_notesSuccessfullyApplied.HasValue && !_notesSuccessfullyApplied.Value)
|
||||||
{
|
{
|
||||||
UiSharedService.ColorTextWrapped("Attempt to import notes from clipboard failed. Check formatting and try again", ImGuiColors.DalamudRed);
|
UiSharedService.ColorTextWrapped("Attempt to import notes from clipboard failed. Check formatting and try again", UiSharedService.AccentColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
var openPopupOnAddition = _configService.Current.OpenPopupOnAdd;
|
var openPopupOnAddition = _configService.Current.OpenPopupOnAdd;
|
||||||
@@ -1049,13 +1087,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
_configService.Save();
|
_configService.Save();
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui.SetNextItemWidth(250 * ImGuiHelpers.GlobalScale);
|
DrawDtrStyleCombo();
|
||||||
_uiShared.DrawCombo("Server Info Bar style", Enumerable.Range(0, DtrEntry.NumStyles), (i) => DtrEntry.RenderDtrStyle(i, "123"),
|
|
||||||
(i) =>
|
|
||||||
{
|
|
||||||
_configService.Current.DtrStyle = i;
|
|
||||||
_configService.Save();
|
|
||||||
}, _configService.Current.DtrStyle);
|
|
||||||
|
|
||||||
if (ImGui.Checkbox("Color-code the Server Info Bar entry according to status", ref useColorsInDtr))
|
if (ImGui.Checkbox("Color-code the Server Info Bar entry according to status", ref useColorsInDtr))
|
||||||
{
|
{
|
||||||
@@ -1091,7 +1123,9 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
var useNameColors = _configService.Current.UseNameColors;
|
var useNameColors = _configService.Current.UseNameColors;
|
||||||
var nameColors = _configService.Current.NameColors;
|
var nameColors = _configService.Current.NameColors;
|
||||||
var autoPausedNameColors = _configService.Current.BlockedNameColors;
|
var autoPausedNameColors = _configService.Current.BlockedNameColors;
|
||||||
if (ImGui.Checkbox("Color nameplates of paired players", ref useNameColors))
|
var typingIndicatorNameplates = _configService.Current.TypingIndicatorShowOnNameplates;
|
||||||
|
var typingIndicatorPartyList = _configService.Current.TypingIndicatorShowOnPartyList;
|
||||||
|
if (ImGui.Checkbox("Coloriser les plaques de nom des paires", ref useNameColors))
|
||||||
{
|
{
|
||||||
_configService.Current.UseNameColors = useNameColors;
|
_configService.Current.UseNameColors = useNameColors;
|
||||||
_configService.Save();
|
_configService.Save();
|
||||||
@@ -1101,7 +1135,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
using (ImRaii.Disabled(!useNameColors))
|
using (ImRaii.Disabled(!useNameColors))
|
||||||
{
|
{
|
||||||
using var indent = ImRaii.PushIndent();
|
using var indent = ImRaii.PushIndent();
|
||||||
if (InputDtrColors("Character Name Color", ref nameColors))
|
if (InputDtrColors("Couleur du nom", ref nameColors))
|
||||||
{
|
{
|
||||||
_configService.Current.NameColors = nameColors;
|
_configService.Current.NameColors = nameColors;
|
||||||
_configService.Save();
|
_configService.Save();
|
||||||
@@ -1110,7 +1144,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
|
|
||||||
if (InputDtrColors("Blocked Character Color", ref autoPausedNameColors))
|
if (InputDtrColors("Couleur des noms bloqués", ref autoPausedNameColors))
|
||||||
{
|
{
|
||||||
_configService.Current.BlockedNameColors = autoPausedNameColors;
|
_configService.Current.BlockedNameColors = autoPausedNameColors;
|
||||||
_configService.Save();
|
_configService.Save();
|
||||||
@@ -1118,6 +1152,43 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ImGui.Checkbox("Afficher la bulle de frappe sur les plaques", ref typingIndicatorNameplates))
|
||||||
|
{
|
||||||
|
_configService.Current.TypingIndicatorShowOnNameplates = typingIndicatorNameplates;
|
||||||
|
_configService.Save();
|
||||||
|
}
|
||||||
|
_uiShared.DrawHelpText("Ajoute une bulle '...' sur la plaque des paires en train d'écrire.");
|
||||||
|
|
||||||
|
using (ImRaii.Disabled(!typingIndicatorNameplates))
|
||||||
|
{
|
||||||
|
using var indentTyping = ImRaii.PushIndent();
|
||||||
|
var bubbleSize = _configService.Current.TypingIndicatorBubbleSize;
|
||||||
|
TypingIndicatorBubbleSize? selectedBubbleSize = _uiShared.DrawCombo("Taille de la bulle de frappe##typingBubbleSize",
|
||||||
|
Enum.GetValues<TypingIndicatorBubbleSize>(),
|
||||||
|
size => size switch
|
||||||
|
{
|
||||||
|
TypingIndicatorBubbleSize.Small => "Petite",
|
||||||
|
TypingIndicatorBubbleSize.Medium => "Moyenne",
|
||||||
|
TypingIndicatorBubbleSize.Large => "Grande",
|
||||||
|
_ => size.ToString()
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
bubbleSize);
|
||||||
|
|
||||||
|
if (selectedBubbleSize.HasValue && selectedBubbleSize.Value != bubbleSize)
|
||||||
|
{
|
||||||
|
_configService.Current.TypingIndicatorBubbleSize = selectedBubbleSize.Value;
|
||||||
|
_configService.Save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui.Checkbox("Tracer la frappe dans la liste de groupe", ref typingIndicatorPartyList))
|
||||||
|
{
|
||||||
|
_configService.Current.TypingIndicatorShowOnPartyList = typingIndicatorPartyList;
|
||||||
|
_configService.Save();
|
||||||
|
}
|
||||||
|
_uiShared.DrawHelpText("Consigne dans les journaux quand une paire du groupe est en train d'écrire (bulle visuelle ultérieure).");
|
||||||
|
|
||||||
if (ImGui.Checkbox("Show separate Visible group", ref showVisibleSeparate))
|
if (ImGui.Checkbox("Show separate Visible group", ref showVisibleSeparate))
|
||||||
{
|
{
|
||||||
_configService.Current.ShowVisibleUsersSeparately = showVisibleSeparate;
|
_configService.Current.ShowVisibleUsersSeparately = showVisibleSeparate;
|
||||||
@@ -1262,6 +1333,14 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
_uiShared.BigText("Global Configuration");
|
_uiShared.BigText("Global Configuration");
|
||||||
|
|
||||||
|
bool showSelfAnalysisWarnings = _playerPerformanceConfigService.Current.ShowSelfAnalysisWarnings;
|
||||||
|
if (ImGui.Checkbox("Display self-analysis warnings", ref showSelfAnalysisWarnings))
|
||||||
|
{
|
||||||
|
_playerPerformanceConfigService.Current.ShowSelfAnalysisWarnings = showSelfAnalysisWarnings;
|
||||||
|
_playerPerformanceConfigService.Save();
|
||||||
|
}
|
||||||
|
_uiShared.DrawHelpText("Disable to suppress UmbraSync chat warnings when your character exceeds the self-analysis thresholds.");
|
||||||
|
|
||||||
bool alwaysShrinkTextures = _playerPerformanceConfigService.Current.TextureShrinkMode == TextureShrinkMode.Always;
|
bool alwaysShrinkTextures = _playerPerformanceConfigService.Current.TextureShrinkMode == TextureShrinkMode.Always;
|
||||||
bool deleteOriginalTextures = _playerPerformanceConfigService.Current.TextureShrinkDeleteOriginal;
|
bool deleteOriginalTextures = _playerPerformanceConfigService.Current.TextureShrinkDeleteOriginal;
|
||||||
|
|
||||||
@@ -1307,7 +1386,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
using (ImRaii.PushColor(ImGuiCol.Text, UiSharedService.AccentColor, totalVramBytes < 2.0 * 1024.0 * 1024.0 * 1024.0))
|
using (ImRaii.PushColor(ImGuiCol.Text, UiSharedService.AccentColor, totalVramBytes < 2.0 * 1024.0 * 1024.0 * 1024.0))
|
||||||
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudYellow, totalVramBytes >= 4.0 * 1024.0 * 1024.0 * 1024.0))
|
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudYellow, totalVramBytes >= 4.0 * 1024.0 * 1024.0 * 1024.0))
|
||||||
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed, totalVramBytes >= 6.0 * 1024.0 * 1024.0 * 1024.0))
|
using (ImRaii.PushColor(ImGuiCol.Text, UiSharedService.AccentColor, totalVramBytes >= 6.0 * 1024.0 * 1024.0 * 1024.0))
|
||||||
ImGui.TextUnformatted($"{totalVramBytes / 1024.0 / 1024.0 / 1024.0:0.00} GiB");
|
ImGui.TextUnformatted($"{totalVramBytes / 1024.0 / 1024.0 / 1024.0:0.00} GiB");
|
||||||
|
|
||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
@@ -1909,6 +1988,11 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
if (ImGui.BeginTabBar("mainTabBar"))
|
if (ImGui.BeginTabBar("mainTabBar"))
|
||||||
{
|
{
|
||||||
|
var accent = UiSharedService.AccentColor;
|
||||||
|
var accentColor = ImGui.ColorConvertFloat4ToU32(accent);
|
||||||
|
ImGui.PushStyleColor(ImGuiCol.TabActive, accentColor);
|
||||||
|
ImGui.PushStyleColor(ImGuiCol.TabHovered, accentColor);
|
||||||
|
|
||||||
if (ImGui.BeginTabItem("General"))
|
if (ImGui.BeginTabItem("General"))
|
||||||
{
|
{
|
||||||
DrawGeneral();
|
DrawGeneral();
|
||||||
@@ -1941,22 +2025,49 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
ImGui.EndDisabled();
|
ImGui.EndDisabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ImGui.BeginTabItem("Chat"))
|
|
||||||
{
|
|
||||||
DrawChatConfig();
|
|
||||||
ImGui.EndTabItem();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ImGui.BeginTabItem("Advanced"))
|
if (ImGui.BeginTabItem("Advanced"))
|
||||||
{
|
{
|
||||||
DrawAdvanced();
|
DrawAdvanced();
|
||||||
ImGui.EndTabItem();
|
ImGui.EndTabItem();
|
||||||
}
|
}
|
||||||
|
ImGui.PopStyleColor(2);
|
||||||
|
|
||||||
ImGui.EndTabBar();
|
ImGui.EndTabBar();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void DrawDtrStyleCombo()
|
||||||
|
{
|
||||||
|
var styleIndex = _configService.Current.DtrStyle;
|
||||||
|
string previewText = styleIndex == 0 ? DtrDefaultPreviewText : DtrEntry.RenderDtrStyle(styleIndex, "123");
|
||||||
|
|
||||||
|
ImGui.SetNextItemWidth(250 * ImGuiHelpers.GlobalScale);
|
||||||
|
bool comboOpen = ImGui.BeginCombo("Server Info Bar style", previewText);
|
||||||
|
|
||||||
|
if (comboOpen)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < DtrEntry.NumStyles; i++)
|
||||||
|
{
|
||||||
|
string label = i == 0 ? DtrDefaultPreviewText : DtrEntry.RenderDtrStyle(i, "123");
|
||||||
|
bool isSelected = i == styleIndex;
|
||||||
|
if (ImGui.Selectable(label, isSelected))
|
||||||
|
{
|
||||||
|
_configService.Current.DtrStyle = i;
|
||||||
|
_configService.Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSelected)
|
||||||
|
{
|
||||||
|
ImGui.SetItemDefaultFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.EndCombo();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
private void UiSharedService_GposeEnd()
|
private void UiSharedService_GposeEnd()
|
||||||
{
|
{
|
||||||
IsOpen = _wasOpen;
|
IsOpen = _wasOpen;
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ namespace MareSynchronos.UI;
|
|||||||
public class StandaloneProfileUi : WindowMediatorSubscriberBase
|
public class StandaloneProfileUi : WindowMediatorSubscriberBase
|
||||||
{
|
{
|
||||||
private readonly MareProfileManager _mareProfileManager;
|
private readonly MareProfileManager _mareProfileManager;
|
||||||
private readonly PairManager _pairManager;
|
|
||||||
private readonly ServerConfigurationManager _serverManager;
|
private readonly ServerConfigurationManager _serverManager;
|
||||||
private readonly UiSharedService _uiSharedService;
|
private readonly UiSharedService _uiSharedService;
|
||||||
private bool _adjustedForScrollBars = false;
|
private bool _adjustedForScrollBars = false;
|
||||||
@@ -24,7 +23,7 @@ public class StandaloneProfileUi : WindowMediatorSubscriberBase
|
|||||||
private IDalamudTextureWrap? _textureWrap;
|
private IDalamudTextureWrap? _textureWrap;
|
||||||
|
|
||||||
public StandaloneProfileUi(ILogger<StandaloneProfileUi> logger, MareMediator mediator, UiSharedService uiBuilder,
|
public StandaloneProfileUi(ILogger<StandaloneProfileUi> logger, MareMediator mediator, UiSharedService uiBuilder,
|
||||||
ServerConfigurationManager serverManager, MareProfileManager mareProfileManager, PairManager pairManager, Pair pair,
|
ServerConfigurationManager serverManager, MareProfileManager mareProfileManager, Pair pair,
|
||||||
PerformanceCollectorService performanceCollector)
|
PerformanceCollectorService performanceCollector)
|
||||||
: base(logger, mediator, "Profile of " + pair.UserData.AliasOrUID + "##UmbraSyncStandaloneProfileUI" + pair.UserData.AliasOrUID, performanceCollector)
|
: base(logger, mediator, "Profile of " + pair.UserData.AliasOrUID + "##UmbraSyncStandaloneProfileUI" + pair.UserData.AliasOrUID, performanceCollector)
|
||||||
{
|
{
|
||||||
@@ -32,7 +31,6 @@ public class StandaloneProfileUi : WindowMediatorSubscriberBase
|
|||||||
_serverManager = serverManager;
|
_serverManager = serverManager;
|
||||||
_mareProfileManager = mareProfileManager;
|
_mareProfileManager = mareProfileManager;
|
||||||
Pair = pair;
|
Pair = pair;
|
||||||
_pairManager = pairManager;
|
|
||||||
Flags = ImGuiWindowFlags.NoResize | ImGuiWindowFlags.AlwaysAutoResize;
|
Flags = ImGuiWindowFlags.NoResize | ImGuiWindowFlags.AlwaysAutoResize;
|
||||||
|
|
||||||
var spacing = ImGui.GetStyle().ItemSpacing;
|
var spacing = ImGui.GetStyle().ItemSpacing;
|
||||||
@@ -111,7 +109,7 @@ public class StandaloneProfileUi : WindowMediatorSubscriberBase
|
|||||||
UiSharedService.ColorText(note, ImGuiColors.DalamudGrey);
|
UiSharedService.ColorText(note, ImGuiColors.DalamudGrey);
|
||||||
}
|
}
|
||||||
string status = Pair.IsVisible ? "Visible" : (Pair.IsOnline ? "Online" : "Offline");
|
string status = Pair.IsVisible ? "Visible" : (Pair.IsOnline ? "Online" : "Offline");
|
||||||
UiSharedService.ColorText(status, (Pair.IsVisible || Pair.IsOnline) ? ImGuiColors.HealerGreen : ImGuiColors.DalamudRed);
|
UiSharedService.ColorText(status, (Pair.IsVisible || Pair.IsOnline) ? ImGuiColors.HealerGreen : UiSharedService.AccentColor);
|
||||||
if (Pair.IsVisible)
|
if (Pair.IsVisible)
|
||||||
{
|
{
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
@@ -374,7 +402,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
|||||||
ImGui.TextUnformatted("Sound Sync");
|
ImGui.TextUnformatted("Sound Sync");
|
||||||
_uiSharedService.BooleanToColoredIcon(!isDisableSounds);
|
_uiSharedService.BooleanToColoredIcon(!isDisableSounds);
|
||||||
ImGui.SameLine(230);
|
ImGui.SameLine(230);
|
||||||
if (_uiSharedService.IconTextButton(isDisableSounds ? FontAwesomeIcon.VolumeUp : FontAwesomeIcon.VolumeMute,
|
if (_uiSharedService.IconTextButton(isDisableSounds ? FontAwesomeIcon.VolumeMute : FontAwesomeIcon.VolumeUp,
|
||||||
isDisableSounds ? "Enable sound sync" : "Disable sound sync"))
|
isDisableSounds ? "Enable sound sync" : "Disable sound sync"))
|
||||||
{
|
{
|
||||||
perm.SetDisableSounds(!perm.IsDisableSounds());
|
perm.SetDisableSounds(!perm.IsDisableSounds());
|
||||||
@@ -385,7 +413,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
|||||||
ImGui.TextUnformatted("Animation Sync");
|
ImGui.TextUnformatted("Animation Sync");
|
||||||
_uiSharedService.BooleanToColoredIcon(!isDisableAnimations);
|
_uiSharedService.BooleanToColoredIcon(!isDisableAnimations);
|
||||||
ImGui.SameLine(230);
|
ImGui.SameLine(230);
|
||||||
if (_uiSharedService.IconTextButton(isDisableAnimations ? FontAwesomeIcon.Running : FontAwesomeIcon.Stop,
|
if (_uiSharedService.IconTextButton(isDisableAnimations ? FontAwesomeIcon.WindowClose : FontAwesomeIcon.Running,
|
||||||
isDisableAnimations ? "Enable animation sync" : "Disable animation sync"))
|
isDisableAnimations ? "Enable animation sync" : "Disable animation sync"))
|
||||||
{
|
{
|
||||||
perm.SetDisableAnimations(!perm.IsDisableAnimations());
|
perm.SetDisableAnimations(!perm.IsDisableAnimations());
|
||||||
@@ -396,7 +424,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
|||||||
ImGui.TextUnformatted("VFX Sync");
|
ImGui.TextUnformatted("VFX Sync");
|
||||||
_uiSharedService.BooleanToColoredIcon(!isDisableVfx);
|
_uiSharedService.BooleanToColoredIcon(!isDisableVfx);
|
||||||
ImGui.SameLine(230);
|
ImGui.SameLine(230);
|
||||||
if (_uiSharedService.IconTextButton(isDisableVfx ? FontAwesomeIcon.Sun : FontAwesomeIcon.Circle,
|
if (_uiSharedService.IconTextButton(isDisableVfx ? FontAwesomeIcon.TimesCircle : FontAwesomeIcon.Sun,
|
||||||
isDisableVfx ? "Enable VFX sync" : "Disable VFX sync"))
|
isDisableVfx ? "Enable VFX sync" : "Disable VFX sync"))
|
||||||
{
|
{
|
||||||
perm.SetDisableVFX(!perm.IsDisableVFX());
|
perm.SetDisableVFX(!perm.IsDisableVFX());
|
||||||
@@ -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));
|
||||||
|
|||||||
533
MareSynchronos/UI/TypingIndicatorOverlay.cs
Normal file
533
MareSynchronos/UI/TypingIndicatorOverlay.cs
Normal file
@@ -0,0 +1,533 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Numerics;
|
||||||
|
using System.Linq;
|
||||||
|
using Dalamud.Bindings.ImGui;
|
||||||
|
using Dalamud.Interface.Utility;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using MareSynchronos.API.Data;
|
||||||
|
using MareSynchronos.API.Data.Extensions;
|
||||||
|
using MareSynchronos.MareConfiguration;
|
||||||
|
using MareSynchronos.MareConfiguration.Models;
|
||||||
|
using MareSynchronos.PlayerData.Pairs;
|
||||||
|
using MareSynchronos.Services;
|
||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using MareSynchronos.WebAPI;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Dalamud.Interface.Textures.TextureWraps;
|
||||||
|
using FFXIVClientStructs.Interop;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.System.Framework;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||||
|
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||||
|
|
||||||
|
namespace MareSynchronos.UI;
|
||||||
|
|
||||||
|
public sealed class TypingIndicatorOverlay : WindowMediatorSubscriberBase
|
||||||
|
{
|
||||||
|
private const int NameplateIconId = 61397;
|
||||||
|
private static readonly TimeSpan TypingDisplayTime = TimeSpan.FromSeconds(2);
|
||||||
|
private static readonly TimeSpan TypingDisplayDelay = TimeSpan.FromMilliseconds(500);
|
||||||
|
private static readonly TimeSpan TypingDisplayFade = TypingDisplayTime;
|
||||||
|
|
||||||
|
private readonly ILogger<TypingIndicatorOverlay> _typedLogger;
|
||||||
|
private readonly MareConfigService _configService;
|
||||||
|
private readonly IGameGui _gameGui;
|
||||||
|
private readonly ITextureProvider _textureProvider;
|
||||||
|
private readonly IClientState _clientState;
|
||||||
|
private readonly PairManager _pairManager;
|
||||||
|
private readonly IPartyList _partyList;
|
||||||
|
private readonly IObjectTable _objectTable;
|
||||||
|
private readonly DalamudUtilService _dalamudUtil;
|
||||||
|
private readonly TypingIndicatorStateService _typingStateService;
|
||||||
|
private readonly ApiController _apiController;
|
||||||
|
|
||||||
|
public TypingIndicatorOverlay(ILogger<TypingIndicatorOverlay> logger, MareMediator mediator, PerformanceCollectorService performanceCollectorService,
|
||||||
|
MareConfigService configService, IGameGui gameGui, ITextureProvider textureProvider, IClientState clientState,
|
||||||
|
IPartyList partyList, IObjectTable objectTable, DalamudUtilService dalamudUtil, PairManager pairManager,
|
||||||
|
TypingIndicatorStateService typingStateService, ApiController apiController)
|
||||||
|
: base(logger, mediator, nameof(TypingIndicatorOverlay), performanceCollectorService)
|
||||||
|
{
|
||||||
|
_typedLogger = logger;
|
||||||
|
_configService = configService;
|
||||||
|
_gameGui = gameGui;
|
||||||
|
_textureProvider = textureProvider;
|
||||||
|
_clientState = clientState;
|
||||||
|
_partyList = partyList;
|
||||||
|
_objectTable = objectTable;
|
||||||
|
_dalamudUtil = dalamudUtil;
|
||||||
|
_pairManager = pairManager;
|
||||||
|
_typingStateService = typingStateService;
|
||||||
|
_apiController = apiController;
|
||||||
|
|
||||||
|
RespectCloseHotkey = false;
|
||||||
|
IsOpen = true;
|
||||||
|
Flags |= ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoSavedSettings | ImGuiWindowFlags.NoFocusOnAppearing
|
||||||
|
| ImGuiWindowFlags.NoBackground | ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.NoNav;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void DrawInternal()
|
||||||
|
{
|
||||||
|
var viewport = ImGui.GetMainViewport();
|
||||||
|
ImGuiHelpers.ForceNextWindowMainViewport();
|
||||||
|
ImGui.SetWindowPos(viewport.Pos);
|
||||||
|
ImGui.SetWindowSize(viewport.Size);
|
||||||
|
|
||||||
|
if (!_clientState.IsLoggedIn)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var showParty = _configService.Current.TypingIndicatorShowOnPartyList;
|
||||||
|
var showNameplates = _configService.Current.TypingIndicatorShowOnNameplates;
|
||||||
|
|
||||||
|
if (!showParty && !showNameplates)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var overlayDrawList = ImGui.GetWindowDrawList();
|
||||||
|
var activeTypers = _typingStateService.GetActiveTypers(TypingDisplayTime);
|
||||||
|
var hasSelf = _typingStateService.TryGetSelfTyping(TypingDisplayTime, out var selfStart, out var selfLast);
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
|
||||||
|
if (showParty)
|
||||||
|
{
|
||||||
|
DrawPartyIndicators(overlayDrawList, activeTypers, hasSelf, now, selfStart, selfLast);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showNameplates)
|
||||||
|
{
|
||||||
|
DrawNameplateIndicators(ImGui.GetWindowDrawList(), activeTypers, hasSelf, now, selfStart, selfLast);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe void DrawPartyIndicators(ImDrawListPtr drawList, IReadOnlyDictionary<string, (UserData User, DateTime FirstSeen, DateTime LastUpdate)> activeTypers,
|
||||||
|
bool selfActive, DateTime now, DateTime selfStart, DateTime selfLast)
|
||||||
|
{
|
||||||
|
var partyAddon = (AtkUnitBase*)_gameGui.GetAddonByName("_PartyList", 1).Address;
|
||||||
|
if (partyAddon == null || !partyAddon->IsVisible)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (selfActive
|
||||||
|
&& (now - selfStart) >= TypingDisplayDelay
|
||||||
|
&& (now - selfLast) <= TypingDisplayFade)
|
||||||
|
{
|
||||||
|
DrawPartyMemberTyping(drawList, partyAddon, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var (uid, entry) in activeTypers)
|
||||||
|
{
|
||||||
|
if ((now - entry.LastUpdate) > TypingDisplayFade)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var pair = _pairManager.GetPairByUID(uid);
|
||||||
|
var targetIndex = -1;
|
||||||
|
var playerName = pair?.PlayerName;
|
||||||
|
var objectId = pair?.PlayerCharacterId ?? uint.MaxValue;
|
||||||
|
|
||||||
|
if (objectId != 0 && objectId != uint.MaxValue)
|
||||||
|
{
|
||||||
|
targetIndex = GetPartyIndexForObjectId(objectId);
|
||||||
|
if (targetIndex >= 0 && !string.IsNullOrEmpty(playerName))
|
||||||
|
{
|
||||||
|
var member = _partyList[targetIndex];
|
||||||
|
var memberName = member?.Name?.TextValue;
|
||||||
|
if (!string.IsNullOrEmpty(memberName) && !memberName.Equals(playerName, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var nameIndex = GetPartyIndexForName(playerName);
|
||||||
|
targetIndex = nameIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetIndex < 0 && !string.IsNullOrEmpty(playerName))
|
||||||
|
{
|
||||||
|
targetIndex = GetPartyIndexForName(playerName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetIndex < 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
DrawPartyMemberTyping(drawList, partyAddon, targetIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe void DrawPartyMemberTyping(ImDrawListPtr drawList, AtkUnitBase* partyList, int memberIndex)
|
||||||
|
{
|
||||||
|
if (memberIndex < 0 || memberIndex > 7) return;
|
||||||
|
|
||||||
|
var nodeIndex = 23 - memberIndex;
|
||||||
|
if (partyList->UldManager.NodeListCount <= nodeIndex) return;
|
||||||
|
|
||||||
|
var memberNode = (AtkComponentNode*)partyList->UldManager.NodeList[nodeIndex];
|
||||||
|
if (memberNode == null || !memberNode->AtkResNode.IsVisible()) return;
|
||||||
|
|
||||||
|
var iconNode = memberNode->Component->UldManager.NodeListCount > 4 ? memberNode->Component->UldManager.NodeList[4] : null;
|
||||||
|
if (iconNode == null) return;
|
||||||
|
|
||||||
|
var align = partyList->UldManager.NodeList[3]->Y;
|
||||||
|
var partyScale = partyList->Scale;
|
||||||
|
|
||||||
|
var iconOffset = new Vector2(-14, 8) * partyScale;
|
||||||
|
var iconSize = new Vector2(iconNode->Width / 2f, iconNode->Height / 2f) * partyScale;
|
||||||
|
|
||||||
|
var iconPos = new Vector2(
|
||||||
|
partyList->X + (memberNode->AtkResNode.X * partyScale) + (iconNode->X * partyScale) + (iconNode->Width * partyScale / 2f),
|
||||||
|
partyList->Y + align + (memberNode->AtkResNode.Y * partyScale) + (iconNode->Y * partyScale) + (iconNode->Height * partyScale / 2f));
|
||||||
|
|
||||||
|
iconPos += iconOffset;
|
||||||
|
|
||||||
|
var texture = _textureProvider.GetFromGame("ui/uld/charamake_dataimport.tex").GetWrapOrEmpty();
|
||||||
|
if (texture == null) return;
|
||||||
|
|
||||||
|
drawList.AddImage(texture.Handle, iconPos, iconPos + iconSize, Vector2.Zero, Vector2.One,
|
||||||
|
ImGui.ColorConvertFloat4ToU32(new Vector4(1f, 1f, 1f, 0.9f)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe void DrawNameplateIndicators(ImDrawListPtr drawList, IReadOnlyDictionary<string, (UserData User, DateTime FirstSeen, DateTime LastUpdate)> activeTypers,
|
||||||
|
bool selfActive, DateTime now, DateTime selfStart, DateTime selfLast)
|
||||||
|
{
|
||||||
|
var iconWrap = _textureProvider.GetFromGameIcon(NameplateIconId).GetWrapOrEmpty();
|
||||||
|
if (iconWrap == null || iconWrap.Handle == IntPtr.Zero)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (selfActive
|
||||||
|
&& _clientState.LocalPlayer != null
|
||||||
|
&& (now - selfStart) >= TypingDisplayDelay
|
||||||
|
&& (now - selfLast) <= TypingDisplayFade)
|
||||||
|
{
|
||||||
|
var selfId = GetEntityId(_clientState.LocalPlayer.Address);
|
||||||
|
if (selfId != 0 && !TryDrawNameplateBubble(drawList, iconWrap, selfId))
|
||||||
|
{
|
||||||
|
DrawWorldFallbackIcon(drawList, iconWrap, _clientState.LocalPlayer.Position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var (uid, entry) in activeTypers)
|
||||||
|
{
|
||||||
|
if ((now - entry.LastUpdate) > TypingDisplayFade)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (string.Equals(uid, _apiController.UID, StringComparison.Ordinal))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var pair = _pairManager.GetPairByUID(uid);
|
||||||
|
var objectId = pair?.PlayerCharacterId ?? 0;
|
||||||
|
var pairName = pair?.PlayerName ?? entry.User.AliasOrUID ?? string.Empty;
|
||||||
|
var pairIdent = pair?.Ident ?? string.Empty;
|
||||||
|
var isPartyMember = IsPartyMember(objectId, pairName);
|
||||||
|
var isRelevantMember = IsPlayerRelevant(pair, isPartyMember);
|
||||||
|
|
||||||
|
if (objectId != uint.MaxValue && objectId != 0 && TryDrawNameplateBubble(drawList, iconWrap, objectId))
|
||||||
|
{
|
||||||
|
_typedLogger.LogTrace("TypingIndicator: drew nameplate bubble for {uid} (objectId={objectId})", uid, objectId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasWorldPosition = TryResolveWorldPosition(pair, entry.User, objectId, out var worldPos);
|
||||||
|
var isNearby = hasWorldPosition && IsWithinRelevantDistance(worldPos);
|
||||||
|
|
||||||
|
if (!isRelevantMember && !isNearby)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (pair == null)
|
||||||
|
{
|
||||||
|
_typedLogger.LogTrace("TypingIndicator: no pair found for {uid}, attempting fallback", uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
_typedLogger.LogTrace("TypingIndicator: fallback draw for {uid} (objectId={objectId}, name={name}, ident={ident})",
|
||||||
|
uid, objectId, pairName, pairIdent);
|
||||||
|
|
||||||
|
if (hasWorldPosition)
|
||||||
|
{
|
||||||
|
DrawWorldFallbackIcon(drawList, iconWrap, worldPos);
|
||||||
|
_typedLogger.LogTrace("TypingIndicator: fallback world draw for {uid} at {pos}", uid, worldPos);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_typedLogger.LogTrace("TypingIndicator: could not resolve position for {uid}", uid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Vector2 GetConfiguredBubbleSize(float scaleX, float scaleY, bool isNameplateVisible, TypingIndicatorBubbleSize? overrideSize = null)
|
||||||
|
{
|
||||||
|
var sizeSetting = overrideSize ?? _configService.Current.TypingIndicatorBubbleSize;
|
||||||
|
var baseSize = sizeSetting switch
|
||||||
|
{
|
||||||
|
TypingIndicatorBubbleSize.Small when isNameplateVisible => 32f,
|
||||||
|
TypingIndicatorBubbleSize.Medium when isNameplateVisible => 44f,
|
||||||
|
TypingIndicatorBubbleSize.Large when isNameplateVisible => 56f,
|
||||||
|
TypingIndicatorBubbleSize.Small => 15f,
|
||||||
|
TypingIndicatorBubbleSize.Medium => 25f,
|
||||||
|
TypingIndicatorBubbleSize.Large => 35f,
|
||||||
|
_ => 35f,
|
||||||
|
};
|
||||||
|
|
||||||
|
return new Vector2(baseSize * scaleX, baseSize * scaleY);
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe bool TryDrawNameplateBubble(ImDrawListPtr drawList, IDalamudTextureWrap textureWrap, uint objectId)
|
||||||
|
{
|
||||||
|
if (textureWrap == null || textureWrap.Handle == IntPtr.Zero)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var framework = Framework.Instance();
|
||||||
|
if (framework == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var ui3D = framework->GetUIModule()->GetUI3DModule();
|
||||||
|
if (ui3D == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var addonNamePlate = (AddonNamePlate*)_gameGui.GetAddonByName("NamePlate", 1).Address;
|
||||||
|
if (addonNamePlate == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
AddonNamePlate.NamePlateObject* namePlate = null;
|
||||||
|
float distance = 0f;
|
||||||
|
|
||||||
|
for (var i = 0; i < ui3D->NamePlateObjectInfoCount; i++)
|
||||||
|
{
|
||||||
|
var objectInfo = ui3D->NamePlateObjectInfoPointers[i];
|
||||||
|
if (objectInfo.Value == null || objectInfo.Value->GameObject == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (objectInfo.Value->GameObject->EntityId != objectId)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (objectInfo.Value->GameObject->YalmDistanceFromPlayerX > 35f)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
namePlate = &addonNamePlate->NamePlateObjectArray[objectInfo.Value->NamePlateIndex];
|
||||||
|
distance = objectInfo.Value->GameObject->YalmDistanceFromPlayerX;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (namePlate == null || namePlate->RootComponentNode == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var iconNode = namePlate->RootComponentNode->Component->UldManager.NodeList[0];
|
||||||
|
if (iconNode == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var scaleX = namePlate->RootComponentNode->AtkResNode.ScaleX;
|
||||||
|
var scaleY = namePlate->RootComponentNode->AtkResNode.ScaleY;
|
||||||
|
var iconVisible = iconNode->IsVisible();
|
||||||
|
var sizeScaleFactor = 1f;
|
||||||
|
var scaleVector = new Vector2(scaleX, scaleY);
|
||||||
|
var rootPosition = new Vector2(namePlate->RootComponentNode->AtkResNode.X, namePlate->RootComponentNode->AtkResNode.Y);
|
||||||
|
var iconLocalPosition = new Vector2(iconNode->X, iconNode->Y) * scaleVector;
|
||||||
|
var iconDimensions = new Vector2(iconNode->Width, iconNode->Height) * scaleVector;
|
||||||
|
|
||||||
|
if (!iconVisible)
|
||||||
|
{
|
||||||
|
sizeScaleFactor = 2.5f;
|
||||||
|
var anchor = rootPosition + iconLocalPosition + new Vector2(iconDimensions.X * 0.5f, 0f);
|
||||||
|
|
||||||
|
var distanceOffset = new Vector2(0f, -16f + distance) * scaleVector;
|
||||||
|
if (iconNode->Height == 24)
|
||||||
|
{
|
||||||
|
distanceOffset.Y += 16f * scaleY;
|
||||||
|
}
|
||||||
|
distanceOffset.Y += 64f * scaleY;
|
||||||
|
|
||||||
|
var referenceSize = GetConfiguredBubbleSize(scaleX * sizeScaleFactor, scaleY * sizeScaleFactor, false, TypingIndicatorBubbleSize.Small);
|
||||||
|
var manualOffset = new Vector2(referenceSize.X * 2.00f, referenceSize.Y * 2.00f);
|
||||||
|
|
||||||
|
var iconSizeHidden = GetConfiguredBubbleSize(scaleX * sizeScaleFactor, scaleY * sizeScaleFactor, false);
|
||||||
|
var center = anchor + distanceOffset + manualOffset;
|
||||||
|
var topLeft = center - (iconSizeHidden / 2f);
|
||||||
|
|
||||||
|
drawList.AddImage(textureWrap.Handle, topLeft, topLeft + iconSizeHidden, Vector2.Zero, Vector2.One,
|
||||||
|
ImGui.ColorConvertFloat4ToU32(new Vector4(1f, 1f, 1f, 0.95f)));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var iconPos = rootPosition + iconLocalPosition + new Vector2(iconDimensions.X, 0f);
|
||||||
|
|
||||||
|
var iconOffset = new Vector2(distance / 1.5f, distance / 3.5f) * scaleVector;
|
||||||
|
if (iconNode->Height == 24)
|
||||||
|
{
|
||||||
|
iconOffset.Y -= 8f * scaleY;
|
||||||
|
}
|
||||||
|
|
||||||
|
iconPos += iconOffset;
|
||||||
|
|
||||||
|
var iconSize = GetConfiguredBubbleSize(scaleX * sizeScaleFactor, scaleY * sizeScaleFactor, true);
|
||||||
|
|
||||||
|
drawList.AddImage(textureWrap.Handle, iconPos, iconPos + iconSize, Vector2.Zero, Vector2.One,
|
||||||
|
ImGui.ColorConvertFloat4ToU32(new Vector4(1f, 1f, 1f, 0.95f)));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawWorldFallbackIcon(ImDrawListPtr drawList, IDalamudTextureWrap textureWrap, Vector3 worldPosition)
|
||||||
|
{
|
||||||
|
var offsetPosition = worldPosition + new Vector3(0f, 1.8f, 0f);
|
||||||
|
if (!_gameGui.WorldToScreen(offsetPosition, out var screenPos))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var iconSize = GetConfiguredBubbleSize(ImGuiHelpers.GlobalScale, ImGuiHelpers.GlobalScale, false);
|
||||||
|
var iconPos = screenPos - (iconSize / 2f) - new Vector2(0f, iconSize.Y * 0.6f);
|
||||||
|
drawList.AddImage(textureWrap.Handle, iconPos, iconPos + iconSize, Vector2.Zero, Vector2.One,
|
||||||
|
ImGui.ColorConvertFloat4ToU32(new Vector4(1f, 1f, 1f, 0.95f)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryGetWorldPosition(uint objectId, out Vector3 position)
|
||||||
|
{
|
||||||
|
position = Vector3.Zero;
|
||||||
|
if (objectId == 0 || objectId == uint.MaxValue)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
foreach (var obj in _objectTable)
|
||||||
|
{
|
||||||
|
if (obj?.EntityId == objectId)
|
||||||
|
{
|
||||||
|
position = obj.Position;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryResolveWorldPosition(Pair? pair, UserData userData, uint objectId, out Vector3 position)
|
||||||
|
{
|
||||||
|
if (TryGetWorldPosition(objectId, out position))
|
||||||
|
{
|
||||||
|
_typedLogger.LogTrace("TypingIndicator: resolved by objectId {objectId}", objectId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pair != null)
|
||||||
|
{
|
||||||
|
var name = pair.PlayerName;
|
||||||
|
if (!string.IsNullOrEmpty(name) && TryGetWorldPositionByName(name!, out position))
|
||||||
|
{
|
||||||
|
_typedLogger.LogTrace("TypingIndicator: resolved by pair name {name}", name);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var ident = pair.Ident;
|
||||||
|
if (!string.IsNullOrEmpty(ident))
|
||||||
|
{
|
||||||
|
var cached = _dalamudUtil.FindPlayerByNameHash(ident);
|
||||||
|
if (!string.IsNullOrEmpty(cached.Name) && TryGetWorldPositionByName(cached.Name, out position))
|
||||||
|
{
|
||||||
|
_typedLogger.LogTrace("TypingIndicator: resolved by cached name {name}", cached.Name);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cached.Address != IntPtr.Zero)
|
||||||
|
{
|
||||||
|
var objRef = _objectTable.CreateObjectReference(cached.Address);
|
||||||
|
if (objRef != null)
|
||||||
|
{
|
||||||
|
position = objRef.Position;
|
||||||
|
_typedLogger.LogTrace("TypingIndicator: resolved by cached address {addr}", cached.Address);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var alias = userData.AliasOrUID;
|
||||||
|
if (!string.IsNullOrEmpty(alias) && TryGetWorldPositionByName(alias, out position))
|
||||||
|
{
|
||||||
|
_typedLogger.LogTrace("TypingIndicator: resolved by user alias {alias}", alias);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryGetWorldPositionByName(string name, out Vector3 position)
|
||||||
|
{
|
||||||
|
position = Vector3.Zero;
|
||||||
|
foreach (var obj in _objectTable)
|
||||||
|
{
|
||||||
|
if (obj != null && obj.Name.TextValue.Equals(name, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
position = obj.Position;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int GetPartyIndexForObjectId(uint objectId)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < _partyList.Count; ++i)
|
||||||
|
{
|
||||||
|
var member = _partyList[i];
|
||||||
|
if (member == null) continue;
|
||||||
|
|
||||||
|
var gameObject = member.GameObject;
|
||||||
|
if (gameObject != null && GetEntityId(gameObject.Address) == objectId)
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int GetPartyIndexForName(string name)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < _partyList.Count; ++i)
|
||||||
|
{
|
||||||
|
var member = _partyList[i];
|
||||||
|
if (member?.Name == null) continue;
|
||||||
|
|
||||||
|
if (member.Name.TextValue.Equals(name, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsPartyMember(uint objectId, string? playerName)
|
||||||
|
{
|
||||||
|
if (objectId != 0 && objectId != uint.MaxValue && GetPartyIndexForObjectId(objectId) >= 0)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(playerName) && GetPartyIndexForName(playerName) >= 0)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsPlayerRelevant(Pair? pair, bool isPartyMember)
|
||||||
|
{
|
||||||
|
if (isPartyMember)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (pair?.UserPair != null)
|
||||||
|
{
|
||||||
|
var userPair = pair.UserPair;
|
||||||
|
if (userPair.OtherPermissions.IsPaired() || userPair.OwnPermissions.IsPaired())
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pair?.GroupPair != null && pair.GroupPair.Any(g =>
|
||||||
|
!g.Value.GroupUserPermissions.IsPaused() &&
|
||||||
|
!g.Key.GroupUserPermissions.IsPaused()))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsWithinRelevantDistance(Vector3 position)
|
||||||
|
{
|
||||||
|
if (_clientState.LocalPlayer == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var distance = Vector3.Distance(_clientState.LocalPlayer.Position, position);
|
||||||
|
return distance <= 40f;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static unsafe uint GetEntityId(nint address)
|
||||||
|
{
|
||||||
|
if (address == nint.Zero) return 0;
|
||||||
|
return ((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)address)->EntityId;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,8 @@ using MareSynchronos.Services.Mediator;
|
|||||||
using MareSynchronos.Services.ServerConfiguration;
|
using MareSynchronos.Services.ServerConfiguration;
|
||||||
using MareSynchronos.WebAPI;
|
using MareSynchronos.WebAPI;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
@@ -36,7 +38,11 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
|||||||
ImGuiWindowFlags.NoScrollbar |
|
ImGuiWindowFlags.NoScrollbar |
|
||||||
ImGuiWindowFlags.NoScrollWithMouse;
|
ImGuiWindowFlags.NoScrollWithMouse;
|
||||||
|
|
||||||
|
public const float ContentFontScale = 0.92f;
|
||||||
|
|
||||||
public static Vector4 AccentColor { get; set; } = ImGuiColors.DalamudViolet;
|
public static Vector4 AccentColor { get; set; } = ImGuiColors.DalamudViolet;
|
||||||
|
public static Vector4 AccentHoverColor { get; set; } = new Vector4(0x3A / 255f, 0x15 / 255f, 0x50 / 255f, 1f);
|
||||||
|
public static Vector4 AccentActiveColor { get; set; } = AccentHoverColor;
|
||||||
|
|
||||||
public readonly FileDialogManager FileDialogManager;
|
public readonly FileDialogManager FileDialogManager;
|
||||||
|
|
||||||
@@ -57,6 +63,8 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
|||||||
private readonly Dictionary<string, object> _selectedComboItems = new(StringComparer.Ordinal);
|
private readonly Dictionary<string, object> _selectedComboItems = new(StringComparer.Ordinal);
|
||||||
private readonly ServerConfigurationManager _serverConfigurationManager;
|
private readonly ServerConfigurationManager _serverConfigurationManager;
|
||||||
private bool _cacheDirectoryHasOtherFilesThanCache = false;
|
private bool _cacheDirectoryHasOtherFilesThanCache = false;
|
||||||
|
private static readonly Stack<float> _fontScaleStack = new();
|
||||||
|
private static float _currentWindowFontScale = 1f;
|
||||||
|
|
||||||
private bool _cacheDirectoryIsValidPath = true;
|
private bool _cacheDirectoryIsValidPath = true;
|
||||||
|
|
||||||
@@ -115,8 +123,12 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
e.OnPreBuild(tk => tk.AddDalamudAssetFont(Dalamud.DalamudAsset.NotoSansJpMedium, new()
|
e.OnPreBuild(tk => tk.AddDalamudAssetFont(Dalamud.DalamudAsset.NotoSansJpMedium, new()
|
||||||
{
|
{
|
||||||
SizePx = 35,
|
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));
|
||||||
@@ -136,6 +148,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
|||||||
public string PlayerName => _dalamudUtil.GetPlayerName();
|
public string PlayerName => _dalamudUtil.GetPlayerName();
|
||||||
|
|
||||||
public IFontHandle UidFont { get; init; }
|
public IFontHandle UidFont { get; init; }
|
||||||
|
public MareConfigService ConfigService => _configService;
|
||||||
public Dictionary<ushort, string> WorldData => _dalamudUtil.WorldData.Value;
|
public Dictionary<ushort, string> WorldData => _dalamudUtil.WorldData.Value;
|
||||||
|
|
||||||
public uint WorldId => _dalamudUtil.GetHomeWorldId();
|
public uint WorldId => _dalamudUtil.GetHomeWorldId();
|
||||||
@@ -213,7 +226,38 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
public static bool CtrlPressed() => (GetKeyState(0xA2) & 0x8000) != 0 || (GetKeyState(0xA3) & 0x8000) != 0;
|
public static bool CtrlPressed() => (GetKeyState(0xA2) & 0x8000) != 0 || (GetKeyState(0xA3) & 0x8000) != 0;
|
||||||
|
|
||||||
public static void DrawGrouped(Action imguiDrawAction, float rounding = 5f, float? expectedWidth = null)
|
public static IDisposable PushFontScale(float scale)
|
||||||
|
{
|
||||||
|
var previous = _currentWindowFontScale;
|
||||||
|
_fontScaleStack.Push(previous);
|
||||||
|
if (Math.Abs(previous - scale) > float.Epsilon)
|
||||||
|
{
|
||||||
|
SetFontScale(scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new FontScaleScope();
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class FontScaleScope : IDisposable
|
||||||
|
{
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_fontScaleStack.Count == 0) return;
|
||||||
|
var previous = _fontScaleStack.Pop();
|
||||||
|
if (Math.Abs(previous - _currentWindowFontScale) > float.Epsilon)
|
||||||
|
{
|
||||||
|
SetFontScale(previous);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void SetFontScale(float scale)
|
||||||
|
{
|
||||||
|
ImGui.SetWindowFontScale(scale);
|
||||||
|
_currentWindowFontScale = scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void DrawGrouped(Action imguiDrawAction, float rounding = 5f, float? expectedWidth = null, bool drawBorder = true)
|
||||||
{
|
{
|
||||||
var cursorPos = ImGui.GetCursorPos();
|
var cursorPos = ImGui.GetCursorPos();
|
||||||
using (ImRaii.Group())
|
using (ImRaii.Group())
|
||||||
@@ -227,11 +271,129 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
|||||||
imguiDrawAction.Invoke();
|
imguiDrawAction.Invoke();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (drawBorder)
|
||||||
|
{
|
||||||
ImGui.GetWindowDrawList().AddRect(
|
ImGui.GetWindowDrawList().AddRect(
|
||||||
ImGui.GetItemRectMin() - ImGui.GetStyle().ItemInnerSpacing,
|
ImGui.GetItemRectMin() - ImGui.GetStyle().ItemInnerSpacing,
|
||||||
ImGui.GetItemRectMax() + ImGui.GetStyle().ItemInnerSpacing,
|
ImGui.GetItemRectMax() + ImGui.GetStyle().ItemInnerSpacing,
|
||||||
Color(ImGuiColors.DalamudGrey2), rounding);
|
Color(ImGuiColors.DalamudGrey2), rounding);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void DrawCard(string id, Action draw, Vector2? padding = null, Vector4? background = null,
|
||||||
|
Vector4? border = null, float? rounding = null, bool stretchWidth = false)
|
||||||
|
{
|
||||||
|
var style = ImGui.GetStyle();
|
||||||
|
var padBase = style.FramePadding;
|
||||||
|
var pad = padding ?? new Vector2(
|
||||||
|
padBase.X + 4f * ImGuiHelpers.GlobalScale,
|
||||||
|
padBase.Y + 3f * ImGuiHelpers.GlobalScale);
|
||||||
|
var cardBg = background ?? new Vector4(0.08f, 0.08f, 0.10f, 0.94f);
|
||||||
|
var cardBorder = border ?? new Vector4(0f, 0f, 0f, 0.85f);
|
||||||
|
float cardRounding = rounding ?? Math.Max(style.FrameRounding, 8f * ImGuiHelpers.GlobalScale);
|
||||||
|
float borderThickness = Math.Max(1f, Math.Max(style.FrameBorderSize, 1f) * ImGuiHelpers.GlobalScale);
|
||||||
|
float borderInset = borderThickness;
|
||||||
|
|
||||||
|
var originalCursor = ImGui.GetCursorPos();
|
||||||
|
if (stretchWidth)
|
||||||
|
{
|
||||||
|
ImGui.SetCursorPosX(ImGui.GetWindowContentRegionMin().X);
|
||||||
|
}
|
||||||
|
|
||||||
|
var startCursor = ImGui.GetCursorPos();
|
||||||
|
var drawList = ImGui.GetWindowDrawList();
|
||||||
|
drawList.ChannelsSplit(2);
|
||||||
|
drawList.ChannelsSetCurrent(1);
|
||||||
|
|
||||||
|
ImGui.PushID(id);
|
||||||
|
ImGui.SetCursorPos(new Vector2(startCursor.X + pad.X, startCursor.Y + pad.Y));
|
||||||
|
ImGui.BeginGroup();
|
||||||
|
draw();
|
||||||
|
ImGui.EndGroup();
|
||||||
|
ImGui.PopID();
|
||||||
|
|
||||||
|
var contentMin = ImGui.GetItemRectMin();
|
||||||
|
var contentMax = ImGui.GetItemRectMax();
|
||||||
|
var cardMin = contentMin - pad;
|
||||||
|
var cardMax = contentMax + pad;
|
||||||
|
var outerMin = cardMin;
|
||||||
|
var outerMax = cardMax;
|
||||||
|
|
||||||
|
if (stretchWidth)
|
||||||
|
{
|
||||||
|
var windowPos = ImGui.GetWindowPos();
|
||||||
|
var regionMin = ImGui.GetWindowContentRegionMin();
|
||||||
|
var regionMax = ImGui.GetWindowContentRegionMax();
|
||||||
|
var scrollX = ImGui.GetScrollX();
|
||||||
|
cardMin.X = windowPos.X + regionMin.X + scrollX;
|
||||||
|
cardMax.X = windowPos.X + regionMax.X + scrollX;
|
||||||
|
outerMin.X = cardMin.X;
|
||||||
|
outerMax.X = cardMax.X;
|
||||||
|
startCursor.X = ImGui.GetWindowContentRegionMin().X;
|
||||||
|
}
|
||||||
|
|
||||||
|
var drawMin = new Vector2(cardMin.X + borderInset, cardMin.Y + borderInset);
|
||||||
|
var drawMax = new Vector2(cardMax.X - borderInset, cardMax.Y - borderInset);
|
||||||
|
var clipMin = drawList.GetClipRectMin();
|
||||||
|
var clipMax = drawList.GetClipRectMax();
|
||||||
|
var clipInset = new Vector2(borderThickness * 0.5f + 0.5f, borderThickness * 0.5f + 0.5f);
|
||||||
|
drawMin = Vector2.Max(drawMin, clipMin + clipInset);
|
||||||
|
drawMax = Vector2.Min(drawMax, clipMax - clipInset);
|
||||||
|
if (drawMax.X <= drawMin.X)
|
||||||
|
{
|
||||||
|
drawMax.X = drawMin.X + borderThickness;
|
||||||
|
}
|
||||||
|
if (drawMax.Y <= drawMin.Y)
|
||||||
|
{
|
||||||
|
drawMax.Y = drawMin.Y + borderThickness;
|
||||||
|
}
|
||||||
|
|
||||||
|
drawList.ChannelsSetCurrent(0);
|
||||||
|
drawList.AddRectFilled(drawMin, drawMax, ImGui.ColorConvertFloat4ToU32(cardBg), cardRounding);
|
||||||
|
if (cardBorder.W > 0f && borderThickness > 0f)
|
||||||
|
{
|
||||||
|
drawList.AddRect(drawMin, drawMax, ImGui.ColorConvertFloat4ToU32(cardBorder), cardRounding, ImDrawFlags.None, borderThickness);
|
||||||
|
}
|
||||||
|
drawList.ChannelsMerge();
|
||||||
|
|
||||||
|
ImGui.SetCursorPos(startCursor);
|
||||||
|
var dummyWidth = outerMax.X - outerMin.X;
|
||||||
|
var dummyHeight = outerMax.Y - outerMin.Y;
|
||||||
|
ImGui.Dummy(new Vector2(dummyWidth, dummyHeight));
|
||||||
|
ImGui.SetCursorPos(new Vector2(startCursor.X, startCursor.Y + dummyHeight));
|
||||||
|
|
||||||
|
if (!stretchWidth)
|
||||||
|
{
|
||||||
|
ImGui.SetCursorPosX(originalCursor.X);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ImGui.SetCursorPosX(startCursor.X);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool DrawArrowToggle(ref bool state, string id)
|
||||||
|
{
|
||||||
|
var framePadding = ImGui.GetStyle().FramePadding;
|
||||||
|
ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, new Vector2(framePadding.X, framePadding.Y * 0.85f));
|
||||||
|
ImGui.PushStyleColor(ImGuiCol.Button, Vector4.Zero);
|
||||||
|
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, new Vector4(1f, 1f, 1f, 0.08f));
|
||||||
|
ImGui.PushStyleColor(ImGuiCol.ButtonActive, new Vector4(1f, 1f, 1f, 0.16f));
|
||||||
|
bool clicked = ImGui.ArrowButton(id, state ? ImGuiDir.Down : ImGuiDir.Right);
|
||||||
|
ImGui.PopStyleColor(3);
|
||||||
|
ImGui.PopStyleVar();
|
||||||
|
if (clicked)
|
||||||
|
{
|
||||||
|
state = !state;
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float GetCardContentPaddingX()
|
||||||
|
{
|
||||||
|
var style = ImGui.GetStyle();
|
||||||
|
return style.FramePadding.X + 4f * ImGuiHelpers.GlobalScale;
|
||||||
|
}
|
||||||
|
|
||||||
public static void DrawGroupedCenteredColorText(string text, Vector4 color, float? maxWidth = null)
|
public static void DrawGroupedCenteredColorText(string text, Vector4 color, float? maxWidth = null)
|
||||||
{
|
{
|
||||||
@@ -310,7 +472,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Vector4 GetBoolColor(bool input) => input ? AccentColor : ImGuiColors.DalamudRed;
|
public static Vector4 GetBoolColor(bool input) => input ? AccentColor : UiSharedService.AccentColor;
|
||||||
|
|
||||||
public float GetIconTextButtonSize(FontAwesomeIcon icon, string text)
|
public float GetIconTextButtonSize(FontAwesomeIcon icon, string text)
|
||||||
{
|
{
|
||||||
@@ -368,6 +530,8 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
|||||||
Vector2 cursorScreenPos = ImGui.GetCursorScreenPos();
|
Vector2 cursorScreenPos = ImGui.GetCursorScreenPos();
|
||||||
float x = vector.X + ImGui.GetStyle().FramePadding.X * 2f;
|
float x = vector.X + ImGui.GetStyle().FramePadding.X * 2f;
|
||||||
float frameHeight = height ?? ImGui.GetFrameHeight();
|
float frameHeight = height ?? ImGui.GetFrameHeight();
|
||||||
|
using var hoverColor = ImRaii.PushColor(ImGuiCol.ButtonHovered, AccentHoverColor);
|
||||||
|
using var activeColor = ImRaii.PushColor(ImGuiCol.ButtonActive, AccentActiveColor);
|
||||||
bool result = ImGui.Button(string.Empty, new Vector2(x, frameHeight));
|
bool result = ImGui.Button(string.Empty, new Vector2(x, frameHeight));
|
||||||
Vector2 pos = new Vector2(cursorScreenPos.X + ImGui.GetStyle().FramePadding.X,
|
Vector2 pos = new Vector2(cursorScreenPos.X + ImGui.GetStyle().FramePadding.X,
|
||||||
cursorScreenPos.Y + (height ?? ImGui.GetFrameHeight()) / 2f - (vector.Y / 2f));
|
cursorScreenPos.Y + (height ?? ImGui.GetFrameHeight()) / 2f - (vector.Y / 2f));
|
||||||
@@ -378,13 +542,19 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IconTextButtonInternal(FontAwesomeIcon icon, string text, Vector4? defaultColor = null, float? width = null)
|
private bool IconTextButtonInternal(FontAwesomeIcon icon, string text, Vector4? defaultColor = null, float? width = null, bool useAccentHover = true)
|
||||||
{
|
{
|
||||||
int num = 0;
|
int colorsPushed = 0;
|
||||||
if (defaultColor.HasValue)
|
if (defaultColor.HasValue)
|
||||||
{
|
{
|
||||||
ImGui.PushStyleColor(ImGuiCol.Button, defaultColor.Value);
|
ImGui.PushStyleColor(ImGuiCol.Button, defaultColor.Value);
|
||||||
num++;
|
colorsPushed++;
|
||||||
|
}
|
||||||
|
if (useAccentHover)
|
||||||
|
{
|
||||||
|
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, AccentHoverColor);
|
||||||
|
ImGui.PushStyleColor(ImGuiCol.ButtonActive, AccentActiveColor);
|
||||||
|
colorsPushed += 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui.PushID(text);
|
ImGui.PushID(text);
|
||||||
@@ -404,9 +574,9 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
|||||||
Vector2 pos2 = new Vector2(pos.X + vector.X + num2, cursorScreenPos.Y + ImGui.GetStyle().FramePadding.Y);
|
Vector2 pos2 = new Vector2(pos.X + vector.X + num2, cursorScreenPos.Y + ImGui.GetStyle().FramePadding.Y);
|
||||||
windowDrawList.AddText(pos2, ImGui.GetColorU32(ImGuiCol.Text), text);
|
windowDrawList.AddText(pos2, ImGui.GetColorU32(ImGuiCol.Text), text);
|
||||||
ImGui.PopID();
|
ImGui.PopID();
|
||||||
if (num > 0)
|
if (colorsPushed > 0)
|
||||||
{
|
{
|
||||||
ImGui.PopStyleColor(num);
|
ImGui.PopStyleColor(colorsPushed);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@@ -416,7 +586,8 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
return IconTextButtonInternal(icon, text,
|
return IconTextButtonInternal(icon, text,
|
||||||
isInPopup ? ColorHelpers.RgbaUintToVector4(ImGui.GetColorU32(ImGuiCol.PopupBg)) : null,
|
isInPopup ? ColorHelpers.RgbaUintToVector4(ImGui.GetColorU32(ImGuiCol.PopupBg)) : null,
|
||||||
width <= 0 ? null : width);
|
width <= 0 ? null : width,
|
||||||
|
!isInPopup);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool IsDirectoryWritable(string dirPath, bool throwIfFails = false)
|
public static bool IsDirectoryWritable(string dirPath, bool throwIfFails = false)
|
||||||
@@ -518,7 +689,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
|||||||
public void BooleanToColoredIcon(bool value, bool inline = true)
|
public void BooleanToColoredIcon(bool value, bool inline = true)
|
||||||
{
|
{
|
||||||
using var colorgreen = ImRaii.PushColor(ImGuiCol.Text, AccentColor, value);
|
using var colorgreen = ImRaii.PushColor(ImGuiCol.Text, AccentColor, value);
|
||||||
using var colorred = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed, !value);
|
using var colorred = ImRaii.PushColor(ImGuiCol.Text, UiSharedService.AccentColor, !value);
|
||||||
|
|
||||||
if (inline) ImGui.SameLine();
|
if (inline) ImGui.SameLine();
|
||||||
|
|
||||||
@@ -592,24 +763,24 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
if (_isPenumbraDirectory)
|
if (_isPenumbraDirectory)
|
||||||
{
|
{
|
||||||
ColorTextWrapped("Do not point the storage path directly to the Penumbra directory. If necessary, make a subfolder in it.", ImGuiColors.DalamudRed);
|
ColorTextWrapped("Do not point the storage path directly to the Penumbra directory. If necessary, make a subfolder in it.", UiSharedService.AccentColor);
|
||||||
}
|
}
|
||||||
else if (_isOneDrive)
|
else if (_isOneDrive)
|
||||||
{
|
{
|
||||||
ColorTextWrapped("Do not point the storage path to a folder in OneDrive. Do not use OneDrive folders for any Mod related functionality.", ImGuiColors.DalamudRed);
|
ColorTextWrapped("Do not point the storage path to a folder in OneDrive. Do not use OneDrive folders for any Mod related functionality.", UiSharedService.AccentColor);
|
||||||
}
|
}
|
||||||
else if (!_isDirectoryWritable)
|
else if (!_isDirectoryWritable)
|
||||||
{
|
{
|
||||||
ColorTextWrapped("The folder you selected does not exist or cannot be written to. Please provide a valid path.", ImGuiColors.DalamudRed);
|
ColorTextWrapped("The folder you selected does not exist or cannot be written to. Please provide a valid path.", UiSharedService.AccentColor);
|
||||||
}
|
}
|
||||||
else if (_cacheDirectoryHasOtherFilesThanCache)
|
else if (_cacheDirectoryHasOtherFilesThanCache)
|
||||||
{
|
{
|
||||||
ColorTextWrapped("Your selected directory has files or directories inside that are not Umbra related. Use an empty directory or a previous storage directory only.", ImGuiColors.DalamudRed);
|
ColorTextWrapped("Your selected directory has files or directories inside that are not Umbra related. Use an empty directory or a previous storage directory only.", UiSharedService.AccentColor);
|
||||||
}
|
}
|
||||||
else if (!_cacheDirectoryIsValidPath)
|
else if (!_cacheDirectoryIsValidPath)
|
||||||
{
|
{
|
||||||
ColorTextWrapped("Your selected directory contains illegal characters unreadable by FFXIV. " +
|
ColorTextWrapped("Your selected directory contains illegal characters unreadable by FFXIV. " +
|
||||||
"Restrict yourself to latin letters (A-Z), underscores (_), dashes (-) and arabic numbers (0-9).", ImGuiColors.DalamudRed);
|
"Restrict yourself to latin letters (A-Z), underscores (_), dashes (-) and arabic numbers (0-9).", UiSharedService.AccentColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
float maxCacheSize = (float)_configService.Current.MaxLocalCacheInGiB;
|
float maxCacheSize = (float)_configService.Current.MaxLocalCacheInGiB;
|
||||||
@@ -763,9 +934,9 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
if (intro)
|
if (intro)
|
||||||
{
|
{
|
||||||
ImGui.SetWindowFontScale(0.8f);
|
SetFontScale(0.8f);
|
||||||
BigText("Mandatory Plugins");
|
BigText("Mandatory Plugins");
|
||||||
ImGui.SetWindowFontScale(1.0f);
|
SetFontScale(1.0f);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -786,9 +957,9 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
if (intro)
|
if (intro)
|
||||||
{
|
{
|
||||||
ImGui.SetWindowFontScale(0.8f);
|
SetFontScale(0.8f);
|
||||||
BigText("Optional Addons");
|
BigText("Optional Addons");
|
||||||
ImGui.SetWindowFontScale(1.0f);
|
SetFontScale(1.0f);
|
||||||
UiSharedService.TextWrapped("These addons are not required for basic operation, but without them you may not see others as intended.");
|
UiSharedService.TextWrapped("These addons are not required for basic operation, but without them you may not see others as intended.");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -848,7 +1019,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
if (!_penumbraExists || !_glamourerExists)
|
if (!_penumbraExists || !_glamourerExists)
|
||||||
{
|
{
|
||||||
ImGui.TextColored(ImGuiColors.DalamudRed, "You need to install both Penumbra and Glamourer and keep them up to date to use Umbra.");
|
ImGui.TextColored(UiSharedService.AccentColor, "You need to install both Penumbra and Glamourer and keep them up to date to use Umbra.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"Author": "SirConstance",
|
"Author": "Keda",
|
||||||
"Name": "UmbraSync",
|
"Name": "UmbraSync",
|
||||||
"Punchline": "Share your true self.",
|
"Punchline": "Parce que nous le valons bien.",
|
||||||
"Description": "This plugin will synchronize your Penumbra mods and current Glamourer state with other paired clients automatically.",
|
"Description": "Ce plugin synchronisera automatiquement vos mods Penumbra et l'état actuel de Glamourer avec les autres clients appairés.",
|
||||||
"InternalName": "UmbraSync",
|
"InternalName": "UmbraSync",
|
||||||
"ApplicableVersion": "any",
|
"ApplicableVersion": "any",
|
||||||
"Tags": [
|
"Tags": [
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace MareSynchronos.Utils;
|
namespace MareSynchronos.Utils;
|
||||||
|
|
||||||
public class PngHdr
|
public static class PngHdr
|
||||||
{
|
{
|
||||||
private static readonly byte[] _magicSignature = [137, 80, 78, 71, 13, 10, 26, 10];
|
private static readonly byte[] _magicSignature = [137, 80, 78, 71, 13, 10, 26, 10];
|
||||||
private static readonly byte[] _IHDR = [(byte)'I', (byte)'H', (byte)'D', (byte)'R'];
|
private static readonly byte[] _IHDR = [(byte)'I', (byte)'H', (byte)'D', (byte)'R'];
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ using MareSynchronos.Services;
|
|||||||
using MareSynchronos.Services.ServerConfiguration;
|
using MareSynchronos.Services.ServerConfiguration;
|
||||||
using MareSynchronos.Utils;
|
using MareSynchronos.Utils;
|
||||||
using MareSynchronos.WebAPI.SignalR;
|
using MareSynchronos.WebAPI.SignalR;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
@@ -15,17 +14,15 @@ namespace MareSynchronos.WebAPI;
|
|||||||
public sealed class AccountRegistrationService : IDisposable
|
public sealed class AccountRegistrationService : IDisposable
|
||||||
{
|
{
|
||||||
private readonly HttpClient _httpClient;
|
private readonly HttpClient _httpClient;
|
||||||
private readonly ILogger<AccountRegistrationService> _logger;
|
|
||||||
private readonly ServerConfigurationManager _serverManager;
|
private readonly ServerConfigurationManager _serverManager;
|
||||||
|
|
||||||
private string GenerateSecretKey()
|
private static string GenerateSecretKey()
|
||||||
{
|
{
|
||||||
return Convert.ToHexString(SHA256.HashData(RandomNumberGenerator.GetBytes(64)));
|
return Convert.ToHexString(SHA256.HashData(RandomNumberGenerator.GetBytes(64)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public AccountRegistrationService(ILogger<AccountRegistrationService> logger, ServerConfigurationManager serverManager)
|
public AccountRegistrationService(ServerConfigurationManager serverManager)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
|
||||||
_serverManager = serverManager;
|
_serverManager = serverManager;
|
||||||
_httpClient = new(
|
_httpClient = new(
|
||||||
new HttpClientHandler
|
new HttpClientHandler
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using MareSynchronos.WebAPI.SignalR;
|
using MareSynchronos.WebAPI.SignalR;
|
||||||
using MareSynchronos.Services.AutoDetect;
|
using MareSynchronos.Services.AutoDetect;
|
||||||
@@ -65,15 +66,21 @@ public class DiscoveryApiClient
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> SendRequestAsync(string endpoint, string token, string? displayName, CancellationToken ct)
|
public async Task<bool> SendRequestAsync(string endpoint, string? token, string? targetUid, string? displayName, CancellationToken ct)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
if (string.IsNullOrEmpty(token) && string.IsNullOrEmpty(targetUid))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Discovery request aborted: no token or targetUid provided");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
var jwt = await _tokenProvider.GetOrUpdateToken(ct).ConfigureAwait(false);
|
var jwt = await _tokenProvider.GetOrUpdateToken(ct).ConfigureAwait(false);
|
||||||
if (string.IsNullOrEmpty(jwt)) return false;
|
if (string.IsNullOrEmpty(jwt)) return false;
|
||||||
using var req = new HttpRequestMessage(HttpMethod.Post, endpoint);
|
using var req = new HttpRequestMessage(HttpMethod.Post, endpoint);
|
||||||
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwt);
|
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwt);
|
||||||
var body = JsonSerializer.Serialize(new { token, displayName });
|
var body = JsonSerializer.Serialize(new RequestPayload(token, targetUid, displayName));
|
||||||
req.Content = new StringContent(body, Encoding.UTF8, "application/json");
|
req.Content = new StringContent(body, Encoding.UTF8, "application/json");
|
||||||
var resp = await _httpClient.SendAsync(req, ct).ConfigureAwait(false);
|
var resp = await _httpClient.SendAsync(req, ct).ConfigureAwait(false);
|
||||||
if (resp.StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
if (resp.StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
||||||
@@ -82,7 +89,7 @@ public class DiscoveryApiClient
|
|||||||
if (string.IsNullOrEmpty(jwt2)) return false;
|
if (string.IsNullOrEmpty(jwt2)) return false;
|
||||||
using var req2 = new HttpRequestMessage(HttpMethod.Post, endpoint);
|
using var req2 = new HttpRequestMessage(HttpMethod.Post, endpoint);
|
||||||
req2.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwt2);
|
req2.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwt2);
|
||||||
var body2 = JsonSerializer.Serialize(new { token, displayName });
|
var body2 = JsonSerializer.Serialize(new RequestPayload(token, targetUid, displayName));
|
||||||
req2.Content = new StringContent(body2, Encoding.UTF8, "application/json");
|
req2.Content = new StringContent(body2, Encoding.UTF8, "application/json");
|
||||||
resp = await _httpClient.SendAsync(req2, ct).ConfigureAwait(false);
|
resp = await _httpClient.SendAsync(req2, ct).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
@@ -102,6 +109,14 @@ public class DiscoveryApiClient
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed record RequestPayload(
|
||||||
|
[property: JsonPropertyName("token"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
|
string? Token,
|
||||||
|
[property: JsonPropertyName("targetUid"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
|
string? TargetUid,
|
||||||
|
[property: JsonPropertyName("displayName"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
|
string? DisplayName);
|
||||||
|
|
||||||
public async Task<bool> PublishAsync(string endpoint, IEnumerable<string> hashes, string? displayName, CancellationToken ct, bool allowRequests = true)
|
public async Task<bool> PublishAsync(string endpoint, IEnumerable<string> hashes, string? displayName, CancellationToken ct, bool allowRequests = true)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using MareSynchronos.API.Data;
|
|||||||
using MareSynchronos.API.Dto.Files;
|
using MareSynchronos.API.Dto.Files;
|
||||||
using MareSynchronos.API.Routes;
|
using MareSynchronos.API.Routes;
|
||||||
using MareSynchronos.FileCache;
|
using MareSynchronos.FileCache;
|
||||||
|
using MareSynchronos.MareConfiguration;
|
||||||
using MareSynchronos.PlayerData.Handlers;
|
using MareSynchronos.PlayerData.Handlers;
|
||||||
using MareSynchronos.Services.Mediator;
|
using MareSynchronos.Services.Mediator;
|
||||||
using MareSynchronos.Utils;
|
using MareSynchronos.Utils;
|
||||||
@@ -20,17 +21,22 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
private readonly Dictionary<string, FileDownloadStatus> _downloadStatus;
|
private readonly Dictionary<string, FileDownloadStatus> _downloadStatus;
|
||||||
private readonly FileCompactor _fileCompactor;
|
private readonly FileCompactor _fileCompactor;
|
||||||
private readonly FileCacheManager _fileDbManager;
|
private readonly FileCacheManager _fileDbManager;
|
||||||
|
private readonly MareConfigService _mareConfigService;
|
||||||
private readonly FileTransferOrchestrator _orchestrator;
|
private readonly FileTransferOrchestrator _orchestrator;
|
||||||
private readonly List<ThrottledStream> _activeDownloadStreams;
|
private readonly List<ThrottledStream> _activeDownloadStreams;
|
||||||
|
private readonly object _queueLock = new();
|
||||||
|
private SemaphoreSlim? _downloadQueueSemaphore;
|
||||||
|
private int _downloadQueueCapacity = -1;
|
||||||
|
|
||||||
public FileDownloadManager(ILogger<FileDownloadManager> logger, MareMediator mediator,
|
public FileDownloadManager(ILogger<FileDownloadManager> logger, MareMediator mediator,
|
||||||
FileTransferOrchestrator orchestrator,
|
FileTransferOrchestrator orchestrator,
|
||||||
FileCacheManager fileCacheManager, FileCompactor fileCompactor) : base(logger, mediator)
|
FileCacheManager fileCacheManager, FileCompactor fileCompactor, MareConfigService mareConfigService) : base(logger, mediator)
|
||||||
{
|
{
|
||||||
_downloadStatus = new Dictionary<string, FileDownloadStatus>(StringComparer.Ordinal);
|
_downloadStatus = new Dictionary<string, FileDownloadStatus>(StringComparer.Ordinal);
|
||||||
_orchestrator = orchestrator;
|
_orchestrator = orchestrator;
|
||||||
_fileDbManager = fileCacheManager;
|
_fileDbManager = fileCacheManager;
|
||||||
_fileCompactor = fileCompactor;
|
_fileCompactor = fileCompactor;
|
||||||
|
_mareConfigService = mareConfigService;
|
||||||
_activeDownloadStreams = [];
|
_activeDownloadStreams = [];
|
||||||
|
|
||||||
Mediator.Subscribe<DownloadLimitChangedMessage>(this, (msg) =>
|
Mediator.Subscribe<DownloadLimitChangedMessage>(this, (msg) =>
|
||||||
@@ -59,6 +65,14 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
public async Task DownloadFiles(GameObjectHandler gameObject, List<FileReplacementData> fileReplacementDto, CancellationToken ct)
|
public async Task DownloadFiles(GameObjectHandler gameObject, List<FileReplacementData> fileReplacementDto, CancellationToken ct)
|
||||||
{
|
{
|
||||||
|
SemaphoreSlim? queueSemaphore = null;
|
||||||
|
if (_mareConfigService.Current.EnableDownloadQueue)
|
||||||
|
{
|
||||||
|
queueSemaphore = GetQueueSemaphore();
|
||||||
|
Logger.LogTrace("Queueing download for {name}. Currently queued: {queued}", gameObject.Name, queueSemaphore.CurrentCount);
|
||||||
|
await queueSemaphore.WaitAsync(ct).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
Mediator.Publish(new HaltScanMessage(nameof(DownloadFiles)));
|
Mediator.Publish(new HaltScanMessage(nameof(DownloadFiles)));
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -70,6 +84,11 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
if (queueSemaphore != null)
|
||||||
|
{
|
||||||
|
queueSemaphore.Release();
|
||||||
|
}
|
||||||
|
|
||||||
Mediator.Publish(new DownloadFinishedMessage(gameObject));
|
Mediator.Publish(new DownloadFinishedMessage(gameObject));
|
||||||
Mediator.Publish(new ResumeScanMessage(nameof(DownloadFiles)));
|
Mediator.Publish(new ResumeScanMessage(nameof(DownloadFiles)));
|
||||||
}
|
}
|
||||||
@@ -132,6 +151,22 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
return (string.Join("", hashName), long.Parse(string.Join("", fileLength)));
|
return (string.Join("", hashName), long.Parse(string.Join("", fileLength)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private SemaphoreSlim GetQueueSemaphore()
|
||||||
|
{
|
||||||
|
var desiredCapacity = Math.Clamp(_mareConfigService.Current.ParallelDownloads, 1, 10);
|
||||||
|
|
||||||
|
lock (_queueLock)
|
||||||
|
{
|
||||||
|
if (_downloadQueueSemaphore == null || _downloadQueueCapacity != desiredCapacity)
|
||||||
|
{
|
||||||
|
_downloadQueueSemaphore = new SemaphoreSlim(desiredCapacity, desiredCapacity);
|
||||||
|
_downloadQueueCapacity = desiredCapacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _downloadQueueSemaphore;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task DownloadAndMungeFileHttpClient(string downloadGroup, Guid requestId, List<DownloadFileTransfer> fileTransfer, string tempPath, IProgress<long> progress, CancellationToken ct)
|
private async Task DownloadAndMungeFileHttpClient(string downloadGroup, Guid requestId, List<DownloadFileTransfer> fileTransfer, string tempPath, IProgress<long> progress, CancellationToken ct)
|
||||||
{
|
{
|
||||||
Logger.LogDebug("GUID {requestId} on server {uri} for files {files}", requestId, fileTransfer[0].DownloadUri, string.Join(", ", fileTransfer.Select(c => c.Hash).ToList()));
|
Logger.LogDebug("GUID {requestId} on server {uri} for files {files}", requestId, fileTransfer[0].DownloadUri, string.Join(", ", fileTransfer.Select(c => c.Hash).ToList()));
|
||||||
|
|||||||
@@ -17,19 +17,16 @@ namespace MareSynchronos.WebAPI.Files;
|
|||||||
public sealed class FileUploadManager : DisposableMediatorSubscriberBase
|
public sealed class FileUploadManager : DisposableMediatorSubscriberBase
|
||||||
{
|
{
|
||||||
private readonly FileCacheManager _fileDbManager;
|
private readonly FileCacheManager _fileDbManager;
|
||||||
private readonly MareConfigService _mareConfigService;
|
|
||||||
private readonly FileTransferOrchestrator _orchestrator;
|
private readonly FileTransferOrchestrator _orchestrator;
|
||||||
private readonly ServerConfigurationManager _serverManager;
|
private readonly ServerConfigurationManager _serverManager;
|
||||||
private readonly Dictionary<string, DateTime> _verifiedUploadedHashes = new(StringComparer.Ordinal);
|
private readonly Dictionary<string, DateTime> _verifiedUploadedHashes = new(StringComparer.Ordinal);
|
||||||
private CancellationTokenSource? _uploadCancellationTokenSource = new();
|
private CancellationTokenSource? _uploadCancellationTokenSource = new();
|
||||||
|
|
||||||
public FileUploadManager(ILogger<FileUploadManager> logger, MareMediator mediator,
|
public FileUploadManager(ILogger<FileUploadManager> logger, MareMediator mediator,
|
||||||
MareConfigService mareConfigService,
|
|
||||||
FileTransferOrchestrator orchestrator,
|
FileTransferOrchestrator orchestrator,
|
||||||
FileCacheManager fileDbManager,
|
FileCacheManager fileDbManager,
|
||||||
ServerConfigurationManager serverManager) : base(logger, mediator)
|
ServerConfigurationManager serverManager) : base(logger, mediator)
|
||||||
{
|
{
|
||||||
_mareConfigService = mareConfigService;
|
|
||||||
_orchestrator = orchestrator;
|
_orchestrator = orchestrator;
|
||||||
_fileDbManager = fileDbManager;
|
_fileDbManager = fileDbManager;
|
||||||
_serverManager = serverManager;
|
_serverManager = serverManager;
|
||||||
|
|||||||
@@ -19,6 +19,6 @@ public class DownloadFileTransfer : FileTransfer
|
|||||||
get => Dto.Size;
|
get => Dto.Size;
|
||||||
}
|
}
|
||||||
|
|
||||||
public long TotalRaw => 0; // XXX
|
public long TotalRaw => Dto.Size;
|
||||||
private DownloadFileDto Dto => (DownloadFileDto)TransferDto;
|
private DownloadFileDto Dto => (DownloadFileDto)TransferDto;
|
||||||
}
|
}
|
||||||
@@ -97,6 +97,12 @@ public partial class ApiController
|
|||||||
await _mareHub!.InvokeAsync(nameof(UserSetProfile), userDescription).ConfigureAwait(false);
|
await _mareHub!.InvokeAsync(nameof(UserSetProfile), userDescription).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task UserSetTypingState(bool isTyping)
|
||||||
|
{
|
||||||
|
CheckConnection();
|
||||||
|
await _mareHub!.SendAsync(nameof(UserSetTypingState), isTyping).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task PushCharacterDataInternal(CharacterData character, List<UserData> visibleCharacters)
|
private async Task PushCharacterDataInternal(CharacterData character, List<UserData> visibleCharacters)
|
||||||
{
|
{
|
||||||
Logger.LogInformation("Pushing character data for {hash} to {charas}", character.DataHash.Value, string.Join(", ", visibleCharacters.Select(c => c.AliasOrUID)));
|
Logger.LogInformation("Pushing character data for {hash} to {charas}", character.DataHash.Value, string.Join(", ", visibleCharacters.Select(c => c.AliasOrUID)));
|
||||||
|
|||||||
@@ -138,6 +138,13 @@ public partial class ApiController
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task Client_UserTypingState(TypingStateDto dto)
|
||||||
|
{
|
||||||
|
Logger.LogTrace("Client_UserTypingState: {uid} typing={typing}", dto.User.UID, dto.IsTyping);
|
||||||
|
Mediator.Publish(new UserTypingStateMessage(dto));
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
public Task Client_UserReceiveCharacterData(OnlineUserCharaDataDto dataDto)
|
public Task Client_UserReceiveCharacterData(OnlineUserCharaDataDto dataDto)
|
||||||
{
|
{
|
||||||
Logger.LogTrace("Client_UserReceiveCharacterData: {user}", dataDto.User);
|
Logger.LogTrace("Client_UserReceiveCharacterData: {user}", dataDto.User);
|
||||||
@@ -224,7 +231,7 @@ public partial class ApiController
|
|||||||
|
|
||||||
public Task Client_GposeLobbyPushWorldData(UserData userData, WorldData worldData)
|
public Task Client_GposeLobbyPushWorldData(UserData userData, WorldData worldData)
|
||||||
{
|
{
|
||||||
//Logger.LogDebug("Client_GposeLobbyPushWorldData: {dto}", userData);
|
Logger.LogDebug("Client_GposeLobbyPushWorldData: {dto}", userData);
|
||||||
ExecuteSafely(() => Mediator.Publish(new GPoseLobbyReceiveWorldData(userData, worldData)));
|
ExecuteSafely(() => Mediator.Publish(new GPoseLobbyReceiveWorldData(userData, worldData)));
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
@@ -313,6 +320,12 @@ public partial class ApiController
|
|||||||
_mareHub!.On(nameof(Client_UserChatMsg), act);
|
_mareHub!.On(nameof(Client_UserChatMsg), act);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void OnUserTypingState(Action<TypingStateDto> act)
|
||||||
|
{
|
||||||
|
if (_initialized) return;
|
||||||
|
_mareHub!.On(nameof(Client_UserTypingState), act);
|
||||||
|
}
|
||||||
|
|
||||||
public void OnUserReceiveCharacterData(Action<OnlineUserCharaDataDto> act)
|
public void OnUserReceiveCharacterData(Action<OnlineUserCharaDataDto> act)
|
||||||
{
|
{
|
||||||
if (_initialized) return;
|
if (_initialized) return;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using MareSynchronos.API.Data;
|
using System;
|
||||||
|
using MareSynchronos.API.Data;
|
||||||
using MareSynchronos.API.Dto.Group;
|
using MareSynchronos.API.Dto.Group;
|
||||||
using MareSynchronos.WebAPI.SignalR.Utils;
|
using MareSynchronos.WebAPI.SignalR.Utils;
|
||||||
using Microsoft.AspNetCore.SignalR.Client;
|
using Microsoft.AspNetCore.SignalR.Client;
|
||||||
@@ -49,12 +50,23 @@ public partial class ApiController
|
|||||||
await _mareHub!.SendAsync(nameof(GroupClear), group).ConfigureAwait(false);
|
await _mareHub!.SendAsync(nameof(GroupClear), group).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<GroupPasswordDto> GroupCreate(string? alias = null)
|
public Task<GroupPasswordDto> GroupCreate()
|
||||||
|
{
|
||||||
|
return GroupCreate(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<GroupPasswordDto> GroupCreate(string? alias)
|
||||||
{
|
{
|
||||||
CheckConnection();
|
CheckConnection();
|
||||||
return await _mareHub!.InvokeAsync<GroupPasswordDto>(nameof(GroupCreate), string.IsNullOrWhiteSpace(alias) ? null : alias.Trim()).ConfigureAwait(false);
|
return await _mareHub!.InvokeAsync<GroupPasswordDto>(nameof(GroupCreate), string.IsNullOrWhiteSpace(alias) ? null : alias.Trim()).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<GroupPasswordDto> GroupCreateTemporary(DateTime expiresAtUtc)
|
||||||
|
{
|
||||||
|
CheckConnection();
|
||||||
|
return await _mareHub!.InvokeAsync<GroupPasswordDto>(nameof(GroupCreateTemporary), expiresAtUtc).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<List<string>> GroupCreateTempInvite(GroupDto group, int amount)
|
public async Task<List<string>> GroupCreateTempInvite(GroupDto group, int amount)
|
||||||
{
|
{
|
||||||
CheckConnection();
|
CheckConnection();
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using MareSynchronos.API.Dto.McdfShare;
|
||||||
|
using Microsoft.AspNetCore.SignalR.Client;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace MareSynchronos.WebAPI;
|
||||||
|
|
||||||
|
public sealed partial class ApiController
|
||||||
|
{
|
||||||
|
public async Task<List<McdfShareEntryDto>> McdfShareGetOwn()
|
||||||
|
{
|
||||||
|
if (!IsConnected) return [];
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await _mareHub!.InvokeAsync<List<McdfShareEntryDto>>(nameof(McdfShareGetOwn)).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(ex, "Error during {method}", nameof(McdfShareGetOwn));
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<McdfShareEntryDto>> McdfShareGetShared()
|
||||||
|
{
|
||||||
|
if (!IsConnected) return [];
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await _mareHub!.InvokeAsync<List<McdfShareEntryDto>>(nameof(McdfShareGetShared)).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(ex, "Error during {method}", nameof(McdfShareGetShared));
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task McdfShareUpload(McdfShareUploadRequestDto requestDto)
|
||||||
|
{
|
||||||
|
if (!IsConnected) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _mareHub!.InvokeAsync(nameof(McdfShareUpload), requestDto).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(ex, "Error during {method}", nameof(McdfShareUpload));
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<McdfSharePayloadDto?> McdfShareDownload(Guid shareId)
|
||||||
|
{
|
||||||
|
if (!IsConnected) return null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await _mareHub!.InvokeAsync<McdfSharePayloadDto?>(nameof(McdfShareDownload), shareId).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(ex, "Error during {method}", nameof(McdfShareDownload));
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> McdfShareDelete(Guid shareId)
|
||||||
|
{
|
||||||
|
if (!IsConnected) return false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await _mareHub!.InvokeAsync<bool>(nameof(McdfShareDelete), shareId).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(ex, "Error during {method}", nameof(McdfShareDelete));
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<McdfShareEntryDto?> McdfShareUpdate(McdfShareUpdateRequestDto requestDto)
|
||||||
|
{
|
||||||
|
if (!IsConnected) return null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await _mareHub!.InvokeAsync<McdfShareEntryDto?>(nameof(McdfShareUpdate), requestDto).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(ex, "Error during {method}", nameof(McdfShareUpdate));
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using MareSynchronos.API.Dto.Group;
|
||||||
|
using Microsoft.AspNetCore.SignalR.Client;
|
||||||
|
|
||||||
|
namespace MareSynchronos.WebAPI;
|
||||||
|
|
||||||
|
public partial class ApiController
|
||||||
|
{
|
||||||
|
public async Task<List<SyncshellDiscoveryEntryDto>> SyncshellDiscoveryList()
|
||||||
|
{
|
||||||
|
CheckConnection();
|
||||||
|
return await _mareHub!.InvokeAsync<List<SyncshellDiscoveryEntryDto>>(nameof(SyncshellDiscoveryList)).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<SyncshellDiscoveryStateDto?> SyncshellDiscoveryGetState(GroupDto group)
|
||||||
|
{
|
||||||
|
CheckConnection();
|
||||||
|
return await _mareHub!.InvokeAsync<SyncshellDiscoveryStateDto?>(nameof(SyncshellDiscoveryGetState), group).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> SyncshellDiscoverySetVisibility(SyncshellDiscoveryVisibilityRequestDto request)
|
||||||
|
{
|
||||||
|
CheckConnection();
|
||||||
|
return await _mareHub!.InvokeAsync<bool>(nameof(SyncshellDiscoverySetVisibility), request).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> SyncshellDiscoveryJoin(GroupDto group)
|
||||||
|
{
|
||||||
|
CheckConnection();
|
||||||
|
return await _mareHub!.InvokeAsync<bool>(nameof(SyncshellDiscoveryJoin), group).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -348,6 +348,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM
|
|||||||
OnUserUpdateSelfPairPermissions(dto => _ = Client_UserUpdateSelfPairPermissions(dto));
|
OnUserUpdateSelfPairPermissions(dto => _ = Client_UserUpdateSelfPairPermissions(dto));
|
||||||
OnUserReceiveUploadStatus(dto => _ = Client_UserReceiveUploadStatus(dto));
|
OnUserReceiveUploadStatus(dto => _ = Client_UserReceiveUploadStatus(dto));
|
||||||
OnUserUpdateProfile(dto => _ = Client_UserUpdateProfile(dto));
|
OnUserUpdateProfile(dto => _ = Client_UserUpdateProfile(dto));
|
||||||
|
OnUserTypingState(dto => _ = Client_UserTypingState(dto));
|
||||||
|
|
||||||
OnGroupChangePermissions((dto) => _ = Client_GroupChangePermissions(dto));
|
OnGroupChangePermissions((dto) => _ = Client_GroupChangePermissions(dto));
|
||||||
OnGroupDelete((dto) => _ = Client_GroupDelete(dto));
|
OnGroupDelete((dto) => _ = Client_GroupDelete(dto));
|
||||||
@@ -393,7 +394,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM
|
|||||||
foreach (var user in users)
|
foreach (var user in users)
|
||||||
{
|
{
|
||||||
Logger.LogDebug("Group Pair: {user}", user);
|
Logger.LogDebug("Group Pair: {user}", user);
|
||||||
_pairManager.AddGroupPair(user);
|
_pairManager.AddGroupPair(user, isInitialLoad: true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Submodule Penumbra.Api updated: dd14131793...c23ee05c1e
15
Program.cs
Normal file
15
Program.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
using System;
|
||||||
|
using Mono.Cecil;
|
||||||
|
|
||||||
|
class Program
|
||||||
|
{
|
||||||
|
static void Main()
|
||||||
|
{
|
||||||
|
var asm = AssemblyDefinition.ReadAssembly("/Users/luca.genovese/Library/Application Support/XIV on Mac/dalamud/Hooks/dev/FFXIVClientStructs.dll");
|
||||||
|
var type = asm.MainModule.GetType("FFXIVClientStructs.FFXIV.Client.UI.AddonNamePlate/NamePlateObject");
|
||||||
|
foreach (var field in type.Fields)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"FIELD {field.Name}: {field.FieldType}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user