Merge branch '2.0.0' into dev
This commit is contained in:
@@ -11,6 +11,8 @@ public sealed class ChatConfig : ILightlessConfiguration
|
|||||||
public bool ShowRulesOverlayOnOpen { get; set; } = true;
|
public bool ShowRulesOverlayOnOpen { get; set; } = true;
|
||||||
public bool ShowMessageTimestamps { get; set; } = true;
|
public bool ShowMessageTimestamps { get; set; } = true;
|
||||||
public float ChatWindowOpacity { get; set; } = .97f;
|
public float ChatWindowOpacity { get; set; } = .97f;
|
||||||
|
public bool FadeWhenUnfocused { get; set; } = false;
|
||||||
|
public float UnfocusedWindowOpacity { get; set; } = 0.6f;
|
||||||
public bool IsWindowPinned { get; set; } = false;
|
public bool IsWindowPinned { get; set; } = false;
|
||||||
public bool AutoOpenChatOnPluginLoad { get; set; } = false;
|
public bool AutoOpenChatOnPluginLoad { get; set; } = false;
|
||||||
public float ChatFontScale { get; set; } = 1.0f;
|
public float ChatFontScale { get; set; } = 1.0f;
|
||||||
|
|||||||
@@ -49,7 +49,8 @@ public class LightlessConfig : ILightlessConfiguration
|
|||||||
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;
|
||||||
public bool PreferNotesOverNamesForVisible { get; set; } = false;
|
public bool PreferNotesOverNamesForVisible { get; set; } = false;
|
||||||
public VisiblePairSortMode VisiblePairSortMode { get; set; } = VisiblePairSortMode.Default;
|
public VisiblePairSortMode VisiblePairSortMode { get; set; } = VisiblePairSortMode.Alphabetical;
|
||||||
|
public OnlinePairSortMode OnlinePairSortMode { get; set; } = OnlinePairSortMode.Alphabetical;
|
||||||
public float ProfileDelay { get; set; } = 1.5f;
|
public float ProfileDelay { get; set; } = 1.5f;
|
||||||
public bool ProfilePopoutRight { get; set; } = false;
|
public bool ProfilePopoutRight { get; set; } = false;
|
||||||
public bool ProfilesAllowNsfw { get; set; } = false;
|
public bool ProfilesAllowNsfw { get; set; } = false;
|
||||||
|
|||||||
@@ -1,36 +1,38 @@
|
|||||||
using LightlessSync.API.Data;
|
using LightlessSync.API.Data;
|
||||||
|
|
||||||
namespace LightlessSync.PlayerData.Pairs;
|
namespace LightlessSync.PlayerData.Pairs;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// orchestrates the lifecycle of a paired character
|
/// orchestrates the lifecycle of a paired character
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IPairHandlerAdapter : IDisposable, IPairPerformanceSubject
|
public interface IPairHandlerAdapter : IDisposable, IPairPerformanceSubject
|
||||||
{
|
{
|
||||||
new string Ident { get; }
|
new string Ident { get; }
|
||||||
bool Initialized { get; }
|
bool Initialized { get; }
|
||||||
bool IsVisible { get; }
|
bool IsVisible { get; }
|
||||||
bool ScheduledForDeletion { get; set; }
|
bool ScheduledForDeletion { get; set; }
|
||||||
CharacterData? LastReceivedCharacterData { get; }
|
CharacterData? LastReceivedCharacterData { get; }
|
||||||
long LastAppliedDataBytes { get; }
|
long LastAppliedDataBytes { get; }
|
||||||
new string? PlayerName { get; }
|
new string? PlayerName { get; }
|
||||||
string PlayerNameHash { get; }
|
string PlayerNameHash { get; }
|
||||||
uint PlayerCharacterId { get; }
|
uint PlayerCharacterId { get; }
|
||||||
DateTime? LastDataReceivedAt { get; }
|
DateTime? LastDataReceivedAt { get; }
|
||||||
DateTime? LastApplyAttemptAt { get; }
|
DateTime? LastApplyAttemptAt { get; }
|
||||||
DateTime? LastSuccessfulApplyAt { get; }
|
DateTime? LastSuccessfulApplyAt { get; }
|
||||||
string? LastFailureReason { get; }
|
string? LastFailureReason { get; }
|
||||||
IReadOnlyList<string> LastBlockingConditions { get; }
|
IReadOnlyList<string> LastBlockingConditions { get; }
|
||||||
bool IsApplying { get; }
|
bool IsApplying { get; }
|
||||||
bool IsDownloading { get; }
|
bool IsDownloading { get; }
|
||||||
int PendingDownloadCount { get; }
|
int PendingDownloadCount { get; }
|
||||||
int ForbiddenDownloadCount { get; }
|
int ForbiddenDownloadCount { get; }
|
||||||
|
DateTime? InvisibleSinceUtc { get; }
|
||||||
|
DateTime? VisibilityEvictionDueAtUtc { get; }
|
||||||
|
|
||||||
void Initialize();
|
void Initialize();
|
||||||
void ApplyData(CharacterData data);
|
void ApplyData(CharacterData data);
|
||||||
void ApplyLastReceivedData(bool forced = false);
|
void ApplyLastReceivedData(bool forced = false);
|
||||||
bool FetchPerformanceMetricsFromCache();
|
bool FetchPerformanceMetricsFromCache();
|
||||||
void LoadCachedCharacterData(CharacterData data);
|
void LoadCachedCharacterData(CharacterData data);
|
||||||
void SetUploading(bool uploading);
|
void SetUploading(bool uploading);
|
||||||
void SetPaused(bool paused);
|
void SetPaused(bool paused);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -194,9 +194,13 @@ public class Pair
|
|||||||
{
|
{
|
||||||
var handler = TryGetHandler();
|
var handler = TryGetHandler();
|
||||||
if (handler is null)
|
if (handler is null)
|
||||||
{
|
|
||||||
return PairDebugInfo.Empty;
|
return PairDebugInfo.Empty;
|
||||||
}
|
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
var dueAt = handler.VisibilityEvictionDueAtUtc;
|
||||||
|
var remainingSeconds = dueAt.HasValue
|
||||||
|
? Math.Max(0, (dueAt.Value - now).TotalSeconds)
|
||||||
|
: (double?)null;
|
||||||
|
|
||||||
return new PairDebugInfo(
|
return new PairDebugInfo(
|
||||||
true,
|
true,
|
||||||
@@ -206,6 +210,9 @@ public class Pair
|
|||||||
handler.LastDataReceivedAt,
|
handler.LastDataReceivedAt,
|
||||||
handler.LastApplyAttemptAt,
|
handler.LastApplyAttemptAt,
|
||||||
handler.LastSuccessfulApplyAt,
|
handler.LastSuccessfulApplyAt,
|
||||||
|
handler.InvisibleSinceUtc,
|
||||||
|
handler.VisibilityEvictionDueAtUtc,
|
||||||
|
remainingSeconds,
|
||||||
handler.LastFailureReason,
|
handler.LastFailureReason,
|
||||||
handler.LastBlockingConditions,
|
handler.LastBlockingConditions,
|
||||||
handler.IsApplying,
|
handler.IsApplying,
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ public sealed record PairDebugInfo(
|
|||||||
DateTime? LastDataReceivedAt,
|
DateTime? LastDataReceivedAt,
|
||||||
DateTime? LastApplyAttemptAt,
|
DateTime? LastApplyAttemptAt,
|
||||||
DateTime? LastSuccessfulApplyAt,
|
DateTime? LastSuccessfulApplyAt,
|
||||||
|
DateTime? InvisibleSinceUtc,
|
||||||
|
DateTime? VisibilityEvictionDueAtUtc,
|
||||||
|
double? VisibilityEvictionRemainingSeconds,
|
||||||
string? LastFailureReason,
|
string? LastFailureReason,
|
||||||
IReadOnlyList<string> BlockingConditions,
|
IReadOnlyList<string> BlockingConditions,
|
||||||
bool IsApplying,
|
bool IsApplying,
|
||||||
@@ -24,6 +27,9 @@ public sealed record PairDebugInfo(
|
|||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
Array.Empty<string>(),
|
Array.Empty<string>(),
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
|
|||||||
@@ -70,7 +70,14 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
private DateTime? _lastSuccessfulApplyAt;
|
private DateTime? _lastSuccessfulApplyAt;
|
||||||
private string? _lastFailureReason;
|
private string? _lastFailureReason;
|
||||||
private IReadOnlyList<string> _lastBlockingConditions = Array.Empty<string>();
|
private IReadOnlyList<string> _lastBlockingConditions = Array.Empty<string>();
|
||||||
|
private readonly object _visibilityGraceGate = new();
|
||||||
|
private CancellationTokenSource? _visibilityGraceCts;
|
||||||
|
private static readonly TimeSpan VisibilityEvictionGrace = TimeSpan.FromMinutes(1);
|
||||||
|
private DateTime? _invisibleSinceUtc;
|
||||||
|
private DateTime? _visibilityEvictionDueAtUtc;
|
||||||
|
|
||||||
|
public DateTime? InvisibleSinceUtc => _invisibleSinceUtc;
|
||||||
|
public DateTime? VisibilityEvictionDueAtUtc => _visibilityEvictionDueAtUtc;
|
||||||
public string Ident { get; }
|
public string Ident { get; }
|
||||||
public bool Initialized { get; private set; }
|
public bool Initialized { get; private set; }
|
||||||
public bool ScheduledForDeletion { get; set; }
|
public bool ScheduledForDeletion { get; set; }
|
||||||
@@ -80,24 +87,37 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
get => _isVisible;
|
get => _isVisible;
|
||||||
private set
|
private set
|
||||||
{
|
{
|
||||||
if (_isVisible != value)
|
if (_isVisible == value) return;
|
||||||
|
|
||||||
|
_isVisible = value;
|
||||||
|
|
||||||
|
if (!_isVisible)
|
||||||
{
|
{
|
||||||
_isVisible = value;
|
DisableSync();
|
||||||
if (!_isVisible)
|
|
||||||
{
|
_invisibleSinceUtc = DateTime.UtcNow;
|
||||||
DisableSync();
|
_visibilityEvictionDueAtUtc = _invisibleSinceUtc.Value.Add(VisibilityEvictionGrace);
|
||||||
ResetPenumbraCollection(reason: "VisibilityLost");
|
|
||||||
}
|
StartVisibilityGraceTask();
|
||||||
else if (_charaHandler is not null && _charaHandler.Address != nint.Zero)
|
|
||||||
{
|
|
||||||
_ = EnsurePenumbraCollection();
|
|
||||||
}
|
|
||||||
var user = GetPrimaryUserData();
|
|
||||||
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter),
|
|
||||||
EventSeverity.Informational, "User Visibility Changed, now: " + (_isVisible ? "Is Visible" : "Is not Visible"))));
|
|
||||||
Mediator.Publish(new RefreshUiMessage());
|
|
||||||
Mediator.Publish(new VisibilityChange());
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
CancelVisibilityGraceTask();
|
||||||
|
|
||||||
|
_invisibleSinceUtc = null;
|
||||||
|
_visibilityEvictionDueAtUtc = null;
|
||||||
|
|
||||||
|
ScheduledForDeletion = false;
|
||||||
|
|
||||||
|
if (_charaHandler is not null && _charaHandler.Address != nint.Zero)
|
||||||
|
_ = EnsurePenumbraCollection();
|
||||||
|
}
|
||||||
|
|
||||||
|
var user = GetPrimaryUserData();
|
||||||
|
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter),
|
||||||
|
EventSeverity.Informational, "User Visibility Changed, now: " + (_isVisible ? "Is Visible" : "Is not Visible"))));
|
||||||
|
Mediator.Publish(new RefreshUiMessage());
|
||||||
|
Mediator.Publish(new VisibilityChange());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -918,6 +938,46 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void CancelVisibilityGraceTask()
|
||||||
|
{
|
||||||
|
lock (_visibilityGraceGate)
|
||||||
|
{
|
||||||
|
_visibilityGraceCts?.CancelDispose();
|
||||||
|
_visibilityGraceCts = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StartVisibilityGraceTask()
|
||||||
|
{
|
||||||
|
CancellationToken token;
|
||||||
|
lock (_visibilityGraceGate)
|
||||||
|
{
|
||||||
|
_visibilityGraceCts = _visibilityGraceCts?.CancelRecreate() ?? new CancellationTokenSource();
|
||||||
|
token = _visibilityGraceCts.Token;
|
||||||
|
}
|
||||||
|
|
||||||
|
_visibilityGraceTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.Delay(VisibilityEvictionGrace, token).ConfigureAwait(false);
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
if (IsVisible) return;
|
||||||
|
|
||||||
|
ScheduledForDeletion = true;
|
||||||
|
ResetPenumbraCollection(reason: "VisibilityLostTimeout");
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// operation cancelled, do nothing
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogDebug(ex, "Visibility grace task failed for {handler}", GetLogIdentifier());
|
||||||
|
}
|
||||||
|
}, CancellationToken.None);
|
||||||
|
}
|
||||||
|
|
||||||
protected override void Dispose(bool disposing)
|
protected override void Dispose(bool disposing)
|
||||||
{
|
{
|
||||||
base.Dispose(disposing);
|
base.Dispose(disposing);
|
||||||
@@ -936,7 +996,10 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
_downloadCancellationTokenSource = null;
|
_downloadCancellationTokenSource = null;
|
||||||
_downloadManager.Dispose();
|
_downloadManager.Dispose();
|
||||||
_charaHandler?.Dispose();
|
_charaHandler?.Dispose();
|
||||||
|
CancelVisibilityGraceTask();
|
||||||
_charaHandler = null;
|
_charaHandler = null;
|
||||||
|
_invisibleSinceUtc = null;
|
||||||
|
_visibilityEvictionDueAtUtc = null;
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(name))
|
if (!string.IsNullOrEmpty(name))
|
||||||
{
|
{
|
||||||
@@ -1265,6 +1328,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Task? _pairDownloadTask;
|
private Task? _pairDownloadTask;
|
||||||
|
private Task _visibilityGraceTask;
|
||||||
|
|
||||||
private async Task DownloadAndApplyCharacterAsync(Guid applicationBase, CharacterData charaData, Dictionary<ObjectKind, HashSet<PlayerChanges>> updatedData,
|
private async Task DownloadAndApplyCharacterAsync(Guid applicationBase, CharacterData charaData, Dictionary<ObjectKind, HashSet<PlayerChanges>> updatedData,
|
||||||
bool updateModdedPaths, bool updateManip, Dictionary<(string GamePath, string? Hash), string>? cachedModdedPaths, CancellationToken downloadToken)
|
bool updateModdedPaths, bool updateManip, Dictionary<(string GamePath, string? Hash), string>? cachedModdedPaths, CancellationToken downloadToken)
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ public sealed class PairHandlerRegistry : IDisposable
|
|||||||
{
|
{
|
||||||
private readonly object _gate = new();
|
private readonly object _gate = new();
|
||||||
private readonly object _pendingGate = new();
|
private readonly object _pendingGate = new();
|
||||||
|
private readonly object _visibilityGate = new();
|
||||||
private readonly Dictionary<string, PairHandlerEntry> _entriesByIdent = new(StringComparer.Ordinal);
|
private readonly Dictionary<string, PairHandlerEntry> _entriesByIdent = new(StringComparer.Ordinal);
|
||||||
|
private readonly Dictionary<string, CancellationTokenSource> _pendingInvisibleEvictions = new(StringComparer.Ordinal);
|
||||||
private readonly Dictionary<IPairHandlerAdapter, PairHandlerEntry> _entriesByHandler = new(ReferenceEqualityComparer.Instance);
|
private readonly Dictionary<IPairHandlerAdapter, PairHandlerEntry> _entriesByHandler = new(ReferenceEqualityComparer.Instance);
|
||||||
|
|
||||||
private readonly IPairHandlerAdapterFactory _handlerFactory;
|
private readonly IPairHandlerAdapterFactory _handlerFactory;
|
||||||
@@ -144,6 +146,37 @@ public sealed class PairHandlerRegistry : IDisposable
|
|||||||
return PairOperationResult<PairUniqueIdentifier>.Ok(registration.PairIdent);
|
return PairOperationResult<PairUniqueIdentifier>.Ok(registration.PairIdent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private PairOperationResult CancelAllInvisibleEvictions()
|
||||||
|
{
|
||||||
|
List<CancellationTokenSource> snapshot;
|
||||||
|
lock (_visibilityGate)
|
||||||
|
{
|
||||||
|
snapshot = [.. _pendingInvisibleEvictions.Values];
|
||||||
|
_pendingInvisibleEvictions.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<string>? errors = null;
|
||||||
|
|
||||||
|
foreach (var cts in snapshot)
|
||||||
|
{
|
||||||
|
try { cts.Cancel(); }
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
(errors ??= new List<string>()).Add($"Cancel: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
try { cts.Dispose(); }
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
(errors ??= new List<string>()).Add($"Dispose: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors is null
|
||||||
|
? PairOperationResult.Ok()
|
||||||
|
: PairOperationResult.Fail($"CancelAllInvisibleEvictions had error(s): {string.Join(" | ", errors)}");
|
||||||
|
}
|
||||||
|
|
||||||
public PairOperationResult ApplyCharacterData(PairRegistration registration, OnlineUserCharaDataDto dto)
|
public PairOperationResult ApplyCharacterData(PairRegistration registration, OnlineUserCharaDataDto dto)
|
||||||
{
|
{
|
||||||
if (registration.CharacterIdent is null)
|
if (registration.CharacterIdent is null)
|
||||||
@@ -300,6 +333,7 @@ public sealed class PairHandlerRegistry : IDisposable
|
|||||||
lock (_gate)
|
lock (_gate)
|
||||||
{
|
{
|
||||||
handlers = _entriesByHandler.Keys.ToList();
|
handlers = _entriesByHandler.Keys.ToList();
|
||||||
|
CancelAllInvisibleEvictions();
|
||||||
_entriesByIdent.Clear();
|
_entriesByIdent.Clear();
|
||||||
_entriesByHandler.Clear();
|
_entriesByHandler.Clear();
|
||||||
}
|
}
|
||||||
@@ -332,6 +366,7 @@ public sealed class PairHandlerRegistry : IDisposable
|
|||||||
lock (_gate)
|
lock (_gate)
|
||||||
{
|
{
|
||||||
handlers = _entriesByHandler.Keys.ToList();
|
handlers = _entriesByHandler.Keys.ToList();
|
||||||
|
CancelAllInvisibleEvictions();
|
||||||
_entriesByIdent.Clear();
|
_entriesByIdent.Clear();
|
||||||
_entriesByHandler.Clear();
|
_entriesByHandler.Clear();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ public sealed class CommandManagerService : IDisposable
|
|||||||
"\t /light analyze - Opens the Lightless Character Data Analysis window" + Environment.NewLine +
|
"\t /light analyze - Opens the Lightless Character Data Analysis window" + Environment.NewLine +
|
||||||
"\t /light settings - Opens the Lightless Settings window" + Environment.NewLine +
|
"\t /light settings - Opens the Lightless Settings window" + Environment.NewLine +
|
||||||
"\t /light finder - Opens the Lightfinder window" + Environment.NewLine +
|
"\t /light finder - Opens the Lightfinder window" + Environment.NewLine +
|
||||||
"\t /light finder - Opens the Lightless Chat window"
|
"\t /light chat - Opens the Lightless Chat window"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -126,11 +126,11 @@ public sealed class TextureMetadataHelper
|
|||||||
private const string TextureSegment = "/texture/";
|
private const string TextureSegment = "/texture/";
|
||||||
private const string MaterialSegment = "/material/";
|
private const string MaterialSegment = "/material/";
|
||||||
|
|
||||||
private const uint NormalSamplerId = 0x0C5EC1F1u;
|
private const uint NormalSamplerId = ShpkFile.NormalSamplerId;
|
||||||
private const uint IndexSamplerId = 0x565F8FD8u;
|
private const uint IndexSamplerId = ShpkFile.IndexSamplerId;
|
||||||
private const uint SpecularSamplerId = 0x2B99E025u;
|
private const uint SpecularSamplerId = ShpkFile.SpecularSamplerId;
|
||||||
private const uint DiffuseSamplerId = 0x115306BEu;
|
private const uint DiffuseSamplerId = ShpkFile.DiffuseSamplerId;
|
||||||
private const uint MaskSamplerId = 0x8A4E82B6u;
|
private const uint MaskSamplerId = ShpkFile.MaskSamplerId;
|
||||||
|
|
||||||
public TextureMetadataHelper(ILogger<TextureMetadataHelper> logger, IDataManager dataManager)
|
public TextureMetadataHelper(ILogger<TextureMetadataHelper> logger, IDataManager dataManager)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -843,12 +843,16 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
//Filter of not grouped/foldered and offline pairs
|
//Filter of not grouped/foldered and offline pairs
|
||||||
var allOnlineNotTaggedPairs = SortEntries(allEntries.Where(FilterNotTaggedUsers));
|
var allOnlineNotTaggedPairs = SortEntries(allEntries.Where(FilterNotTaggedUsers));
|
||||||
var onlineNotTaggedPairs = SortEntries(filteredEntries.Where(e => FilterNotTaggedUsers(e) && FilterOnlineOrPausedSelf(e)));
|
if (allOnlineNotTaggedPairs.Count > 0 && _configService.Current.ShowOfflineUsersSeparately) {
|
||||||
|
var filteredOnlineEntries = SortOnlineEntries(filteredEntries.Where(e => FilterNotTaggedUsers(e) && FilterOnlineOrPausedSelf(e)));
|
||||||
if (allOnlineNotTaggedPairs.Count > 0)
|
|
||||||
{
|
|
||||||
drawFolders.Add(_drawEntityFactory.CreateTagFolder(
|
drawFolders.Add(_drawEntityFactory.CreateTagFolder(
|
||||||
_configService.Current.ShowOfflineUsersSeparately ? TagHandler.CustomOnlineTag : TagHandler.CustomAllTag,
|
TagHandler.CustomOnlineTag,
|
||||||
|
filteredOnlineEntries,
|
||||||
|
allOnlineNotTaggedPairs));
|
||||||
|
} else if (allOnlineNotTaggedPairs.Count > 0 && !_configService.Current.ShowOfflineUsersSeparately) {
|
||||||
|
var onlineNotTaggedPairs = SortEntries(filteredEntries.Where(FilterNotTaggedUsers));
|
||||||
|
drawFolders.Add(_drawEntityFactory.CreateTagFolder(
|
||||||
|
TagHandler.CustomAllTag,
|
||||||
onlineNotTaggedPairs,
|
onlineNotTaggedPairs,
|
||||||
allOnlineNotTaggedPairs));
|
allOnlineNotTaggedPairs));
|
||||||
}
|
}
|
||||||
@@ -885,7 +889,7 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool PassesFilter(PairUiEntry entry, string filter)
|
private static bool PassesFilter(PairUiEntry entry, string filter)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(filter)) return true;
|
if (string.IsNullOrEmpty(filter)) return true;
|
||||||
|
|
||||||
@@ -946,6 +950,17 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ImmutableList<PairUiEntry> SortOnlineEntries(IEnumerable<PairUiEntry> entries)
|
||||||
|
{
|
||||||
|
var entryList = entries.ToList();
|
||||||
|
return _configService.Current.OnlinePairSortMode switch
|
||||||
|
{
|
||||||
|
OnlinePairSortMode.Alphabetical => [.. entryList.OrderBy(e => AlphabeticalSortKey(e), StringComparer.OrdinalIgnoreCase)],
|
||||||
|
OnlinePairSortMode.PreferredDirectPairs => SortVisibleByPreferred(entryList),
|
||||||
|
_ => SortEntries(entryList),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private ImmutableList<PairUiEntry> SortVisibleByMetric(IEnumerable<PairUiEntry> entries, Func<PairUiEntry, long> selector)
|
private ImmutableList<PairUiEntry> SortVisibleByMetric(IEnumerable<PairUiEntry> entries, Func<PairUiEntry, long> selector)
|
||||||
{
|
{
|
||||||
return [.. entries
|
return [.. entries
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ using Dalamud.Interface.Utility.Raii;
|
|||||||
using LightlessSync.UI.Handlers;
|
using LightlessSync.UI.Handlers;
|
||||||
using LightlessSync.UI.Models;
|
using LightlessSync.UI.Models;
|
||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using LightlessSync.UI;
|
|
||||||
using LightlessSync.UI.Style;
|
using LightlessSync.UI.Style;
|
||||||
|
using OtterGui.Text;
|
||||||
|
|
||||||
namespace LightlessSync.UI.Components;
|
namespace LightlessSync.UI.Components;
|
||||||
|
|
||||||
@@ -113,9 +113,13 @@ public abstract class DrawFolderBase : IDrawFolder
|
|||||||
using var indent = ImRaii.PushIndent(_uiSharedService.GetIconSize(FontAwesomeIcon.EllipsisV).X + ImGui.GetStyle().ItemSpacing.X, false);
|
using var indent = ImRaii.PushIndent(_uiSharedService.GetIconSize(FontAwesomeIcon.EllipsisV).X + ImGui.GetStyle().ItemSpacing.X, false);
|
||||||
if (DrawPairs.Any())
|
if (DrawPairs.Any())
|
||||||
{
|
{
|
||||||
foreach (var item in DrawPairs)
|
using var clipper = ImUtf8.ListClipper(DrawPairs.Count, ImGui.GetFrameHeightWithSpacing());
|
||||||
|
while (clipper.Step())
|
||||||
{
|
{
|
||||||
item.DrawPairedClient();
|
for (var i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
|
||||||
|
{
|
||||||
|
DrawPairs[i].DrawPairedClient();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -169,11 +169,16 @@ public class DrawFolderTag : DrawFolderBase
|
|||||||
|
|
||||||
protected override float DrawRightSide(float currentRightSideX)
|
protected override float DrawRightSide(float currentRightSideX)
|
||||||
{
|
{
|
||||||
if (_id == TagHandler.CustomVisibleTag)
|
if (string.Equals(_id, TagHandler.CustomVisibleTag, StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
return DrawVisibleFilter(currentRightSideX);
|
return DrawVisibleFilter(currentRightSideX);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (string.Equals(_id, TagHandler.CustomOnlineTag, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return DrawOnlineFilter(currentRightSideX);
|
||||||
|
}
|
||||||
|
|
||||||
if (!RenderPause)
|
if (!RenderPause)
|
||||||
{
|
{
|
||||||
return currentRightSideX;
|
return currentRightSideX;
|
||||||
@@ -254,7 +259,7 @@ public class DrawFolderTag : DrawFolderBase
|
|||||||
foreach (VisiblePairSortMode mode in Enum.GetValues<VisiblePairSortMode>())
|
foreach (VisiblePairSortMode mode in Enum.GetValues<VisiblePairSortMode>())
|
||||||
{
|
{
|
||||||
var selected = _configService.Current.VisiblePairSortMode == mode;
|
var selected = _configService.Current.VisiblePairSortMode == mode;
|
||||||
if (ImGui.MenuItem(GetSortLabel(mode), string.Empty, selected))
|
if (ImGui.MenuItem(GetSortVisibleLabel(mode), string.Empty, selected))
|
||||||
{
|
{
|
||||||
if (!selected)
|
if (!selected)
|
||||||
{
|
{
|
||||||
@@ -273,7 +278,49 @@ public class DrawFolderTag : DrawFolderBase
|
|||||||
return buttonStart - spacingX;
|
return buttonStart - spacingX;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string GetSortLabel(VisiblePairSortMode mode) => mode switch
|
private float DrawOnlineFilter(float currentRightSideX)
|
||||||
|
{
|
||||||
|
var buttonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Filter);
|
||||||
|
var spacingX = ImGui.GetStyle().ItemSpacing.X;
|
||||||
|
var buttonStart = currentRightSideX - buttonSize.X;
|
||||||
|
|
||||||
|
ImGui.SameLine(buttonStart);
|
||||||
|
if (_uiSharedService.IconButton(FontAwesomeIcon.Filter))
|
||||||
|
{
|
||||||
|
SuppressNextRowToggle();
|
||||||
|
ImGui.OpenPopup($"online-filter-{_id}");
|
||||||
|
}
|
||||||
|
|
||||||
|
UiSharedService.AttachToolTip("Adjust how online pairs are ordered.");
|
||||||
|
|
||||||
|
if (ImGui.BeginPopup($"online-filter-{_id}"))
|
||||||
|
{
|
||||||
|
ImGui.TextUnformatted("Online Pair Ordering");
|
||||||
|
ImGui.Separator();
|
||||||
|
|
||||||
|
foreach (OnlinePairSortMode mode in Enum.GetValues<OnlinePairSortMode>())
|
||||||
|
{
|
||||||
|
var selected = _configService.Current.OnlinePairSortMode == mode;
|
||||||
|
if (ImGui.MenuItem(GetSortOnlineLabel(mode), string.Empty, selected))
|
||||||
|
{
|
||||||
|
if (!selected)
|
||||||
|
{
|
||||||
|
_configService.Current.OnlinePairSortMode = mode;
|
||||||
|
_configService.Save();
|
||||||
|
_mediator.Publish(new RefreshUiMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.CloseCurrentPopup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.EndPopup();
|
||||||
|
}
|
||||||
|
|
||||||
|
return buttonStart - spacingX;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetSortVisibleLabel(VisiblePairSortMode mode) => mode switch
|
||||||
{
|
{
|
||||||
VisiblePairSortMode.Alphabetical => "Alphabetical",
|
VisiblePairSortMode.Alphabetical => "Alphabetical",
|
||||||
VisiblePairSortMode.VramUsage => "VRAM usage (descending)",
|
VisiblePairSortMode.VramUsage => "VRAM usage (descending)",
|
||||||
@@ -282,4 +329,11 @@ public class DrawFolderTag : DrawFolderBase
|
|||||||
VisiblePairSortMode.PreferredDirectPairs => "Preferred permissions & Direct pairs",
|
VisiblePairSortMode.PreferredDirectPairs => "Preferred permissions & Direct pairs",
|
||||||
_ => "Default",
|
_ => "Default",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private static string GetSortOnlineLabel(OnlinePairSortMode mode) => mode switch
|
||||||
|
{
|
||||||
|
OnlinePairSortMode.Alphabetical => "Alphabetical",
|
||||||
|
OnlinePairSortMode.PreferredDirectPairs => "Preferred permissions & Direct pairs",
|
||||||
|
_ => "Default",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
@@ -301,6 +301,14 @@ namespace LightlessSync.UI
|
|||||||
bool ShellFinderEnabled = _configService.Current.SyncshellFinderEnabled;
|
bool ShellFinderEnabled = _configService.Current.SyncshellFinderEnabled;
|
||||||
bool isBroadcasting = _broadcastService.IsBroadcasting;
|
bool isBroadcasting = _broadcastService.IsBroadcasting;
|
||||||
|
|
||||||
|
if (isBroadcasting)
|
||||||
|
{
|
||||||
|
var warningColor = UIColors.Get("LightlessYellow");
|
||||||
|
_uiSharedService.DrawNoteLine("! ", warningColor,
|
||||||
|
new SeStringUtils.RichTextEntry("Syncshell Finder can only be changed while Lightfinder is disabled.", warningColor));
|
||||||
|
ImGuiHelpers.ScaledDummy(0.2f);
|
||||||
|
}
|
||||||
|
|
||||||
if (isBroadcasting)
|
if (isBroadcasting)
|
||||||
ImGui.BeginDisabled();
|
ImGui.BeginDisabled();
|
||||||
|
|
||||||
|
|||||||
7
LightlessSync/UI/Models/OnlinePairSortMode.cs
Normal file
7
LightlessSync/UI/Models/OnlinePairSortMode.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace LightlessSync.UI.Models;
|
||||||
|
|
||||||
|
public enum OnlinePairSortMode
|
||||||
|
{
|
||||||
|
Alphabetical = 0,
|
||||||
|
PreferredDirectPairs = 1,
|
||||||
|
}
|
||||||
@@ -2,10 +2,9 @@ namespace LightlessSync.UI.Models;
|
|||||||
|
|
||||||
public enum VisiblePairSortMode
|
public enum VisiblePairSortMode
|
||||||
{
|
{
|
||||||
Default = 0,
|
Alphabetical = 0,
|
||||||
Alphabetical = 1,
|
VramUsage = 1,
|
||||||
VramUsage = 2,
|
EffectiveVramUsage = 2,
|
||||||
EffectiveVramUsage = 3,
|
TriangleCount = 3,
|
||||||
TriangleCount = 4,
|
PreferredDirectPairs = 4,
|
||||||
PreferredDirectPairs = 5,
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1463,7 +1463,10 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
DrawPairPropertyRow("Has Handler", FormatBool(debugInfo.HasHandler));
|
DrawPairPropertyRow("Has Handler", FormatBool(debugInfo.HasHandler));
|
||||||
DrawPairPropertyRow("Handler Initialized", FormatBool(debugInfo.HandlerInitialized));
|
DrawPairPropertyRow("Handler Initialized", FormatBool(debugInfo.HandlerInitialized));
|
||||||
DrawPairPropertyRow("Handler Visible", FormatBool(debugInfo.HandlerVisible));
|
DrawPairPropertyRow("Handler Visible", FormatBool(debugInfo.HandlerVisible));
|
||||||
|
DrawPairPropertyRow("Last Time person rendered in", FormatTimestamp(debugInfo.InvisibleSinceUtc));
|
||||||
|
DrawPairPropertyRow("Handler Timer Temp Collection removal", FormatCountdown(debugInfo.VisibilityEvictionRemainingSeconds));
|
||||||
DrawPairPropertyRow("Handler Scheduled For Deletion", FormatBool(debugInfo.HandlerScheduledForDeletion));
|
DrawPairPropertyRow("Handler Scheduled For Deletion", FormatBool(debugInfo.HandlerScheduledForDeletion));
|
||||||
|
|
||||||
DrawPairPropertyRow("Note", pair.GetNote() ?? "(none)");
|
DrawPairPropertyRow("Note", pair.GetNote() ?? "(none)");
|
||||||
ImGui.EndTable();
|
ImGui.EndTable();
|
||||||
}
|
}
|
||||||
@@ -1698,6 +1701,19 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
return value is null ? "n/a" : value.Value.ToLocalTime().ToString("G", CultureInfo.CurrentCulture);
|
return value is null ? "n/a" : value.Value.ToLocalTime().ToString("G", CultureInfo.CurrentCulture);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string? FormatCountdown(double? remainingSeconds)
|
||||||
|
{
|
||||||
|
if (!remainingSeconds.HasValue)
|
||||||
|
return "No";
|
||||||
|
|
||||||
|
var secs = Math.Max(0, remainingSeconds.Value);
|
||||||
|
var t = TimeSpan.FromSeconds(secs);
|
||||||
|
|
||||||
|
return t.TotalHours >= 1
|
||||||
|
? $"{(int)t.TotalHours:00}:{t.Minutes:00}:{t.Seconds:00}"
|
||||||
|
: $"{(int)t.TotalMinutes:00}:{t.Seconds:00}";
|
||||||
|
}
|
||||||
|
|
||||||
private static string FormatBytes(long value) => value < 0 ? "n/a" : UiSharedService.ByteToString(value);
|
private static string FormatBytes(long value) => value < 0 ? "n/a" : UiSharedService.ByteToString(value);
|
||||||
|
|
||||||
private static string FormatCharacterId(uint id) => id == uint.MaxValue ? "n/a" : $"{id} (0x{id:X8})";
|
private static string FormatCharacterId(uint id) => id == uint.MaxValue ? "n/a" : $"{id} (0x{id:X8})";
|
||||||
|
|||||||
@@ -350,9 +350,9 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
|||||||
? shell.Group.Alias
|
? shell.Group.Alias
|
||||||
: shell.Group.GID;
|
: shell.Group.GID;
|
||||||
|
|
||||||
|
var style = ImGui.GetStyle();
|
||||||
float startX = ImGui.GetCursorPosX();
|
float startX = ImGui.GetCursorPosX();
|
||||||
float availWidth = ImGui.GetContentRegionAvail().X;
|
float availW = ImGui.GetContentRegionAvail().X;
|
||||||
float rightTextW = ImGui.CalcTextSize(broadcasterName).X;
|
|
||||||
|
|
||||||
ImGui.BeginGroup();
|
ImGui.BeginGroup();
|
||||||
|
|
||||||
@@ -364,13 +364,45 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
|||||||
Mediator.Publish(new GroupProfileOpenStandaloneMessage(shell.Group));
|
Mediator.Publish(new GroupProfileOpenStandaloneMessage(shell.Group));
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui.SameLine();
|
float nameRightX = ImGui.GetItemRectMax().X;
|
||||||
float rightX = startX + availWidth - rightTextW;
|
|
||||||
var pos = ImGui.GetCursorPos();
|
var regionMinScreen = ImGui.GetCursorScreenPos();
|
||||||
ImGui.SetCursorPos(new Vector2(rightX, pos.Y + 3f * ImGuiHelpers.GlobalScale));
|
float regionRightX = regionMinScreen.X + availW;
|
||||||
ImGui.TextUnformatted(broadcasterName);
|
|
||||||
if (ImGui.IsItemHovered())
|
float minBroadcasterX = nameRightX + style.ItemSpacing.X;
|
||||||
ImGui.SetTooltip("Broadcaster of the syncshell.");
|
|
||||||
|
float maxBroadcasterWidth = regionRightX - minBroadcasterX;
|
||||||
|
|
||||||
|
string broadcasterToShow = broadcasterName;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(broadcasterName) && maxBroadcasterWidth > 0f)
|
||||||
|
{
|
||||||
|
float bcFullWidth = ImGui.CalcTextSize(broadcasterName).X;
|
||||||
|
string toolTip;
|
||||||
|
|
||||||
|
if (bcFullWidth > maxBroadcasterWidth)
|
||||||
|
{
|
||||||
|
broadcasterToShow = TruncateTextToWidth(broadcasterName, maxBroadcasterWidth);
|
||||||
|
toolTip = broadcasterName + Environment.NewLine + Environment.NewLine + "Broadcaster of the syncshell.";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
toolTip = "Broadcaster of the syncshell.";
|
||||||
|
}
|
||||||
|
|
||||||
|
float bcWidth = ImGui.CalcTextSize(broadcasterToShow).X;
|
||||||
|
|
||||||
|
float broadX = regionRightX - bcWidth;
|
||||||
|
|
||||||
|
broadX = MathF.Max(broadX, minBroadcasterX);
|
||||||
|
|
||||||
|
ImGui.SameLine();
|
||||||
|
var curPos = ImGui.GetCursorPos();
|
||||||
|
ImGui.SetCursorPos(new Vector2(broadX - regionMinScreen.X + startX, curPos.Y + 3f * ImGuiHelpers.GlobalScale));
|
||||||
|
ImGui.TextUnformatted(broadcasterToShow);
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip(toolTip);
|
||||||
|
}
|
||||||
|
|
||||||
ImGui.EndGroup();
|
ImGui.EndGroup();
|
||||||
|
|
||||||
@@ -590,6 +622,40 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
|||||||
float widthUsed = cursorLocalX - baseLocal.X;
|
float widthUsed = cursorLocalX - baseLocal.X;
|
||||||
return (widthUsed, rowHeight);
|
return (widthUsed, rowHeight);
|
||||||
}
|
}
|
||||||
|
private static string TruncateTextToWidth(string text, float maxWidth)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(text))
|
||||||
|
return text;
|
||||||
|
|
||||||
|
const string ellipsis = "...";
|
||||||
|
float ellipsisWidth = ImGui.CalcTextSize(ellipsis).X;
|
||||||
|
|
||||||
|
if (maxWidth <= ellipsisWidth)
|
||||||
|
return ellipsis;
|
||||||
|
|
||||||
|
int low = 0;
|
||||||
|
int high = text.Length;
|
||||||
|
string best = ellipsis;
|
||||||
|
|
||||||
|
while (low <= high)
|
||||||
|
{
|
||||||
|
int mid = (low + high) / 2;
|
||||||
|
string candidate = string.Concat(text.AsSpan(0, mid), ellipsis);
|
||||||
|
float width = ImGui.CalcTextSize(candidate).X;
|
||||||
|
|
||||||
|
if (width <= maxWidth)
|
||||||
|
{
|
||||||
|
best = candidate;
|
||||||
|
low = mid + 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
high = mid - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
private IDalamudTextureWrap? GetIconWrap(uint iconId)
|
private IDalamudTextureWrap? GetIconWrap(uint iconId)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -11,9 +11,11 @@ using LightlessSync.LightlessConfiguration;
|
|||||||
using LightlessSync.LightlessConfiguration.Models;
|
using LightlessSync.LightlessConfiguration.Models;
|
||||||
using LightlessSync.Services;
|
using LightlessSync.Services;
|
||||||
using LightlessSync.Services.Chat;
|
using LightlessSync.Services.Chat;
|
||||||
|
using LightlessSync.Services.LightFinder;
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
using LightlessSync.Services.ServerConfiguration;
|
using LightlessSync.Services.ServerConfiguration;
|
||||||
using LightlessSync.UI.Services;
|
using LightlessSync.UI.Services;
|
||||||
|
using LightlessSync.UI.Style;
|
||||||
using LightlessSync.Utils;
|
using LightlessSync.Utils;
|
||||||
using LightlessSync.WebAPI;
|
using LightlessSync.WebAPI;
|
||||||
using LightlessSync.WebAPI.SignalR.Utils;
|
using LightlessSync.WebAPI.SignalR.Utils;
|
||||||
@@ -29,10 +31,13 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
|||||||
private const string ReportPopupId = "Report Message##zone_chat_report_popup";
|
private const string ReportPopupId = "Report Message##zone_chat_report_popup";
|
||||||
private const string ChannelDragPayloadId = "zone_chat_channel_drag";
|
private const string ChannelDragPayloadId = "zone_chat_channel_drag";
|
||||||
private const float DefaultWindowOpacity = .97f;
|
private const float DefaultWindowOpacity = .97f;
|
||||||
|
private const float DefaultUnfocusedWindowOpacity = 0.6f;
|
||||||
private const float MinWindowOpacity = 0.05f;
|
private const float MinWindowOpacity = 0.05f;
|
||||||
private const float MaxWindowOpacity = 1f;
|
private const float MaxWindowOpacity = 1f;
|
||||||
private const float MinChatFontScale = 0.75f;
|
private const float MinChatFontScale = 0.75f;
|
||||||
private const float MaxChatFontScale = 1.5f;
|
private const float MaxChatFontScale = 1.5f;
|
||||||
|
private const float UnfocusedFadeOutSpeed = 0.22f;
|
||||||
|
private const float FocusFadeInSpeed = 2.0f;
|
||||||
private const int ReportReasonMaxLength = 500;
|
private const int ReportReasonMaxLength = 500;
|
||||||
private const int ReportContextMaxLength = 1000;
|
private const int ReportContextMaxLength = 1000;
|
||||||
private const int MaxChannelNoteTabLength = 25;
|
private const int MaxChannelNoteTabLength = 25;
|
||||||
@@ -40,6 +45,7 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
|||||||
private readonly UiSharedService _uiSharedService;
|
private readonly UiSharedService _uiSharedService;
|
||||||
private readonly ZoneChatService _zoneChatService;
|
private readonly ZoneChatService _zoneChatService;
|
||||||
private readonly PairUiService _pairUiService;
|
private readonly PairUiService _pairUiService;
|
||||||
|
private readonly LightFinderService _lightFinderService;
|
||||||
private readonly LightlessProfileManager _profileManager;
|
private readonly LightlessProfileManager _profileManager;
|
||||||
private readonly ApiController _apiController;
|
private readonly ApiController _apiController;
|
||||||
private readonly ChatConfigService _chatConfigService;
|
private readonly ChatConfigService _chatConfigService;
|
||||||
@@ -49,16 +55,20 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
|||||||
private readonly Dictionary<string, string> _draftMessages = new(StringComparer.Ordinal);
|
private readonly Dictionary<string, string> _draftMessages = new(StringComparer.Ordinal);
|
||||||
private readonly ImGuiWindowFlags _unpinnedWindowFlags;
|
private readonly ImGuiWindowFlags _unpinnedWindowFlags;
|
||||||
private float _currentWindowOpacity = DefaultWindowOpacity;
|
private float _currentWindowOpacity = DefaultWindowOpacity;
|
||||||
|
private float _baseWindowOpacity = DefaultWindowOpacity;
|
||||||
private bool _isWindowPinned;
|
private bool _isWindowPinned;
|
||||||
private bool _showRulesOverlay;
|
private bool _showRulesOverlay;
|
||||||
private bool _refocusChatInput;
|
private bool _refocusChatInput;
|
||||||
private string? _refocusChatInputKey;
|
private string? _refocusChatInputKey;
|
||||||
|
private bool _isWindowFocused = true;
|
||||||
|
private int _titleBarStylePopCount;
|
||||||
|
|
||||||
private string? _selectedChannelKey;
|
private string? _selectedChannelKey;
|
||||||
private bool _scrollToBottom = true;
|
private bool _scrollToBottom = true;
|
||||||
private float? _pendingChannelScroll;
|
private float? _pendingChannelScroll;
|
||||||
private float _channelScroll;
|
private float _channelScroll;
|
||||||
private float _channelScrollMax;
|
private float _channelScrollMax;
|
||||||
|
private readonly SeluneBrush _seluneBrush = new();
|
||||||
private ChatChannelSnapshot? _reportTargetChannel;
|
private ChatChannelSnapshot? _reportTargetChannel;
|
||||||
private ChatMessageEntry? _reportTargetMessage;
|
private ChatMessageEntry? _reportTargetMessage;
|
||||||
private string _reportReason = string.Empty;
|
private string _reportReason = string.Empty;
|
||||||
@@ -79,6 +89,7 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
|||||||
UiSharedService uiSharedService,
|
UiSharedService uiSharedService,
|
||||||
ZoneChatService zoneChatService,
|
ZoneChatService zoneChatService,
|
||||||
PairUiService pairUiService,
|
PairUiService pairUiService,
|
||||||
|
LightFinderService lightFinderService,
|
||||||
LightlessProfileManager profileManager,
|
LightlessProfileManager profileManager,
|
||||||
ChatConfigService chatConfigService,
|
ChatConfigService chatConfigService,
|
||||||
ServerConfigurationManager serverConfigurationManager,
|
ServerConfigurationManager serverConfigurationManager,
|
||||||
@@ -91,6 +102,7 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
|||||||
_uiSharedService = uiSharedService;
|
_uiSharedService = uiSharedService;
|
||||||
_zoneChatService = zoneChatService;
|
_zoneChatService = zoneChatService;
|
||||||
_pairUiService = pairUiService;
|
_pairUiService = pairUiService;
|
||||||
|
_lightFinderService = lightFinderService;
|
||||||
_profileManager = profileManager;
|
_profileManager = profileManager;
|
||||||
_chatConfigService = chatConfigService;
|
_chatConfigService = chatConfigService;
|
||||||
_serverConfigurationManager = serverConfigurationManager;
|
_serverConfigurationManager = serverConfigurationManager;
|
||||||
@@ -124,8 +136,25 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
RefreshWindowFlags();
|
RefreshWindowFlags();
|
||||||
base.PreDraw();
|
base.PreDraw();
|
||||||
_currentWindowOpacity = Math.Clamp(_chatConfigService.Current.ChatWindowOpacity, MinWindowOpacity, MaxWindowOpacity);
|
var config = _chatConfigService.Current;
|
||||||
|
var baseOpacity = Math.Clamp(config.ChatWindowOpacity, MinWindowOpacity, MaxWindowOpacity);
|
||||||
|
_baseWindowOpacity = baseOpacity;
|
||||||
|
|
||||||
|
if (config.FadeWhenUnfocused)
|
||||||
|
{
|
||||||
|
var unfocusedOpacity = Math.Clamp(config.UnfocusedWindowOpacity, MinWindowOpacity, MaxWindowOpacity);
|
||||||
|
var targetOpacity = _isWindowFocused ? baseOpacity : Math.Min(baseOpacity, unfocusedOpacity);
|
||||||
|
var delta = ImGui.GetIO().DeltaTime;
|
||||||
|
var speed = _isWindowFocused ? FocusFadeInSpeed : UnfocusedFadeOutSpeed;
|
||||||
|
_currentWindowOpacity = MoveTowards(_currentWindowOpacity, targetOpacity, speed * delta);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_currentWindowOpacity = baseOpacity;
|
||||||
|
}
|
||||||
|
|
||||||
ImGui.SetNextWindowBgAlpha(_currentWindowOpacity);
|
ImGui.SetNextWindowBgAlpha(_currentWindowOpacity);
|
||||||
|
PushTitleBarFadeColors(_currentWindowOpacity);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateHideState()
|
private void UpdateHideState()
|
||||||
@@ -179,8 +208,36 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
protected override void DrawInternal()
|
protected override void DrawInternal()
|
||||||
{
|
{
|
||||||
|
if (_titleBarStylePopCount > 0)
|
||||||
|
{
|
||||||
|
ImGui.PopStyleColor(_titleBarStylePopCount);
|
||||||
|
_titleBarStylePopCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var config = _chatConfigService.Current;
|
||||||
|
var isFocused = ImGui.IsWindowFocused(ImGuiFocusedFlags.RootAndChildWindows);
|
||||||
|
var isHovered = ImGui.IsWindowHovered(ImGuiHoveredFlags.RootAndChildWindows);
|
||||||
|
if (config.FadeWhenUnfocused && isHovered && !isFocused)
|
||||||
|
{
|
||||||
|
ImGui.SetWindowFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
_isWindowFocused = config.FadeWhenUnfocused ? (isFocused || isHovered) : isFocused;
|
||||||
|
|
||||||
|
var contentAlpha = 1f;
|
||||||
|
if (config.FadeWhenUnfocused)
|
||||||
|
{
|
||||||
|
var baseOpacity = MathF.Max(_baseWindowOpacity, 0.001f);
|
||||||
|
contentAlpha = Math.Clamp(_currentWindowOpacity / baseOpacity, 0f, 1f);
|
||||||
|
}
|
||||||
|
|
||||||
|
using var alpha = ImRaii.PushStyle(ImGuiStyleVar.Alpha, contentAlpha);
|
||||||
|
var drawList = ImGui.GetWindowDrawList();
|
||||||
|
var windowPos = ImGui.GetWindowPos();
|
||||||
|
var windowSize = ImGui.GetWindowSize();
|
||||||
|
using var selune = Selune.Begin(_seluneBrush, drawList, windowPos, windowSize);
|
||||||
var childBgColor = ImGui.GetStyle().Colors[(int)ImGuiCol.ChildBg];
|
var childBgColor = ImGui.GetStyle().Colors[(int)ImGuiCol.ChildBg];
|
||||||
childBgColor.W *= _currentWindowOpacity;
|
childBgColor.W *= _baseWindowOpacity;
|
||||||
using var childBg = ImRaii.PushColor(ImGuiCol.ChildBg, childBgColor);
|
using var childBg = ImRaii.PushColor(ImGuiCol.ChildBg, childBgColor);
|
||||||
DrawConnectionControls();
|
DrawConnectionControls();
|
||||||
|
|
||||||
@@ -192,36 +249,58 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
|||||||
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey3);
|
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey3);
|
||||||
ImGui.TextWrapped("No chat channels available.");
|
ImGui.TextWrapped("No chat channels available.");
|
||||||
ImGui.PopStyleColor();
|
ImGui.PopStyleColor();
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
EnsureSelectedChannel(channels);
|
|
||||||
CleanupDrafts(channels);
|
|
||||||
|
|
||||||
DrawChannelButtons(channels);
|
|
||||||
|
|
||||||
if (_selectedChannelKey is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var activeChannel = channels.FirstOrDefault(channel => string.Equals(channel.Key, _selectedChannelKey, StringComparison.Ordinal));
|
|
||||||
if (activeChannel.Equals(default(ChatChannelSnapshot)))
|
|
||||||
{
|
{
|
||||||
activeChannel = channels[0];
|
EnsureSelectedChannel(channels);
|
||||||
_selectedChannelKey = activeChannel.Key;
|
CleanupDrafts(channels);
|
||||||
|
|
||||||
|
DrawChannelButtons(channels);
|
||||||
|
|
||||||
|
if (_selectedChannelKey is null)
|
||||||
|
{
|
||||||
|
selune.DrawHighlightOnly(ImGui.GetIO().DeltaTime);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var activeChannel = channels.FirstOrDefault(channel => string.Equals(channel.Key, _selectedChannelKey, StringComparison.Ordinal));
|
||||||
|
if (activeChannel.Equals(default(ChatChannelSnapshot)))
|
||||||
|
{
|
||||||
|
activeChannel = channels[0];
|
||||||
|
_selectedChannelKey = activeChannel.Key;
|
||||||
|
}
|
||||||
|
|
||||||
|
_zoneChatService.SetActiveChannel(activeChannel.Key);
|
||||||
|
|
||||||
|
DrawHeader(activeChannel);
|
||||||
|
ImGui.Separator();
|
||||||
|
DrawMessageArea(activeChannel, _currentWindowOpacity);
|
||||||
|
ImGui.Separator();
|
||||||
|
DrawInput(activeChannel);
|
||||||
}
|
}
|
||||||
|
|
||||||
_zoneChatService.SetActiveChannel(activeChannel.Key);
|
|
||||||
|
|
||||||
DrawHeader(activeChannel);
|
|
||||||
ImGui.Separator();
|
|
||||||
DrawMessageArea(activeChannel, _currentWindowOpacity);
|
|
||||||
ImGui.Separator();
|
|
||||||
DrawInput(activeChannel);
|
|
||||||
|
|
||||||
if (_showRulesOverlay)
|
if (_showRulesOverlay)
|
||||||
{
|
{
|
||||||
DrawRulesOverlay();
|
DrawRulesOverlay();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
selune.DrawHighlightOnly(ImGui.GetIO().DeltaTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PushTitleBarFadeColors(float opacity)
|
||||||
|
{
|
||||||
|
_titleBarStylePopCount = 0;
|
||||||
|
var alpha = Math.Clamp(opacity, 0f, 1f);
|
||||||
|
var colors = ImGui.GetStyle().Colors;
|
||||||
|
|
||||||
|
var titleBg = colors[(int)ImGuiCol.TitleBg];
|
||||||
|
var titleBgActive = colors[(int)ImGuiCol.TitleBgActive];
|
||||||
|
var titleBgCollapsed = colors[(int)ImGuiCol.TitleBgCollapsed];
|
||||||
|
|
||||||
|
ImGui.PushStyleColor(ImGuiCol.TitleBg, new Vector4(titleBg.X, titleBg.Y, titleBg.Z, titleBg.W * alpha));
|
||||||
|
ImGui.PushStyleColor(ImGuiCol.TitleBgActive, new Vector4(titleBgActive.X, titleBgActive.Y, titleBgActive.Z, titleBgActive.W * alpha));
|
||||||
|
ImGui.PushStyleColor(ImGuiCol.TitleBgCollapsed, new Vector4(titleBgCollapsed.X, titleBgCollapsed.Y, titleBgCollapsed.Z, titleBgCollapsed.W * alpha));
|
||||||
|
_titleBarStylePopCount = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawHeader(ChatChannelSnapshot channel)
|
private void DrawHeader(ChatChannelSnapshot channel)
|
||||||
@@ -1119,18 +1198,56 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
|||||||
var groupSize = ImGui.GetItemRectSize();
|
var groupSize = ImGui.GetItemRectSize();
|
||||||
var minBlockX = cursorStart.X + groupSize.X + style.ItemSpacing.X;
|
var minBlockX = cursorStart.X + groupSize.X + style.ItemSpacing.X;
|
||||||
var availableAfterGroup = contentRightX - (cursorStart.X + groupSize.X);
|
var availableAfterGroup = contentRightX - (cursorStart.X + groupSize.X);
|
||||||
|
var lightfinderButtonWidth = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.PersonCirclePlus).X;
|
||||||
var settingsButtonWidth = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Cog).X;
|
var settingsButtonWidth = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Cog).X;
|
||||||
var pinIcon = _isWindowPinned ? FontAwesomeIcon.Lock : FontAwesomeIcon.Unlock;
|
var pinIcon = _isWindowPinned ? FontAwesomeIcon.Lock : FontAwesomeIcon.Unlock;
|
||||||
var pinButtonWidth = _uiSharedService.GetIconButtonSize(pinIcon).X;
|
var pinButtonWidth = _uiSharedService.GetIconButtonSize(pinIcon).X;
|
||||||
var blockWidth = rulesButtonWidth + style.ItemSpacing.X + settingsButtonWidth + style.ItemSpacing.X + pinButtonWidth;
|
var blockWidth = lightfinderButtonWidth + style.ItemSpacing.X + rulesButtonWidth + style.ItemSpacing.X + settingsButtonWidth + style.ItemSpacing.X + pinButtonWidth;
|
||||||
var desiredBlockX = availableAfterGroup > blockWidth + style.ItemSpacing.X
|
var desiredBlockX = availableAfterGroup > blockWidth + style.ItemSpacing.X
|
||||||
? contentRightX - blockWidth
|
? contentRightX - blockWidth
|
||||||
: minBlockX;
|
: minBlockX;
|
||||||
desiredBlockX = Math.Max(cursorStart.X, desiredBlockX);
|
desiredBlockX = Math.Max(cursorStart.X, desiredBlockX);
|
||||||
var rulesPos = new Vector2(desiredBlockX, cursorStart.Y);
|
var lightfinderPos = new Vector2(desiredBlockX, cursorStart.Y);
|
||||||
var settingsPos = new Vector2(desiredBlockX + rulesButtonWidth + style.ItemSpacing.X, cursorStart.Y);
|
var rulesPos = new Vector2(lightfinderPos.X + lightfinderButtonWidth + style.ItemSpacing.X, cursorStart.Y);
|
||||||
|
var settingsPos = new Vector2(rulesPos.X + rulesButtonWidth + style.ItemSpacing.X, cursorStart.Y);
|
||||||
var pinPos = new Vector2(settingsPos.X + settingsButtonWidth + style.ItemSpacing.X, cursorStart.Y);
|
var pinPos = new Vector2(settingsPos.X + settingsButtonWidth + style.ItemSpacing.X, cursorStart.Y);
|
||||||
|
|
||||||
|
ImGui.SameLine();
|
||||||
|
ImGui.SetCursorPos(lightfinderPos);
|
||||||
|
var lightfinderEnabled = _lightFinderService.IsBroadcasting;
|
||||||
|
var lightfinderColor = lightfinderEnabled ? UIColors.Get("LightlessGreen") : ImGuiColors.DalamudGrey3;
|
||||||
|
var lightfinderButtonSize = new Vector2(lightfinderButtonWidth, ImGui.GetFrameHeight());
|
||||||
|
ImGui.InvisibleButton("zone_chat_lightfinder_button", lightfinderButtonSize);
|
||||||
|
var lightfinderMin = ImGui.GetItemRectMin();
|
||||||
|
var lightfinderMax = ImGui.GetItemRectMax();
|
||||||
|
var iconSize = _uiSharedService.GetIconSize(FontAwesomeIcon.PersonCirclePlus);
|
||||||
|
var iconPos = new Vector2(
|
||||||
|
lightfinderMin.X + (lightfinderButtonSize.X - iconSize.X) * 0.5f,
|
||||||
|
lightfinderMin.Y + (lightfinderButtonSize.Y - iconSize.Y) * 0.5f);
|
||||||
|
using (_uiSharedService.IconFont.Push())
|
||||||
|
{
|
||||||
|
ImGui.GetWindowDrawList().AddText(iconPos, ImGui.GetColorU32(lightfinderColor), FontAwesomeIcon.PersonCirclePlus.ToIconString());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui.IsItemClicked())
|
||||||
|
{
|
||||||
|
Mediator.Publish(new UiToggleMessage(typeof(LightFinderUI)));
|
||||||
|
}
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
{
|
||||||
|
var padding = new Vector2(8f * ImGuiHelpers.GlobalScale);
|
||||||
|
Selune.RegisterHighlight(
|
||||||
|
lightfinderMin - padding,
|
||||||
|
lightfinderMax + padding,
|
||||||
|
SeluneHighlightMode.Point,
|
||||||
|
exactSize: true,
|
||||||
|
clipToElement: true,
|
||||||
|
clipPadding: padding,
|
||||||
|
highlightColorOverride: lightfinderColor,
|
||||||
|
highlightAlphaOverride: 0.2f);
|
||||||
|
ImGui.SetTooltip("If Lightfinder is enabled, you will be able to see the character names of other Lightfinder users in the same zone when they send a message.");
|
||||||
|
}
|
||||||
|
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
ImGui.SetCursorPos(rulesPos);
|
ImGui.SetCursorPos(rulesPos);
|
||||||
if (ImGui.Button("Rules", new Vector2(rulesButtonWidth, 0f)))
|
if (ImGui.Button("Rules", new Vector2(rulesButtonWidth, 0f)))
|
||||||
@@ -1376,9 +1493,55 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
|||||||
ImGui.SetTooltip("Adjust chat window transparency.\nRight-click to reset to default.");
|
ImGui.SetTooltip("Adjust chat window transparency.\nRight-click to reset to default.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var fadeUnfocused = chatConfig.FadeWhenUnfocused;
|
||||||
|
if (ImGui.Checkbox("Fade window when unfocused", ref fadeUnfocused))
|
||||||
|
{
|
||||||
|
chatConfig.FadeWhenUnfocused = fadeUnfocused;
|
||||||
|
_chatConfigService.Save();
|
||||||
|
}
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
{
|
||||||
|
ImGui.SetTooltip("When enabled, the chat window fades after it loses focus.\nHovering the window restores focus.");
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.BeginDisabled(!fadeUnfocused);
|
||||||
|
var unfocusedOpacity = Math.Clamp(chatConfig.UnfocusedWindowOpacity, MinWindowOpacity, MaxWindowOpacity);
|
||||||
|
var unfocusedChanged = ImGui.SliderFloat("Unfocused transparency", ref unfocusedOpacity, MinWindowOpacity, MaxWindowOpacity, "%.2f");
|
||||||
|
var resetUnfocused = ImGui.IsItemClicked(ImGuiMouseButton.Right);
|
||||||
|
if (resetUnfocused)
|
||||||
|
{
|
||||||
|
unfocusedOpacity = DefaultUnfocusedWindowOpacity;
|
||||||
|
unfocusedChanged = true;
|
||||||
|
}
|
||||||
|
if (unfocusedChanged)
|
||||||
|
{
|
||||||
|
chatConfig.UnfocusedWindowOpacity = unfocusedOpacity;
|
||||||
|
_chatConfigService.Save();
|
||||||
|
}
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
{
|
||||||
|
ImGui.SetTooltip("Target transparency while the chat window is unfocused.\nRight-click to reset to default.");
|
||||||
|
}
|
||||||
|
ImGui.EndDisabled();
|
||||||
|
|
||||||
ImGui.EndPopup();
|
ImGui.EndPopup();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static float MoveTowards(float current, float target, float maxDelta)
|
||||||
|
{
|
||||||
|
if (current < target)
|
||||||
|
{
|
||||||
|
return MathF.Min(current + maxDelta, target);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current > target)
|
||||||
|
{
|
||||||
|
return MathF.Max(current - maxDelta, target);
|
||||||
|
}
|
||||||
|
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
private void ToggleChatConnection(bool currentlyEnabled)
|
private void ToggleChatConnection(bool currentlyEnabled)
|
||||||
{
|
{
|
||||||
_ = Task.Run(async () =>
|
_ = Task.Run(async () =>
|
||||||
|
|||||||
Reference in New Issue
Block a user