Compare commits
47 Commits
master
...
2.0.0-cras
| Author | SHA1 | Date | |
|---|---|---|---|
| 543ea6c865 | |||
|
|
3bbda69699 | ||
|
|
deb7f67e59 | ||
|
|
9ba45670c5 | ||
|
|
f7bb73bcd1 | ||
|
|
4c07162ee3 | ||
|
|
a4d62af73d | ||
|
|
5fba3c01e7 | ||
|
|
906dda3885 | ||
|
|
f812b6d09e | ||
| 7e61954541 | |||
|
|
89f59a98f5 | ||
| bbb3375661 | |||
|
|
e95a2c3352 | ||
|
|
a8340c3279 | ||
|
|
e25979e089 | ||
|
|
ca7375b9c3 | ||
|
|
f8752fcb4d | ||
|
|
d1c955c74f | ||
|
|
91e60694ad | ||
|
|
f37fdefddd | ||
|
|
18fa0a47b1 | ||
|
|
9f5cc9e0d1 | ||
|
|
b02db4c1e1 | ||
|
|
d6b31ed5b9 | ||
|
|
9e600bfae0 | ||
|
|
1a73d5a4d9 | ||
|
|
a933330418 | ||
|
|
ea34b18f40 | ||
|
|
67dc215e83 | ||
|
|
baf3869cec | ||
|
|
eeda5aeb66 | ||
|
|
754df95071 | ||
|
|
24fca31606 | ||
|
|
a99c1c01b0 | ||
|
|
85999fab8f | ||
|
|
70745613e1 | ||
|
|
5c8e239a7b | ||
|
|
5eed65149a | ||
|
|
1ab4e2f94b | ||
|
|
f792bc1954 | ||
|
|
ced72ab9eb | ||
|
|
6c1cc77aaa | ||
|
|
5b81caf5a8 | ||
|
|
4e03b381dc | ||
|
|
3222133aa0 | ||
|
|
0ec423e65c |
Submodule LightlessAPI updated: 56566003e0...4ecd5375e6
@@ -155,5 +155,6 @@ public class LightlessConfig : ILightlessConfiguration
|
|||||||
public bool SyncshellFinderEnabled { get; set; } = false;
|
public bool SyncshellFinderEnabled { get; set; } = false;
|
||||||
public string? SelectedFinderSyncshell { get; set; } = null;
|
public string? SelectedFinderSyncshell { get; set; } = null;
|
||||||
public string LastSeenVersion { get; set; } = string.Empty;
|
public string LastSeenVersion { get; set; } = string.Empty;
|
||||||
|
public bool EnableParticleEffects { get; set; } = true;
|
||||||
public HashSet<Guid> OrphanableTempCollections { get; set; } = [];
|
public HashSet<Guid> OrphanableTempCollections { get; set; } = [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors></Authors>
|
<Authors></Authors>
|
||||||
<Company></Company>
|
<Company></Company>
|
||||||
<Version>2.0.2</Version>
|
<Version>2.0.3</Version>
|
||||||
<Description></Description>
|
<Description></Description>
|
||||||
<Copyright></Copyright>
|
<Copyright></Copyright>
|
||||||
<PackageProjectUrl>https://github.com/Light-Public-Syncshells/LightlessClient</PackageProjectUrl>
|
<PackageProjectUrl>https://github.com/Light-Public-Syncshells/LightlessClient</PackageProjectUrl>
|
||||||
@@ -37,6 +37,7 @@
|
|||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="10.0.1" />
|
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="10.0.1" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="10.0.1" />
|
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="10.0.1" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="10.0.1" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.1" />
|
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.1" />
|
||||||
<PackageReference Include="Glamourer.Api" Version="2.8.0" />
|
<PackageReference Include="Glamourer.Api" Version="2.8.0" />
|
||||||
<PackageReference Include="NReco.Logging.File" Version="1.3.1" />
|
<PackageReference Include="NReco.Logging.File" Version="1.3.1" />
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
services.AddSingleton<IdDisplayHandler>();
|
services.AddSingleton<IdDisplayHandler>();
|
||||||
services.AddSingleton<PlayerPerformanceService>();
|
services.AddSingleton<PlayerPerformanceService>();
|
||||||
services.AddSingleton<PenumbraTempCollectionJanitor>();
|
services.AddSingleton<PenumbraTempCollectionJanitor>();
|
||||||
|
services.AddSingleton<LocationShareService>();
|
||||||
|
|
||||||
services.AddSingleton<TextureMetadataHelper>(sp =>
|
services.AddSingleton<TextureMetadataHelper>(sp =>
|
||||||
new TextureMetadataHelper(sp.GetRequiredService<ILogger<TextureMetadataHelper>>(), gameData));
|
new TextureMetadataHelper(sp.GetRequiredService<ILogger<TextureMetadataHelper>>(), gameData));
|
||||||
@@ -480,19 +481,11 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
sp.GetRequiredService<UiSharedService>(),
|
sp.GetRequiredService<UiSharedService>(),
|
||||||
sp.GetRequiredService<ApiController>(),
|
sp.GetRequiredService<ApiController>(),
|
||||||
sp.GetRequiredService<LightFinderScannerService>(),
|
sp.GetRequiredService<LightFinderScannerService>(),
|
||||||
sp.GetRequiredService<LightFinderPlateHandler>()));
|
|
||||||
|
|
||||||
services.AddScoped<WindowMediatorSubscriberBase, SyncshellFinderUI>(sp => new SyncshellFinderUI(
|
|
||||||
sp.GetRequiredService<ILogger<SyncshellFinderUI>>(),
|
|
||||||
sp.GetRequiredService<LightlessMediator>(),
|
|
||||||
sp.GetRequiredService<PerformanceCollectorService>(),
|
|
||||||
sp.GetRequiredService<LightFinderService>(),
|
|
||||||
sp.GetRequiredService<UiSharedService>(),
|
|
||||||
sp.GetRequiredService<ApiController>(),
|
|
||||||
sp.GetRequiredService<LightFinderScannerService>(),
|
|
||||||
sp.GetRequiredService<PairUiService>(),
|
sp.GetRequiredService<PairUiService>(),
|
||||||
sp.GetRequiredService<DalamudUtilService>(),
|
sp.GetRequiredService<DalamudUtilService>(),
|
||||||
sp.GetRequiredService<LightlessProfileManager>()));
|
sp.GetRequiredService<LightlessProfileManager>(),
|
||||||
|
sp.GetRequiredService<ActorObjectService>(),
|
||||||
|
sp.GetRequiredService<LightFinderPlateHandler>()));
|
||||||
|
|
||||||
services.AddScoped<IPopupHandler, BanUserPopupHandler>();
|
services.AddScoped<IPopupHandler, BanUserPopupHandler>();
|
||||||
services.AddScoped<IPopupHandler, CensusPopupHandler>();
|
services.AddScoped<IPopupHandler, CensusPopupHandler>();
|
||||||
@@ -578,7 +571,6 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_host.StopAsync().GetAwaiter().GetResult();
|
_host.StopAsync().ContinueWith(_ => _host.Dispose()).Wait(TimeSpan.FromSeconds(5));
|
||||||
_host.Dispose();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ using LightlessSync.UI;
|
|||||||
using LightlessSync.UI.Services;
|
using LightlessSync.UI.Services;
|
||||||
using LightlessSync.Utils;
|
using LightlessSync.Utils;
|
||||||
using LightlessSync.WebAPI;
|
using LightlessSync.WebAPI;
|
||||||
using Lumina.Excel.Sheets;
|
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
@@ -172,9 +171,8 @@ internal class ContextMenuService : IHostedService
|
|||||||
_logger.LogTrace("Cannot send pair request to {TargetName}@{World} while in PvP or GPose.", target.TargetName, target.TargetHomeWorld.RowId);
|
_logger.LogTrace("Cannot send pair request to {TargetName}@{World} while in PvP or GPose.", target.TargetName, target.TargetHomeWorld.RowId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var world = GetWorld(target.TargetHomeWorld.RowId);
|
if (!IsWorldValid(target.TargetHomeWorld.RowId))
|
||||||
if (!IsWorldValid(world))
|
|
||||||
{
|
{
|
||||||
_logger.LogTrace("Target player {TargetName}@{World} is on an invalid world.", target.TargetName, target.TargetHomeWorld.RowId);
|
_logger.LogTrace("Target player {TargetName}@{World} is on an invalid world.", target.TargetName, target.TargetHomeWorld.RowId);
|
||||||
return;
|
return;
|
||||||
@@ -226,9 +224,8 @@ internal class ContextMenuService : IHostedService
|
|||||||
{
|
{
|
||||||
if (args.Target is not MenuTargetDefault target)
|
if (args.Target is not MenuTargetDefault target)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var world = GetWorld(target.TargetHomeWorld.RowId);
|
if (!target.TargetHomeWorld.IsValid || !IsWorldValid(target.TargetHomeWorld.RowId))
|
||||||
if (!IsWorldValid(world))
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -237,7 +234,7 @@ internal class ContextMenuService : IHostedService
|
|||||||
|
|
||||||
if (targetData == null || targetData.Address == nint.Zero)
|
if (targetData == null || targetData.Address == nint.Zero)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Target player {TargetName}@{World} not found in object table.", target.TargetName, world.Name);
|
_logger.LogWarning("Target player {TargetName}@{World} not found in object table.", target.TargetName, target.TargetHomeWorld.Value.Name);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,7 +249,7 @@ internal class ContextMenuService : IHostedService
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Notify in chat when NotificationService is disabled
|
// Notify in chat when NotificationService is disabled
|
||||||
NotifyInChat($"Pair request sent to {target.TargetName}@{world.Name}.", NotificationType.Info);
|
NotifyInChat($"Pair request sent to {target.TargetName}@{target.TargetHomeWorld.Value.Name}.", NotificationType.Info);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -312,37 +309,8 @@ internal class ContextMenuService : IHostedService
|
|||||||
p.HomeWorld.RowId == target.TargetHomeWorld.RowId);
|
p.HomeWorld.RowId == target.TargetHomeWorld.RowId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private World GetWorld(uint worldId)
|
private bool IsWorldValid(uint worldId)
|
||||||
{
|
{
|
||||||
var sheet = _gameData.GetExcelSheet<World>()!;
|
return _dalamudUtil.WorldData.Value.ContainsKey((ushort)worldId);
|
||||||
var luminaWorlds = sheet.Where(x =>
|
|
||||||
{
|
|
||||||
var dc = x.DataCenter.ValueNullable;
|
|
||||||
var name = x.Name.ExtractText();
|
|
||||||
var internalName = x.InternalName.ExtractText();
|
|
||||||
|
|
||||||
if (dc == null || dc.Value.Region == 0 || string.IsNullOrWhiteSpace(dc.Value.Name.ExtractText()))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(internalName))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (name.Contains('-', StringComparison.Ordinal) || name.Contains('_', StringComparison.Ordinal))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return x.DataCenter.Value.Region != 5 || x.RowId > 3001 && x.RowId != 1200 && IsChineseJapaneseKoreanString(name);
|
|
||||||
});
|
|
||||||
|
|
||||||
return luminaWorlds.FirstOrDefault(x => x.RowId == worldId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsChineseJapaneseKoreanString(string text) => text.All(IsChineseJapaneseKoreanCharacter);
|
|
||||||
|
|
||||||
private static bool IsChineseJapaneseKoreanCharacter(char c) => c >= 0x4E00 && c <= 0x9FFF;
|
|
||||||
|
|
||||||
public static bool IsWorldValid(World world)
|
|
||||||
{
|
|
||||||
var name = world.Name.ToString();
|
|
||||||
return !string.IsNullOrWhiteSpace(name) && char.IsUpper(name[0]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
using Dalamud.Game.ClientState.Conditions;
|
using Dalamud.Game.ClientState.Conditions;
|
||||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||||
using Dalamud.Game.ClientState.Objects.Types;
|
using Dalamud.Game.ClientState.Objects.Types;
|
||||||
|
using Dalamud.Game.Text;
|
||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
using Dalamud.Utility;
|
using Dalamud.Utility;
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game.Control;
|
using FFXIVClientStructs.FFXIV.Client.Game.Control;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Game.UI;
|
||||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||||
using LightlessSync.API.Dto.CharaData;
|
using LightlessSync.API.Dto.CharaData;
|
||||||
@@ -26,6 +28,7 @@ using System.Text;
|
|||||||
using BattleNpcSubKind = FFXIVClientStructs.FFXIV.Client.Game.Object.BattleNpcSubKind;
|
using BattleNpcSubKind = FFXIVClientStructs.FFXIV.Client.Game.Object.BattleNpcSubKind;
|
||||||
using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
|
using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
|
||||||
using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject;
|
using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject;
|
||||||
|
using Map = Lumina.Excel.Sheets.Map;
|
||||||
using VisibilityFlags = FFXIVClientStructs.FFXIV.Client.Game.Object.VisibilityFlags;
|
using VisibilityFlags = FFXIVClientStructs.FFXIV.Client.Game.Object.VisibilityFlags;
|
||||||
|
|
||||||
namespace LightlessSync.Services;
|
namespace LightlessSync.Services;
|
||||||
@@ -57,6 +60,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
private string _lastGlobalBlockReason = string.Empty;
|
private string _lastGlobalBlockReason = string.Empty;
|
||||||
private ushort _lastZone = 0;
|
private ushort _lastZone = 0;
|
||||||
private ushort _lastWorldId = 0;
|
private ushort _lastWorldId = 0;
|
||||||
|
private uint _lastMapId = 0;
|
||||||
private bool _sentBetweenAreas = false;
|
private bool _sentBetweenAreas = false;
|
||||||
private Lazy<ulong> _cid;
|
private Lazy<ulong> _cid;
|
||||||
|
|
||||||
@@ -86,7 +90,8 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
WorldData = new(() =>
|
WorldData = new(() =>
|
||||||
{
|
{
|
||||||
return gameData.GetExcelSheet<Lumina.Excel.Sheets.World>(clientLanguage)!
|
return gameData.GetExcelSheet<Lumina.Excel.Sheets.World>(clientLanguage)!
|
||||||
.Where(w => !w.Name.IsEmpty && w.DataCenter.RowId != 0 && (w.IsPublic || char.IsUpper(w.Name.ToString()[0])))
|
.Where(w => !w.Name.IsEmpty && w.DataCenter.RowId != 0 && (w.IsPublic || char.IsUpper(w.Name.ToString()[0])
|
||||||
|
|| w is { RowId: > 1000, Region: 101 or 201 }))
|
||||||
.ToDictionary(w => (ushort)w.RowId, w => w.Name.ToString());
|
.ToDictionary(w => (ushort)w.RowId, w => w.Name.ToString());
|
||||||
});
|
});
|
||||||
JobData = new(() =>
|
JobData = new(() =>
|
||||||
@@ -659,7 +664,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
|
|
||||||
var location = new LocationInfo();
|
var location = new LocationInfo();
|
||||||
location.ServerId = _playerState.CurrentWorld.RowId;
|
location.ServerId = _playerState.CurrentWorld.RowId;
|
||||||
//location.InstanceId = UIState.Instance()->PublicInstance.InstanceId; //TODO:Need API update first
|
location.InstanceId = UIState.Instance()->PublicInstance.InstanceId;
|
||||||
location.TerritoryId = _clientState.TerritoryType;
|
location.TerritoryId = _clientState.TerritoryType;
|
||||||
location.MapId = _clientState.MapId;
|
location.MapId = _clientState.MapId;
|
||||||
if (houseMan != null)
|
if (houseMan != null)
|
||||||
@@ -685,7 +690,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
var outside = houseMan->OutdoorTerritory;
|
var outside = houseMan->OutdoorTerritory;
|
||||||
var house = outside->HouseId;
|
var house = outside->HouseId;
|
||||||
location.WardId = house.WardIndex + 1u;
|
location.WardId = house.WardIndex + 1u;
|
||||||
location.HouseId = (uint)houseMan->GetCurrentPlot() + 1;
|
//location.HouseId = (uint)houseMan->GetCurrentPlot() + 1;
|
||||||
location.DivisionId = houseMan->GetCurrentDivision();
|
location.DivisionId = houseMan->GetCurrentDivision();
|
||||||
}
|
}
|
||||||
//_logger.LogWarning(LocationToString(location));
|
//_logger.LogWarning(LocationToString(location));
|
||||||
@@ -713,10 +718,10 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
str += $" - {MapData.Value[(ushort)location.MapId].MapName}";
|
str += $" - {MapData.Value[(ushort)location.MapId].MapName}";
|
||||||
}
|
}
|
||||||
|
|
||||||
// if (location.InstanceId is not 0)
|
if (location.InstanceId is not 0)
|
||||||
// {
|
{
|
||||||
// str += ((SeIconChar)(57520 + location.InstanceId)).ToIconString();
|
str += ((SeIconChar)(57520 + location.InstanceId)).ToIconString();
|
||||||
// }
|
}
|
||||||
|
|
||||||
if (location.WardId is not 0)
|
if (location.WardId is not 0)
|
||||||
{
|
{
|
||||||
@@ -914,6 +919,28 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
return WorldData.Value.TryGetValue(worldId, out var worldName) ? worldName : null;
|
return WorldData.Value.TryGetValue(worldId, out var worldName) ? worldName : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void TargetPlayerByAddress(nint address)
|
||||||
|
{
|
||||||
|
if (address == nint.Zero) return;
|
||||||
|
if (_clientState.IsPvP) return;
|
||||||
|
|
||||||
|
_ = RunOnFrameworkThread(() =>
|
||||||
|
{
|
||||||
|
var gameObject = CreateGameObject(address);
|
||||||
|
if (gameObject is null) return;
|
||||||
|
|
||||||
|
var useFocusTarget = _configService.Current.UseFocusTarget;
|
||||||
|
if (useFocusTarget)
|
||||||
|
{
|
||||||
|
_targetManager.FocusTarget = gameObject;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_targetManager.Target = gameObject;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private unsafe void CheckCharacterForDrawing(nint address, string characterName)
|
private unsafe void CheckCharacterForDrawing(nint address, string characterName)
|
||||||
{
|
{
|
||||||
var gameObj = (GameObject*)address;
|
var gameObj = (GameObject*)address;
|
||||||
@@ -1135,6 +1162,18 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
Mediator.Publish(new ZoneSwitchEndMessage());
|
Mediator.Publish(new ZoneSwitchEndMessage());
|
||||||
Mediator.Publish(new ResumeScanMessage(nameof(ConditionFlag.BetweenAreas)));
|
Mediator.Publish(new ResumeScanMessage(nameof(ConditionFlag.BetweenAreas)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Map
|
||||||
|
if (!_sentBetweenAreas)
|
||||||
|
{
|
||||||
|
var mapid = _clientState.MapId;
|
||||||
|
if (mapid != _lastMapId)
|
||||||
|
{
|
||||||
|
_lastMapId = mapid;
|
||||||
|
Mediator.Publish(new MapChangedMessage(mapid));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
var localPlayer = _objectTable.LocalPlayer;
|
var localPlayer = _objectTable.LocalPlayer;
|
||||||
if (localPlayer != null)
|
if (localPlayer != null)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
using LightlessSync.API.Dto.User;
|
using LightlessSync.API.Dto.User;
|
||||||
using LightlessSync.Services.ActorTracking;
|
using LightlessSync.Services.ActorTracking;
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
@@ -23,6 +23,7 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
|
|||||||
private readonly HashSet<string> _syncshellCids = [];
|
private readonly HashSet<string> _syncshellCids = [];
|
||||||
private volatile bool _pendingLocalBroadcast;
|
private volatile bool _pendingLocalBroadcast;
|
||||||
private TimeSpan? _pendingLocalTtl;
|
private TimeSpan? _pendingLocalTtl;
|
||||||
|
private string? _pendingLocalGid;
|
||||||
|
|
||||||
private static readonly TimeSpan _maxAllowedTtl = TimeSpan.FromMinutes(4);
|
private static readonly TimeSpan _maxAllowedTtl = TimeSpan.FromMinutes(4);
|
||||||
private static readonly TimeSpan _retryDelay = TimeSpan.FromMinutes(1);
|
private static readonly TimeSpan _retryDelay = TimeSpan.FromMinutes(1);
|
||||||
@@ -36,6 +37,7 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
|
|||||||
private const int _maxQueueSize = 100;
|
private const int _maxQueueSize = 100;
|
||||||
|
|
||||||
private volatile bool _batchRunning = false;
|
private volatile bool _batchRunning = false;
|
||||||
|
private volatile bool _disposed = false;
|
||||||
|
|
||||||
public IReadOnlyDictionary<string, BroadcastEntry> BroadcastCache => _broadcastCache;
|
public IReadOnlyDictionary<string, BroadcastEntry> BroadcastCache => _broadcastCache;
|
||||||
public readonly record struct BroadcastEntry(bool IsBroadcasting, DateTime ExpiryTime, string? GID);
|
public readonly record struct BroadcastEntry(bool IsBroadcasting, DateTime ExpiryTime, string? GID);
|
||||||
@@ -68,6 +70,9 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
public void Update()
|
public void Update()
|
||||||
{
|
{
|
||||||
|
if (_disposed)
|
||||||
|
return;
|
||||||
|
|
||||||
_frameCounter++;
|
_frameCounter++;
|
||||||
var lookupsThisFrame = 0;
|
var lookupsThisFrame = 0;
|
||||||
|
|
||||||
@@ -111,7 +116,14 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
private async Task BatchUpdateBroadcastCacheAsync(List<string> cids)
|
private async Task BatchUpdateBroadcastCacheAsync(List<string> cids)
|
||||||
{
|
{
|
||||||
|
if (_disposed)
|
||||||
|
return;
|
||||||
|
|
||||||
var results = await _broadcastService.AreUsersBroadcastingAsync(cids).ConfigureAwait(false);
|
var results = await _broadcastService.AreUsersBroadcastingAsync(cids).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (_disposed)
|
||||||
|
return;
|
||||||
|
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
|
|
||||||
foreach (var (cid, info) in results)
|
foreach (var (cid, info) in results)
|
||||||
@@ -130,6 +142,9 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
|
|||||||
(_, old) => new BroadcastEntry(info.IsBroadcasting, expiry, info.GID));
|
(_, old) => new BroadcastEntry(info.IsBroadcasting, expiry, info.GID));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_disposed)
|
||||||
|
return;
|
||||||
|
|
||||||
var activeCids = _broadcastCache
|
var activeCids = _broadcastCache
|
||||||
.Where(e => e.Value.IsBroadcasting && e.Value.ExpiryTime > now)
|
.Where(e => e.Value.IsBroadcasting && e.Value.ExpiryTime > now)
|
||||||
.Select(e => e.Key)
|
.Select(e => e.Key)
|
||||||
@@ -142,6 +157,9 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
private void OnBroadcastStatusChanged(BroadcastStatusChangedMessage msg)
|
private void OnBroadcastStatusChanged(BroadcastStatusChangedMessage msg)
|
||||||
{
|
{
|
||||||
|
if (_disposed)
|
||||||
|
return;
|
||||||
|
|
||||||
if (!msg.Enabled)
|
if (!msg.Enabled)
|
||||||
{
|
{
|
||||||
_broadcastCache.Clear();
|
_broadcastCache.Clear();
|
||||||
@@ -158,6 +176,7 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
_pendingLocalBroadcast = true;
|
_pendingLocalBroadcast = true;
|
||||||
_pendingLocalTtl = msg.Ttl;
|
_pendingLocalTtl = msg.Ttl;
|
||||||
|
_pendingLocalGid = msg.Gid;
|
||||||
TryPrimeLocalBroadcastCache();
|
TryPrimeLocalBroadcastCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,11 +192,12 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
|
|||||||
var expiry = DateTime.UtcNow + ttl;
|
var expiry = DateTime.UtcNow + ttl;
|
||||||
|
|
||||||
_broadcastCache.AddOrUpdate(localCid,
|
_broadcastCache.AddOrUpdate(localCid,
|
||||||
new BroadcastEntry(true, expiry, null),
|
new BroadcastEntry(true, expiry, _pendingLocalGid),
|
||||||
(_, old) => new BroadcastEntry(true, expiry, old.GID));
|
(_, old) => new BroadcastEntry(true, expiry, _pendingLocalGid ?? old.GID));
|
||||||
|
|
||||||
_pendingLocalBroadcast = false;
|
_pendingLocalBroadcast = false;
|
||||||
_pendingLocalTtl = null;
|
_pendingLocalTtl = null;
|
||||||
|
_pendingLocalGid = null;
|
||||||
|
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
var activeCids = _broadcastCache
|
var activeCids = _broadcastCache
|
||||||
@@ -187,10 +207,14 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
_lightFinderPlateHandler.UpdateBroadcastingCids(activeCids);
|
_lightFinderPlateHandler.UpdateBroadcastingCids(activeCids);
|
||||||
_lightFinderNativePlateHandler.UpdateBroadcastingCids(activeCids);
|
_lightFinderNativePlateHandler.UpdateBroadcastingCids(activeCids);
|
||||||
|
UpdateSyncshellBroadcasts();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateSyncshellBroadcasts()
|
private void UpdateSyncshellBroadcasts()
|
||||||
{
|
{
|
||||||
|
if (_disposed)
|
||||||
|
return;
|
||||||
|
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
var nearbyCids = GetNearbyHashedCids(out _);
|
var nearbyCids = GetNearbyHashedCids(out _);
|
||||||
var newSet = nearbyCids.Count == 0
|
var newSet = nearbyCids.Count == 0
|
||||||
@@ -324,17 +348,35 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
protected override void Dispose(bool disposing)
|
protected override void Dispose(bool disposing)
|
||||||
{
|
{
|
||||||
|
_disposed = true;
|
||||||
base.Dispose(disposing);
|
base.Dispose(disposing);
|
||||||
_framework.Update -= OnFrameworkUpdate;
|
_framework.Update -= OnFrameworkUpdate;
|
||||||
if (_cleanupTask != null)
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
_cleanupTask?.Wait(100, _cleanupCts.Token);
|
_cleanupCts.Cancel();
|
||||||
|
}
|
||||||
|
catch (ObjectDisposedException)
|
||||||
|
{
|
||||||
|
// Already disposed, can be ignored :)
|
||||||
}
|
}
|
||||||
|
|
||||||
_cleanupCts.Cancel();
|
try
|
||||||
_cleanupCts.Dispose();
|
{
|
||||||
|
_cleanupTask?.Wait(100);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// Task may have already completed or been cancelled?
|
||||||
|
}
|
||||||
|
|
||||||
_cleanupTask?.Wait(100);
|
try
|
||||||
_cleanupCts.Dispose();
|
{
|
||||||
|
_cleanupCts.Dispose();
|
||||||
|
}
|
||||||
|
catch (ObjectDisposedException)
|
||||||
|
{
|
||||||
|
// Already disposed, ignore
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
using LightlessSync.API.Dto.Group;
|
using LightlessSync.API.Dto.Group;
|
||||||
using LightlessSync.API.Dto.User;
|
using LightlessSync.API.Dto.User;
|
||||||
using LightlessSync.LightlessConfiguration;
|
using LightlessSync.LightlessConfiguration;
|
||||||
@@ -121,7 +121,10 @@ public class LightFinderService : IHostedService, IMediatorSubscriber
|
|||||||
_waitingForTtlFetch = false;
|
_waitingForTtlFetch = false;
|
||||||
|
|
||||||
if (!wasEnabled || previousRemaining != validTtl)
|
if (!wasEnabled || previousRemaining != validTtl)
|
||||||
_mediator.Publish(new BroadcastStatusChangedMessage(true, validTtl));
|
{
|
||||||
|
var gid = _config.Current.SyncshellFinderEnabled ? _config.Current.SelectedFinderSyncshell : null;
|
||||||
|
_mediator.Publish(new BroadcastStatusChangedMessage(true, validTtl, gid));
|
||||||
|
}
|
||||||
|
|
||||||
_logger.LogInformation("Lightfinder broadcast enabled ({Context}), TTL: {TTL}", context, validTtl);
|
_logger.LogInformation("Lightfinder broadcast enabled ({Context}), TTL: {TTL}", context, validTtl);
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
137
LightlessSync/Services/LocationShareService.cs
Normal file
137
LightlessSync/Services/LocationShareService.cs
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
using LightlessSync.API.Data;
|
||||||
|
using LightlessSync.API.Dto.CharaData;
|
||||||
|
using LightlessSync.API.Dto.User;
|
||||||
|
using LightlessSync.Services.Mediator;
|
||||||
|
using LightlessSync.WebAPI;
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Primitives;
|
||||||
|
|
||||||
|
namespace LightlessSync.Services
|
||||||
|
{
|
||||||
|
public class LocationShareService : DisposableMediatorSubscriberBase
|
||||||
|
{
|
||||||
|
private readonly DalamudUtilService _dalamudUtilService;
|
||||||
|
private readonly ApiController _apiController;
|
||||||
|
private IMemoryCache _locations = new MemoryCache(new MemoryCacheOptions());
|
||||||
|
private IMemoryCache _sharingStatus = new MemoryCache(new MemoryCacheOptions());
|
||||||
|
private CancellationTokenSource _resetToken = new CancellationTokenSource();
|
||||||
|
|
||||||
|
public LocationShareService(ILogger<LocationShareService> logger, LightlessMediator mediator, DalamudUtilService dalamudUtilService, ApiController apiController) : base(logger, mediator)
|
||||||
|
{
|
||||||
|
_dalamudUtilService = dalamudUtilService;
|
||||||
|
_apiController = apiController;
|
||||||
|
|
||||||
|
|
||||||
|
Mediator.Subscribe<DisconnectedMessage>(this, (msg) =>
|
||||||
|
{
|
||||||
|
_resetToken.Cancel();
|
||||||
|
_resetToken.Dispose();
|
||||||
|
_resetToken = new CancellationTokenSource();
|
||||||
|
});
|
||||||
|
Mediator.Subscribe<ConnectedMessage>(this, (msg) =>
|
||||||
|
{
|
||||||
|
_ = _apiController.UpdateLocation(new LocationDto(new UserData(_apiController.UID, apiController.DisplayName), _dalamudUtilService.GetMapData()));
|
||||||
|
_ = RequestAllLocation();
|
||||||
|
} );
|
||||||
|
Mediator.Subscribe<LocationSharingMessage>(this, UpdateLocationList);
|
||||||
|
Mediator.Subscribe<MapChangedMessage>(this,
|
||||||
|
msg => _ = _apiController.UpdateLocation(new LocationDto(new UserData(_apiController.UID, _apiController.DisplayName), _dalamudUtilService.GetMapData())));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateLocationList(LocationSharingMessage msg)
|
||||||
|
{
|
||||||
|
if (_locations.TryGetValue(msg.User.UID, out _) && msg.LocationInfo.ServerId is 0)
|
||||||
|
{
|
||||||
|
_locations.Remove(msg.User.UID);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( msg.LocationInfo.ServerId is not 0 && msg.ExpireAt > DateTime.UtcNow)
|
||||||
|
{
|
||||||
|
AddLocationInfo(msg.User.UID, msg.LocationInfo, msg.ExpireAt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddLocationInfo(string uid, LocationInfo location, DateTimeOffset expireAt)
|
||||||
|
{
|
||||||
|
var options = new MemoryCacheEntryOptions()
|
||||||
|
.SetAbsoluteExpiration(expireAt)
|
||||||
|
.AddExpirationToken(new CancellationChangeToken(_resetToken.Token));
|
||||||
|
_locations.Set(uid, location, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RequestAllLocation()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var (data, status) = await _apiController.RequestAllLocationInfo().ConfigureAwait(false);
|
||||||
|
foreach (var dto in data)
|
||||||
|
{
|
||||||
|
AddLocationInfo(dto.LocationDto.User.UID, dto.LocationDto.Location, dto.ExpireAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var dto in status)
|
||||||
|
{
|
||||||
|
AddStatus(dto.User.UID, dto.ExpireAt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.LogError(e,"RequestAllLocation error : ");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddStatus(string uid, DateTimeOffset expireAt)
|
||||||
|
{
|
||||||
|
var options = new MemoryCacheEntryOptions()
|
||||||
|
.SetAbsoluteExpiration(expireAt)
|
||||||
|
.AddExpirationToken(new CancellationChangeToken(_resetToken.Token));
|
||||||
|
_sharingStatus.Set(uid, expireAt, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetUserLocation(string uid)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_locations.TryGetValue<LocationInfo>(uid, out var location))
|
||||||
|
{
|
||||||
|
return _dalamudUtilService.LocationToString(location);
|
||||||
|
}
|
||||||
|
return String.Empty;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.LogError(e,"GetUserLocation error : ");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public DateTimeOffset GetSharingStatus(string uid)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_sharingStatus.TryGetValue<DateTimeOffset>(uid, out var expireAt))
|
||||||
|
{
|
||||||
|
return expireAt;
|
||||||
|
}
|
||||||
|
return DateTimeOffset.MinValue;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.LogError(e,"GetSharingStatus error : ");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateSharingStatus(List<string> users, DateTimeOffset expireAt)
|
||||||
|
{
|
||||||
|
foreach (var user in users)
|
||||||
|
{
|
||||||
|
AddStatus(user, expireAt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -63,23 +63,31 @@ public sealed class LightlessMediator : IHostedService
|
|||||||
|
|
||||||
_ = Task.Run(async () =>
|
_ = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
while (!_loopCts.Token.IsCancellationRequested)
|
try
|
||||||
{
|
{
|
||||||
while (!_processQueue)
|
while (!_loopCts.Token.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
|
while (!_processQueue)
|
||||||
|
{
|
||||||
|
await Task.Delay(100, _loopCts.Token).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
await Task.Delay(100, _loopCts.Token).ConfigureAwait(false);
|
await Task.Delay(100, _loopCts.Token).ConfigureAwait(false);
|
||||||
|
|
||||||
|
HashSet<MessageBase> processedMessages = [];
|
||||||
|
while (_messageQueue.TryDequeue(out var message))
|
||||||
|
{
|
||||||
|
if (processedMessages.Contains(message)) { continue; }
|
||||||
|
|
||||||
|
processedMessages.Add(message);
|
||||||
|
|
||||||
|
ExecuteMessage(message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
await Task.Delay(100, _loopCts.Token).ConfigureAwait(false);
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
HashSet<MessageBase> processedMessages = [];
|
_logger.LogInformation("LightlessMediator stopped");
|
||||||
while (_messageQueue.TryDequeue(out var message))
|
|
||||||
{
|
|
||||||
if (processedMessages.Contains(message)) { continue; }
|
|
||||||
processedMessages.Add(message);
|
|
||||||
|
|
||||||
ExecuteMessage(message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ public record GPoseLobbyReceivePoseData(UserData UserData, PoseData PoseData) :
|
|||||||
public record GPoseLobbyReceiveWorldData(UserData UserData, WorldData WorldData) : MessageBase;
|
public record GPoseLobbyReceiveWorldData(UserData UserData, WorldData WorldData) : MessageBase;
|
||||||
public record OpenCharaDataHubWithFilterMessage(UserData UserData) : MessageBase;
|
public record OpenCharaDataHubWithFilterMessage(UserData UserData) : MessageBase;
|
||||||
public record EnableBroadcastMessage(string HashedCid, bool Enabled) : MessageBase;
|
public record EnableBroadcastMessage(string HashedCid, bool Enabled) : MessageBase;
|
||||||
public record BroadcastStatusChangedMessage(bool Enabled, TimeSpan? Ttl) : MessageBase;
|
public record BroadcastStatusChangedMessage(bool Enabled, TimeSpan? Ttl, string? Gid = null) : MessageBase;
|
||||||
public record UserLeftSyncshell(string gid) : MessageBase;
|
public record UserLeftSyncshell(string gid) : MessageBase;
|
||||||
public record UserJoinedSyncshell(string gid) : MessageBase;
|
public record UserJoinedSyncshell(string gid) : MessageBase;
|
||||||
public record SyncshellBroadcastsUpdatedMessage : MessageBase;
|
public record SyncshellBroadcastsUpdatedMessage : MessageBase;
|
||||||
@@ -135,5 +135,7 @@ public record ChatChannelsUpdated : MessageBase;
|
|||||||
public record ChatChannelMessageAdded(string ChannelKey, ChatMessageEntry Message) : MessageBase;
|
public record ChatChannelMessageAdded(string ChannelKey, ChatMessageEntry Message) : MessageBase;
|
||||||
public record GroupCollectionChangedMessage : MessageBase;
|
public record GroupCollectionChangedMessage : MessageBase;
|
||||||
public record OpenUserProfileMessage(UserData User) : MessageBase;
|
public record OpenUserProfileMessage(UserData User) : MessageBase;
|
||||||
|
public record LocationSharingMessage(UserData User, LocationInfo LocationInfo, DateTimeOffset ExpireAt) : MessageBase;
|
||||||
|
public record MapChangedMessage(uint MapId) : MessageBase;
|
||||||
#pragma warning restore S2094
|
#pragma warning restore S2094
|
||||||
#pragma warning restore MA0048 // File name must match type name
|
#pragma warning restore MA0048 // File name must match type name
|
||||||
@@ -34,44 +34,65 @@ namespace LightlessSync.UI;
|
|||||||
|
|
||||||
public class CompactUi : WindowMediatorSubscriberBase
|
public class CompactUi : WindowMediatorSubscriberBase
|
||||||
{
|
{
|
||||||
private readonly CharacterAnalyzer _characterAnalyzer;
|
#region Constants
|
||||||
|
|
||||||
|
private const float ConnectButtonHighlightThickness = 14f;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Services
|
||||||
|
|
||||||
private readonly ApiController _apiController;
|
private readonly ApiController _apiController;
|
||||||
|
private readonly CharacterAnalyzer _characterAnalyzer;
|
||||||
|
private readonly DalamudUtilService _dalamudUtilService;
|
||||||
|
private readonly DrawEntityFactory _drawEntityFactory;
|
||||||
|
private readonly FileUploadManager _fileTransferManager;
|
||||||
|
private readonly IpcManager _ipcManager;
|
||||||
|
private readonly LightFinderService _broadcastService;
|
||||||
private readonly LightlessConfigService _configService;
|
private readonly LightlessConfigService _configService;
|
||||||
private readonly LightlessMediator _lightlessMediator;
|
private readonly LightlessMediator _lightlessMediator;
|
||||||
private readonly PairLedger _pairLedger;
|
private readonly PairLedger _pairLedger;
|
||||||
private readonly ConcurrentDictionary<GameObjectHandler, Dictionary<string, FileDownloadStatus>> _currentDownloads = new();
|
|
||||||
private readonly DrawEntityFactory _drawEntityFactory;
|
|
||||||
private readonly FileUploadManager _fileTransferManager;
|
|
||||||
private readonly PlayerPerformanceConfigService _playerPerformanceConfig;
|
|
||||||
private readonly PairUiService _pairUiService;
|
private readonly PairUiService _pairUiService;
|
||||||
private readonly SelectTagForPairUi _selectTagForPairUi;
|
private readonly PlayerPerformanceConfigService _playerPerformanceConfig;
|
||||||
private readonly SelectTagForSyncshellUi _selectTagForSyncshellUi;
|
|
||||||
private readonly SelectSyncshellForTagUi _selectSyncshellForTagUi;
|
|
||||||
private readonly RenameSyncshellTagUi _renameSyncshellTagUi;
|
|
||||||
private readonly SelectPairForTagUi _selectPairsForGroupUi;
|
|
||||||
private readonly RenamePairTagUi _renamePairTagUi;
|
|
||||||
private readonly IpcManager _ipcManager;
|
|
||||||
private readonly ServerConfigurationManager _serverManager;
|
private readonly ServerConfigurationManager _serverManager;
|
||||||
private readonly TopTabMenu _tabMenu;
|
|
||||||
private readonly TagHandler _tagHandler;
|
private readonly TagHandler _tagHandler;
|
||||||
private readonly UiSharedService _uiSharedService;
|
private readonly UiSharedService _uiSharedService;
|
||||||
private readonly LightFinderService _broadcastService;
|
|
||||||
private readonly DalamudUtilService _dalamudUtilService;
|
#endregion
|
||||||
|
|
||||||
|
#region UI Components
|
||||||
|
|
||||||
|
private readonly AnimatedHeader _animatedHeader = new();
|
||||||
|
private readonly RenamePairTagUi _renamePairTagUi;
|
||||||
|
private readonly RenameSyncshellTagUi _renameSyncshellTagUi;
|
||||||
|
private readonly SelectPairForTagUi _selectPairsForGroupUi;
|
||||||
|
private readonly SelectSyncshellForTagUi _selectSyncshellForTagUi;
|
||||||
|
private readonly SelectTagForPairUi _selectTagForPairUi;
|
||||||
|
private readonly SelectTagForSyncshellUi _selectTagForSyncshellUi;
|
||||||
|
private readonly SeluneBrush _seluneBrush = new();
|
||||||
|
private readonly TopTabMenu _tabMenu;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region State
|
||||||
|
|
||||||
|
private readonly ConcurrentDictionary<GameObjectHandler, Dictionary<string, FileDownloadStatus>> _currentDownloads = new();
|
||||||
private List<IDrawFolder> _drawFolders;
|
private List<IDrawFolder> _drawFolders;
|
||||||
|
private Pair? _focusedPair;
|
||||||
private Pair? _lastAddedUser;
|
private Pair? _lastAddedUser;
|
||||||
private string _lastAddedUserComment = string.Empty;
|
private string _lastAddedUserComment = string.Empty;
|
||||||
private Vector2 _lastPosition = Vector2.One;
|
private Vector2 _lastPosition = Vector2.One;
|
||||||
private Vector2 _lastSize = Vector2.One;
|
private Vector2 _lastSize = Vector2.One;
|
||||||
|
private int _pendingFocusFrame = -1;
|
||||||
|
private Pair? _pendingFocusPair;
|
||||||
private bool _showModalForUserAddition;
|
private bool _showModalForUserAddition;
|
||||||
private float _transferPartHeight;
|
private float _transferPartHeight;
|
||||||
private bool _wasOpen;
|
private bool _wasOpen;
|
||||||
private float _windowContentWidth;
|
private float _windowContentWidth;
|
||||||
private readonly SeluneBrush _seluneBrush = new();
|
|
||||||
private const float _connectButtonHighlightThickness = 14f;
|
#endregion
|
||||||
private Pair? _focusedPair;
|
|
||||||
private Pair? _pendingFocusPair;
|
#region Constructor
|
||||||
private int _pendingFocusFrame = -1;
|
|
||||||
|
|
||||||
public CompactUi(
|
public CompactUi(
|
||||||
ILogger<CompactUi> logger,
|
ILogger<CompactUi> logger,
|
||||||
@@ -127,6 +148,11 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
.Apply();
|
.Apply();
|
||||||
|
|
||||||
_drawFolders = [.. DrawFolders];
|
_drawFolders = [.. DrawFolders];
|
||||||
|
|
||||||
|
_animatedHeader.Height = 120f;
|
||||||
|
_animatedHeader.EnableBottomGradient = true;
|
||||||
|
_animatedHeader.GradientHeight = 250f;
|
||||||
|
_animatedHeader.EnableParticles = _configService.Current.EnableParticleEffects;
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
string dev = "Dev Build";
|
string dev = "Dev Build";
|
||||||
@@ -150,9 +176,14 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
_lightlessMediator = mediator;
|
_lightlessMediator = mediator;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Lifecycle
|
||||||
|
|
||||||
public override void OnClose()
|
public override void OnClose()
|
||||||
{
|
{
|
||||||
ForceReleaseFocus();
|
ForceReleaseFocus();
|
||||||
|
_animatedHeader.ClearParticles();
|
||||||
base.OnClose();
|
base.OnClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,6 +195,13 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
using var selune = Selune.Begin(_seluneBrush, drawList, windowPos, windowSize);
|
using var selune = Selune.Begin(_seluneBrush, drawList, windowPos, windowSize);
|
||||||
|
|
||||||
_windowContentWidth = UiSharedService.GetWindowContentRegionWidth();
|
_windowContentWidth = UiSharedService.GetWindowContentRegionWidth();
|
||||||
|
|
||||||
|
// Draw animated header background (just the gradient/particles, content drawn by existing methods)
|
||||||
|
var startCursorY = ImGui.GetCursorPosY();
|
||||||
|
_animatedHeader.Draw(_windowContentWidth, (_, _) => { });
|
||||||
|
// Reset cursor to draw content on top of the header background
|
||||||
|
ImGui.SetCursorPosY(startCursorY);
|
||||||
|
|
||||||
if (!_apiController.IsCurrentVersion)
|
if (!_apiController.IsCurrentVersion)
|
||||||
{
|
{
|
||||||
var ver = _apiController.CurrentClientVersion;
|
var ver = _apiController.CurrentClientVersion;
|
||||||
@@ -209,17 +247,11 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
using (ImRaii.PushId("header")) DrawUIDHeader();
|
using (ImRaii.PushId("header")) DrawUIDHeader();
|
||||||
_uiSharedService.RoundedSeparator(UIColors.Get("LightlessPurple"), 2.5f, 1f, 12f);
|
|
||||||
using (ImRaii.PushId("serverstatus"))
|
|
||||||
{
|
|
||||||
DrawServerStatus();
|
|
||||||
}
|
|
||||||
selune.DrawHighlightOnly(ImGui.GetIO().DeltaTime);
|
selune.DrawHighlightOnly(ImGui.GetIO().DeltaTime);
|
||||||
var style = ImGui.GetStyle();
|
var style = ImGui.GetStyle();
|
||||||
var contentMinY = windowPos.Y + ImGui.GetWindowContentRegionMin().Y;
|
var contentMinY = windowPos.Y + ImGui.GetWindowContentRegionMin().Y;
|
||||||
var gradientInset = 4f * ImGuiHelpers.GlobalScale;
|
var gradientInset = 4f * ImGuiHelpers.GlobalScale;
|
||||||
var gradientTop = MathF.Max(contentMinY, ImGui.GetCursorScreenPos().Y - style.ItemSpacing.Y + gradientInset);
|
var gradientTop = MathF.Max(contentMinY, ImGui.GetCursorScreenPos().Y - style.ItemSpacing.Y + gradientInset);
|
||||||
ImGui.Separator();
|
|
||||||
|
|
||||||
if (_apiController.ServerState is ServerState.Connected)
|
if (_apiController.ServerState is ServerState.Connected)
|
||||||
{
|
{
|
||||||
@@ -227,7 +259,6 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
using (ImRaii.PushId("global-topmenu")) _tabMenu.Draw(pairSnapshot);
|
using (ImRaii.PushId("global-topmenu")) _tabMenu.Draw(pairSnapshot);
|
||||||
using (ImRaii.PushId("pairlist")) DrawPairs();
|
using (ImRaii.PushId("pairlist")) DrawPairs();
|
||||||
ImGui.Separator();
|
|
||||||
var transfersTop = ImGui.GetCursorScreenPos().Y;
|
var transfersTop = ImGui.GetCursorScreenPos().Y;
|
||||||
var gradientBottom = MathF.Max(gradientTop, transfersTop - style.ItemSpacing.Y - gradientInset);
|
var gradientBottom = MathF.Max(gradientTop, transfersTop - style.ItemSpacing.Y - gradientInset);
|
||||||
selune.DrawGradient(gradientTop, gradientBottom, ImGui.GetIO().DeltaTime);
|
selune.DrawGradient(gradientTop, gradientBottom, ImGui.GetIO().DeltaTime);
|
||||||
@@ -290,6 +321,10 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Content Drawing
|
||||||
|
|
||||||
private void DrawPairs()
|
private void DrawPairs()
|
||||||
{
|
{
|
||||||
float ySize = Math.Abs(_transferPartHeight) < 0.0001f
|
float ySize = Math.Abs(_transferPartHeight) < 0.0001f
|
||||||
@@ -308,95 +343,6 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
ImGui.EndChild();
|
ImGui.EndChild();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawServerStatus()
|
|
||||||
{
|
|
||||||
var buttonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Link);
|
|
||||||
var userCount = _apiController.OnlineUsers.ToString(CultureInfo.InvariantCulture);
|
|
||||||
var userSize = ImGui.CalcTextSize(userCount);
|
|
||||||
var textSize = ImGui.CalcTextSize("Users Online");
|
|
||||||
#if DEBUG
|
|
||||||
string shardConnection = $"Shard: {_apiController.ServerInfo.ShardName}";
|
|
||||||
#else
|
|
||||||
string shardConnection = string.Equals(_apiController.ServerInfo.ShardName, "Main", StringComparison.OrdinalIgnoreCase) ? string.Empty : $"Shard: {_apiController.ServerInfo.ShardName}";
|
|
||||||
#endif
|
|
||||||
var shardTextSize = ImGui.CalcTextSize(shardConnection);
|
|
||||||
var printShard = !string.IsNullOrEmpty(_apiController.ServerInfo.ShardName) && shardConnection != string.Empty;
|
|
||||||
|
|
||||||
if (_apiController.ServerState is ServerState.Connected)
|
|
||||||
{
|
|
||||||
ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth()) / 2 - (userSize.X + textSize.X) / 2 - ImGui.GetStyle().ItemSpacing.X / 2);
|
|
||||||
if (!printShard) ImGui.AlignTextToFramePadding();
|
|
||||||
ImGui.TextColored(UIColors.Get("LightlessPurple"), userCount);
|
|
||||||
ImGui.SameLine();
|
|
||||||
if (!printShard) ImGui.AlignTextToFramePadding();
|
|
||||||
ImGui.TextUnformatted("Users Online");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ImGui.AlignTextToFramePadding();
|
|
||||||
ImGui.TextColored(UIColors.Get("DimRed"), "Not connected to any server");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (printShard)
|
|
||||||
{
|
|
||||||
ImGui.SetCursorPosY(ImGui.GetCursorPosY() - ImGui.GetStyle().ItemSpacing.Y);
|
|
||||||
ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth()) / 2 - shardTextSize.X / 2);
|
|
||||||
ImGui.TextUnformatted(shardConnection);
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.SameLine();
|
|
||||||
if (printShard)
|
|
||||||
{
|
|
||||||
ImGui.SetCursorPosY(ImGui.GetCursorPosY() - ((userSize.Y + textSize.Y) / 2 + shardTextSize.Y) / 2 - ImGui.GetStyle().ItemSpacing.Y + buttonSize.Y / 2);
|
|
||||||
}
|
|
||||||
bool isConnectingOrConnected = _apiController.ServerState is ServerState.Connected or ServerState.Connecting or ServerState.Reconnecting;
|
|
||||||
var color = UiSharedService.GetBoolColor(!isConnectingOrConnected);
|
|
||||||
var connectedIcon = isConnectingOrConnected ? FontAwesomeIcon.Unlink : FontAwesomeIcon.Link;
|
|
||||||
|
|
||||||
ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth() - buttonSize.X);
|
|
||||||
if (printShard)
|
|
||||||
{
|
|
||||||
ImGui.SetCursorPosY(ImGui.GetCursorPosY() - ((userSize.Y + textSize.Y) / 2 + shardTextSize.Y) / 2 - ImGui.GetStyle().ItemSpacing.Y + buttonSize.Y / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_apiController.ServerState is not (ServerState.Reconnecting or ServerState.Disconnecting))
|
|
||||||
{
|
|
||||||
using (ImRaii.PushColor(ImGuiCol.Text, color))
|
|
||||||
{
|
|
||||||
if (_uiSharedService.IconButton(connectedIcon))
|
|
||||||
{
|
|
||||||
if (isConnectingOrConnected && !_serverManager.CurrentServer.FullPause)
|
|
||||||
{
|
|
||||||
_serverManager.CurrentServer.FullPause = true;
|
|
||||||
_serverManager.Save();
|
|
||||||
}
|
|
||||||
else if (!isConnectingOrConnected && _serverManager.CurrentServer.FullPause)
|
|
||||||
{
|
|
||||||
_serverManager.CurrentServer.FullPause = false;
|
|
||||||
_serverManager.Save();
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = _apiController.CreateConnectionsAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem) || ImGui.IsItemActive())
|
|
||||||
{
|
|
||||||
Selune.RegisterHighlight(
|
|
||||||
ImGui.GetItemRectMin(),
|
|
||||||
ImGui.GetItemRectMax(),
|
|
||||||
SeluneHighlightMode.Both,
|
|
||||||
borderOnly: true,
|
|
||||||
borderThicknessOverride: _connectButtonHighlightThickness,
|
|
||||||
exactSize: true,
|
|
||||||
clipToElement: true,
|
|
||||||
roundingOverride: ImGui.GetStyle().FrameRounding);
|
|
||||||
}
|
|
||||||
|
|
||||||
UiSharedService.AttachToolTip(isConnectingOrConnected ? "Disconnect from " + _serverManager.CurrentServer.ServerName : "Connect to " + _serverManager.CurrentServer.ServerName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawTransfers()
|
private void DrawTransfers()
|
||||||
{
|
{
|
||||||
var currentUploads = _fileTransferManager.GetCurrentUploadsSnapshot();
|
var currentUploads = _fileTransferManager.GetCurrentUploadsSnapshot();
|
||||||
@@ -492,11 +438,9 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
return new DownloadSummary(totalFiles, transferredFiles, transferredBytes, totalBytes);
|
return new DownloadSummary(totalFiles, transferredFiles, transferredBytes, totalBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Auto)]
|
#endregion
|
||||||
private readonly record struct DownloadSummary(int TotalFiles, int TransferredFiles, long TransferredBytes, long TotalBytes)
|
|
||||||
{
|
#region Header Drawing
|
||||||
public bool HasDownloads => TotalFiles > 0 || TotalBytes > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawUIDHeader()
|
private void DrawUIDHeader()
|
||||||
{
|
{
|
||||||
@@ -532,21 +476,52 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
using (_uiSharedService.IconFont.Push())
|
using (_uiSharedService.IconFont.Push())
|
||||||
iconSize = ImGui.CalcTextSize(FontAwesomeIcon.PersonCirclePlus.ToIconString());
|
iconSize = ImGui.CalcTextSize(FontAwesomeIcon.PersonCirclePlus.ToIconString());
|
||||||
|
|
||||||
float contentWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X;
|
float uidStartX = 25f;
|
||||||
float uidStartX = (contentWidth - uidTextSize.X) / 2f;
|
|
||||||
float cursorY = ImGui.GetCursorPosY();
|
float cursorY = ImGui.GetCursorPosY();
|
||||||
|
|
||||||
|
ImGui.SetCursorPosY(cursorY);
|
||||||
|
ImGui.SetCursorPosX(uidStartX);
|
||||||
|
|
||||||
|
bool headerItemClicked;
|
||||||
|
using (_uiSharedService.UidFont.Push())
|
||||||
|
{
|
||||||
|
if (useVanityColors)
|
||||||
|
{
|
||||||
|
var seString = SeStringUtils.BuildFormattedPlayerName(uidText, vanityTextColor, vanityGlowColor);
|
||||||
|
var cursorPos = ImGui.GetCursorScreenPos();
|
||||||
|
var targetFontSize = ImGui.GetFontSize();
|
||||||
|
var font = ImGui.GetFont();
|
||||||
|
SeStringUtils.RenderSeStringWithHitbox(seString, cursorPos, targetFontSize ,font , "uid-header");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ImGui.TextColored(uidColor, uidText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the actual rendered text rect for proper icon alignment
|
||||||
|
var uidTextRect = ImGui.GetItemRectMax() - ImGui.GetItemRectMin();
|
||||||
|
var uidTextRectMin = ImGui.GetItemRectMin();
|
||||||
|
var uidTextHovered = ImGui.IsItemHovered();
|
||||||
|
headerItemClicked = ImGui.IsItemClicked();
|
||||||
|
|
||||||
|
// Track position for icons next to UID text
|
||||||
|
// Use uidTextSize.Y (actual font height) for vertical centering, not hitbox height
|
||||||
|
float nextIconX = uidTextRectMin.X + uidTextRect.X + 10f;
|
||||||
|
float iconYOffset = (uidTextSize.Y - iconSize.Y) * 0.5f;
|
||||||
|
float textVerticalOffset = (uidTextRect.Y - uidTextSize.Y) * 0.5f;
|
||||||
|
var buttonSize = new Vector2(iconSize.X, uidTextSize.Y);
|
||||||
|
|
||||||
if (_configService.Current.BroadcastEnabled && _apiController.IsConnected)
|
if (_configService.Current.BroadcastEnabled && _apiController.IsConnected)
|
||||||
{
|
{
|
||||||
float iconYOffset = (uidTextSize.Y - iconSize.Y) * 0.5f;
|
ImGui.SetCursorScreenPos(new Vector2(nextIconX, uidTextRectMin.Y + textVerticalOffset));
|
||||||
var buttonSize = new Vector2(iconSize.X, uidTextSize.Y);
|
|
||||||
|
|
||||||
ImGui.SetCursorPos(new Vector2(ImGui.GetStyle().ItemSpacing.X + 5f, cursorY));
|
|
||||||
ImGui.InvisibleButton("BroadcastIcon", buttonSize);
|
ImGui.InvisibleButton("BroadcastIcon", buttonSize);
|
||||||
|
|
||||||
var iconPos = ImGui.GetItemRectMin() + new Vector2(0f, iconYOffset);
|
var iconPos = ImGui.GetItemRectMin() + new Vector2(0f, iconYOffset);
|
||||||
using (_uiSharedService.IconFont.Push())
|
using (_uiSharedService.IconFont.Push())
|
||||||
ImGui.GetWindowDrawList().AddText(iconPos, ImGui.GetColorU32(UIColors.Get("LightlessGreen")), FontAwesomeIcon.PersonCirclePlus.ToIconString());
|
ImGui.GetWindowDrawList().AddText(iconPos, ImGui.GetColorU32(UIColors.Get("LightlessGreen")), FontAwesomeIcon.Wifi.ToIconString());
|
||||||
|
|
||||||
|
nextIconX = ImGui.GetItemRectMax().X + 6f;
|
||||||
|
|
||||||
|
|
||||||
if (ImGui.IsItemHovered())
|
if (ImGui.IsItemHovered())
|
||||||
@@ -618,50 +593,8 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
if (ImGui.IsItemClicked())
|
if (ImGui.IsItemClicked())
|
||||||
_lightlessMediator.Publish(new UiToggleMessage(typeof(LightFinderUI)));
|
_lightlessMediator.Publish(new UiToggleMessage(typeof(LightFinderUI)));
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui.SetCursorPosY(cursorY);
|
// Warning threshold icon (next to lightfinder or UID text)
|
||||||
ImGui.SetCursorPosX(uidStartX);
|
|
||||||
|
|
||||||
bool headerItemClicked;
|
|
||||||
using (_uiSharedService.UidFont.Push())
|
|
||||||
{
|
|
||||||
if (useVanityColors)
|
|
||||||
{
|
|
||||||
var seString = SeStringUtils.BuildFormattedPlayerName(uidText, vanityTextColor, vanityGlowColor);
|
|
||||||
var cursorPos = ImGui.GetCursorScreenPos();
|
|
||||||
var targetFontSize = ImGui.GetFontSize();
|
|
||||||
var font = ImGui.GetFont();
|
|
||||||
SeStringUtils.RenderSeStringWithHitbox(seString, cursorPos, targetFontSize ,font , "uid-header");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ImGui.TextColored(uidColor, uidText);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ImGui.IsItemHovered())
|
|
||||||
{
|
|
||||||
var padding = new Vector2(35f * ImGuiHelpers.GlobalScale);
|
|
||||||
Selune.RegisterHighlight(
|
|
||||||
ImGui.GetItemRectMin() - padding,
|
|
||||||
ImGui.GetItemRectMax() + padding,
|
|
||||||
SeluneHighlightMode.Point,
|
|
||||||
exactSize: true,
|
|
||||||
clipToElement: true,
|
|
||||||
clipPadding: padding,
|
|
||||||
highlightColorOverride: vanityGlowColor,
|
|
||||||
highlightAlphaOverride: 0.05f);
|
|
||||||
}
|
|
||||||
|
|
||||||
headerItemClicked = ImGui.IsItemClicked();
|
|
||||||
|
|
||||||
if (headerItemClicked)
|
|
||||||
{
|
|
||||||
ImGui.SetClipboardText(uidText);
|
|
||||||
}
|
|
||||||
|
|
||||||
UiSharedService.AttachToolTip("Click to copy");
|
|
||||||
|
|
||||||
if (_apiController.ServerState is ServerState.Connected && analysisSummary.HasData)
|
if (_apiController.ServerState is ServerState.Connected && analysisSummary.HasData)
|
||||||
{
|
{
|
||||||
var objectSummary = analysisSummary.Objects.Values.FirstOrDefault(summary => summary.HasEntries);
|
var objectSummary = analysisSummary.Objects.Values.FirstOrDefault(summary => summary.HasEntries);
|
||||||
@@ -675,24 +608,30 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
if ((isOverTriHold || isOverVRAMUsage) && _playerPerformanceConfig.Current.WarnOnExceedingThresholds)
|
if ((isOverTriHold || isOverVRAMUsage) && _playerPerformanceConfig.Current.WarnOnExceedingThresholds)
|
||||||
{
|
{
|
||||||
ImGui.SameLine();
|
ImGui.SetCursorScreenPos(new Vector2(nextIconX, uidTextRectMin.Y + textVerticalOffset));
|
||||||
ImGui.SetCursorPosY(cursorY + 15f);
|
|
||||||
_uiSharedService.IconText(FontAwesomeIcon.ExclamationTriangle, UIColors.Get("LightlessYellow"));
|
ImGui.InvisibleButton("WarningThresholdIcon", buttonSize);
|
||||||
|
var warningIconPos = ImGui.GetItemRectMin() + new Vector2(0f, iconYOffset);
|
||||||
|
using (_uiSharedService.IconFont.Push())
|
||||||
|
ImGui.GetWindowDrawList().AddText(warningIconPos, ImGui.GetColorU32(UIColors.Get("LightlessYellow")), FontAwesomeIcon.ExclamationTriangle.ToIconString());
|
||||||
|
|
||||||
string warningMessage = "";
|
if (ImGui.IsItemHovered())
|
||||||
if (isOverTriHold)
|
|
||||||
{
|
{
|
||||||
warningMessage += $"You exceed your own triangles threshold by " +
|
string warningMessage = "";
|
||||||
$"{actualTriCount - _playerPerformanceConfig.Current.TrisWarningThresholdThousands * 1000} triangles.";
|
if (isOverTriHold)
|
||||||
warningMessage += Environment.NewLine;
|
{
|
||||||
|
warningMessage += $"You exceed your own triangles threshold by " +
|
||||||
|
$"{actualTriCount - _playerPerformanceConfig.Current.TrisWarningThresholdThousands * 1000} triangles.";
|
||||||
|
warningMessage += Environment.NewLine;
|
||||||
|
}
|
||||||
|
if (isOverVRAMUsage)
|
||||||
|
{
|
||||||
|
warningMessage += $"You exceed your own VRAM threshold by " +
|
||||||
|
$"{UiSharedService.ByteToString(actualVramUsage - (_playerPerformanceConfig.Current.VRAMSizeWarningThresholdMiB * 1024 * 1024))}.";
|
||||||
|
}
|
||||||
|
UiSharedService.AttachToolTip(warningMessage);
|
||||||
}
|
}
|
||||||
if (isOverVRAMUsage)
|
|
||||||
{
|
|
||||||
warningMessage += $"You exceed your own VRAM threshold by " +
|
|
||||||
$"{UiSharedService.ByteToString(actualVramUsage - (_playerPerformanceConfig.Current.VRAMSizeWarningThresholdMiB * 1024 * 1024))}.";
|
|
||||||
}
|
|
||||||
UiSharedService.AttachToolTip(warningMessage);
|
|
||||||
if (ImGui.IsItemClicked())
|
if (ImGui.IsItemClicked())
|
||||||
{
|
{
|
||||||
_lightlessMediator.Publish(new UiToggleMessage(typeof(DataAnalysisUi)));
|
_lightlessMediator.Publish(new UiToggleMessage(typeof(DataAnalysisUi)));
|
||||||
@@ -701,6 +640,34 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (uidTextHovered)
|
||||||
|
{
|
||||||
|
var padding = new Vector2(35f * ImGuiHelpers.GlobalScale);
|
||||||
|
Selune.RegisterHighlight(
|
||||||
|
uidTextRectMin - padding,
|
||||||
|
uidTextRectMin + uidTextRect + padding,
|
||||||
|
SeluneHighlightMode.Point,
|
||||||
|
exactSize: true,
|
||||||
|
clipToElement: true,
|
||||||
|
clipPadding: padding,
|
||||||
|
highlightColorOverride: vanityGlowColor,
|
||||||
|
highlightAlphaOverride: 0.05f);
|
||||||
|
|
||||||
|
ImGui.SetTooltip("Click to copy");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (headerItemClicked)
|
||||||
|
{
|
||||||
|
ImGui.SetClipboardText(uidText);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect/Disconnect button next to big UID (use screen pos to avoid affecting layout)
|
||||||
|
DrawConnectButton(uidTextRectMin.Y + textVerticalOffset, uidTextSize.Y);
|
||||||
|
|
||||||
|
// Add spacing below the big UID
|
||||||
|
ImGuiHelpers.ScaledDummy(5f);
|
||||||
|
|
||||||
if (_apiController.ServerState is ServerState.Connected)
|
if (_apiController.ServerState is ServerState.Connected)
|
||||||
{
|
{
|
||||||
if (headerItemClicked)
|
if (headerItemClicked)
|
||||||
@@ -708,10 +675,12 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
ImGui.SetClipboardText(_apiController.DisplayName);
|
ImGui.SetClipboardText(_apiController.DisplayName);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.Equals(_apiController.DisplayName, _apiController.UID, StringComparison.Ordinal))
|
// Only show smaller UID line if DisplayName differs from UID (custom vanity name)
|
||||||
|
bool hasCustomName = !string.Equals(_apiController.DisplayName, _apiController.UID, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
if (hasCustomName)
|
||||||
{
|
{
|
||||||
var origTextSize = ImGui.CalcTextSize(_apiController.UID);
|
ImGui.SetCursorPosX(uidStartX);
|
||||||
ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X) / 2 - (origTextSize.X / 2));
|
|
||||||
|
|
||||||
if (useVanityColors)
|
if (useVanityColors)
|
||||||
{
|
{
|
||||||
@@ -746,14 +715,88 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
ImGui.SetClipboardText(_apiController.UID);
|
ImGui.SetClipboardText(_apiController.UID);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Users Online on same line as smaller UID (with separator)
|
||||||
|
ImGui.SameLine();
|
||||||
|
ImGui.AlignTextToFramePadding();
|
||||||
|
ImGui.TextUnformatted("|");
|
||||||
|
ImGui.SameLine();
|
||||||
|
ImGui.TextColored(UIColors.Get("LightlessGreen"), _apiController.OnlineUsers.ToString(CultureInfo.InvariantCulture));
|
||||||
|
ImGui.SameLine();
|
||||||
|
ImGui.TextUnformatted("Users Online");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// No custom name - just show Users Online aligned to uidStartX
|
||||||
|
ImGui.SetCursorPosX(uidStartX);
|
||||||
|
ImGui.TextColored(UIColors.Get("LightlessGreen"), _apiController.OnlineUsers.ToString(CultureInfo.InvariantCulture));
|
||||||
|
ImGui.SameLine();
|
||||||
|
ImGui.TextUnformatted("Users Online");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
ImGui.SetCursorPosX(uidStartX);
|
||||||
UiSharedService.ColorTextWrapped(_apiController.ServerState.GetServerError(_apiController.AuthFailureMessage), uidColor);
|
UiSharedService.ColorTextWrapped(_apiController.ServerState.GetServerError(_apiController.AuthFailureMessage), uidColor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void DrawConnectButton(float screenY, float textHeight)
|
||||||
|
{
|
||||||
|
var buttonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Link);
|
||||||
|
bool isConnectingOrConnected = _apiController.ServerState is ServerState.Connected or ServerState.Connecting or ServerState.Reconnecting;
|
||||||
|
var color = UiSharedService.GetBoolColor(!isConnectingOrConnected);
|
||||||
|
var connectedIcon = isConnectingOrConnected ? FontAwesomeIcon.Unlink : FontAwesomeIcon.Link;
|
||||||
|
|
||||||
|
// Position on right side, vertically centered with text
|
||||||
|
if (_apiController.ServerState is not (ServerState.Reconnecting or ServerState.Disconnecting))
|
||||||
|
{
|
||||||
|
var windowPos = ImGui.GetWindowPos();
|
||||||
|
var screenX = windowPos.X + UiSharedService.GetWindowContentRegionWidth() - buttonSize.X - 13f;
|
||||||
|
var yOffset = (textHeight - buttonSize.Y) * 0.5f;
|
||||||
|
ImGui.SetCursorScreenPos(new Vector2(screenX, screenY + yOffset));
|
||||||
|
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.Text, color))
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.Button, ImGui.ColorConvertFloat4ToU32(new(0, 0, 0, 0))))
|
||||||
|
{
|
||||||
|
if (_uiSharedService.IconButton(connectedIcon, buttonSize.Y))
|
||||||
|
{
|
||||||
|
if (isConnectingOrConnected && !_serverManager.CurrentServer.FullPause)
|
||||||
|
{
|
||||||
|
_serverManager.CurrentServer.FullPause = true;
|
||||||
|
_serverManager.Save();
|
||||||
|
}
|
||||||
|
else if (!isConnectingOrConnected && _serverManager.CurrentServer.FullPause)
|
||||||
|
{
|
||||||
|
_serverManager.CurrentServer.FullPause = false;
|
||||||
|
_serverManager.Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = _apiController.CreateConnectionsAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem) || ImGui.IsItemActive())
|
||||||
|
{
|
||||||
|
Selune.RegisterHighlight(
|
||||||
|
ImGui.GetItemRectMin(),
|
||||||
|
ImGui.GetItemRectMax(),
|
||||||
|
SeluneHighlightMode.Both,
|
||||||
|
borderOnly: true,
|
||||||
|
borderThicknessOverride: ConnectButtonHighlightThickness,
|
||||||
|
exactSize: true,
|
||||||
|
clipToElement: true,
|
||||||
|
roundingOverride: ImGui.GetStyle().FrameRounding);
|
||||||
|
}
|
||||||
|
|
||||||
|
UiSharedService.AttachToolTip(isConnectingOrConnected ? "Disconnect from " + _serverManager.CurrentServer.ServerName : "Connect to " + _serverManager.CurrentServer.ServerName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Folder Building
|
||||||
|
|
||||||
private IEnumerable<IDrawFolder> DrawFolders
|
private IEnumerable<IDrawFolder> DrawFolders
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
@@ -889,6 +932,10 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Filtering & Sorting
|
||||||
|
|
||||||
private static 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;
|
||||||
@@ -1032,10 +1079,11 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
return SortGroupEntries(entries, group);
|
return SortGroupEntries(entries, group);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UiSharedService_GposeEnd()
|
#endregion
|
||||||
{
|
|
||||||
IsOpen = _wasOpen;
|
#region GPose Handlers
|
||||||
}
|
|
||||||
|
private void UiSharedService_GposeEnd() => IsOpen = _wasOpen;
|
||||||
|
|
||||||
private void UiSharedService_GposeStart()
|
private void UiSharedService_GposeStart()
|
||||||
{
|
{
|
||||||
@@ -1043,6 +1091,10 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
IsOpen = false;
|
IsOpen = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Focus Tracking
|
||||||
|
|
||||||
private void RegisterFocusCharacter(Pair pair)
|
private void RegisterFocusCharacter(Pair pair)
|
||||||
{
|
{
|
||||||
_pendingFocusPair = pair;
|
_pendingFocusPair = pair;
|
||||||
@@ -1088,4 +1140,16 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
_pendingFocusPair = null;
|
_pendingFocusPair = null;
|
||||||
_pendingFocusFrame = -1;
|
_pendingFocusFrame = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Helper Types
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Auto)]
|
||||||
|
private readonly record struct DownloadSummary(int TotalFiles, int TransferredFiles, long TransferredBytes, long TotalBytes)
|
||||||
|
{
|
||||||
|
public bool HasDownloads => TotalFiles > 0 || TotalBytes > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ public class DrawUserPair
|
|||||||
private readonly UiSharedService _uiSharedService;
|
private readonly UiSharedService _uiSharedService;
|
||||||
private readonly PlayerPerformanceConfigService _performanceConfigService;
|
private readonly PlayerPerformanceConfigService _performanceConfigService;
|
||||||
private readonly LightlessConfigService _configService;
|
private readonly LightlessConfigService _configService;
|
||||||
|
private readonly LocationShareService _locationShareService;
|
||||||
private readonly CharaDataManager _charaDataManager;
|
private readonly CharaDataManager _charaDataManager;
|
||||||
private readonly PairLedger _pairLedger;
|
private readonly PairLedger _pairLedger;
|
||||||
private float _menuWidth = -1;
|
private float _menuWidth = -1;
|
||||||
@@ -57,6 +58,7 @@ public class DrawUserPair
|
|||||||
UiSharedService uiSharedService,
|
UiSharedService uiSharedService,
|
||||||
PlayerPerformanceConfigService performanceConfigService,
|
PlayerPerformanceConfigService performanceConfigService,
|
||||||
LightlessConfigService configService,
|
LightlessConfigService configService,
|
||||||
|
LocationShareService locationShareService,
|
||||||
CharaDataManager charaDataManager,
|
CharaDataManager charaDataManager,
|
||||||
PairLedger pairLedger)
|
PairLedger pairLedger)
|
||||||
{
|
{
|
||||||
@@ -74,6 +76,7 @@ public class DrawUserPair
|
|||||||
_uiSharedService = uiSharedService;
|
_uiSharedService = uiSharedService;
|
||||||
_performanceConfigService = performanceConfigService;
|
_performanceConfigService = performanceConfigService;
|
||||||
_configService = configService;
|
_configService = configService;
|
||||||
|
_locationShareService = locationShareService;
|
||||||
_charaDataManager = charaDataManager;
|
_charaDataManager = charaDataManager;
|
||||||
_pairLedger = pairLedger;
|
_pairLedger = pairLedger;
|
||||||
}
|
}
|
||||||
@@ -216,6 +219,48 @@ public class DrawUserPair
|
|||||||
_ = _apiController.UserSetPairPermissions(new UserPermissionsDto(_pair.UserData, permissions));
|
_ = _apiController.UserSetPairPermissions(new UserPermissionsDto(_pair.UserData, permissions));
|
||||||
}
|
}
|
||||||
UiSharedService.AttachToolTip("Changes VFX sync permissions with this user." + (individual ? individualText : string.Empty));
|
UiSharedService.AttachToolTip("Changes VFX sync permissions with this user." + (individual ? individualText : string.Empty));
|
||||||
|
|
||||||
|
ImGui.SetCursorPosX(10f);
|
||||||
|
_uiSharedService.IconText(FontAwesomeIcon.Globe);
|
||||||
|
ImGui.SameLine();
|
||||||
|
if (ImGui.BeginMenu("Toggle Location sharing"))
|
||||||
|
{
|
||||||
|
if (ImGui.MenuItem("Share for 30 Mins"))
|
||||||
|
{
|
||||||
|
_ = ToggleLocationSharing([_pair.UserData.UID], DateTimeOffset.UtcNow.AddMinutes(30));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui.MenuItem("Share for 1 Hour"))
|
||||||
|
{
|
||||||
|
_ = ToggleLocationSharing([_pair.UserData.UID], DateTimeOffset.UtcNow.AddHours(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui.MenuItem("Share for 3 Hours"))
|
||||||
|
{
|
||||||
|
_ = ToggleLocationSharing([_pair.UserData.UID], DateTimeOffset.UtcNow.AddHours(3));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui.MenuItem("Share until manually stop"))
|
||||||
|
{
|
||||||
|
_ = ToggleLocationSharing([_pair.UserData.UID], DateTimeOffset.MaxValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.Separator();
|
||||||
|
if (ImGui.MenuItem("Stop Sharing"))
|
||||||
|
{
|
||||||
|
_ = ToggleLocationSharing([_pair.UserData.UID], DateTimeOffset.MinValue);
|
||||||
|
}
|
||||||
|
ImGui.EndMenu();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ToggleLocationSharing(List<string> users, DateTimeOffset expireAt)
|
||||||
|
{
|
||||||
|
var updated = await _apiController.ToggleLocationSharing(new LocationSharingToggleDto(users, expireAt)).ConfigureAwait(false);
|
||||||
|
if (updated)
|
||||||
|
{
|
||||||
|
_locationShareService.UpdateSharingStatus(users, expireAt);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawIndividualMenu()
|
private void DrawIndividualMenu()
|
||||||
@@ -574,6 +619,71 @@ public class DrawUserPair
|
|||||||
var individualVFXDisabled = (_pair.UserPair?.OwnPermissions.IsDisableVFX() ?? false) || (_pair.UserPair?.OtherPermissions.IsDisableVFX() ?? false);
|
var individualVFXDisabled = (_pair.UserPair?.OwnPermissions.IsDisableVFX() ?? false) || (_pair.UserPair?.OtherPermissions.IsDisableVFX() ?? false);
|
||||||
var individualIsSticky = _pair.UserPair!.OwnPermissions.IsSticky();
|
var individualIsSticky = _pair.UserPair!.OwnPermissions.IsSticky();
|
||||||
var individualIcon = individualIsSticky ? FontAwesomeIcon.ArrowCircleUp : FontAwesomeIcon.InfoCircle;
|
var individualIcon = individualIsSticky ? FontAwesomeIcon.ArrowCircleUp : FontAwesomeIcon.InfoCircle;
|
||||||
|
|
||||||
|
var shareLocationIcon = FontAwesomeIcon.Globe;
|
||||||
|
var location = _locationShareService.GetUserLocation(_pair.UserPair!.User.UID);
|
||||||
|
var shareLocation = !string.IsNullOrEmpty(location);
|
||||||
|
var expireAt = _locationShareService.GetSharingStatus(_pair.UserPair!.User.UID);
|
||||||
|
var shareLocationToOther = expireAt > DateTimeOffset.UtcNow;
|
||||||
|
var shareColor = shareLocation switch
|
||||||
|
{
|
||||||
|
true when shareLocationToOther => UIColors.Get("LightlessGreen"),
|
||||||
|
true when !shareLocationToOther => UIColors.Get("LightlessBlue"),
|
||||||
|
_ => UIColors.Get("LightlessYellow"),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (shareLocation || shareLocationToOther)
|
||||||
|
{
|
||||||
|
currentRightSide -= (_uiSharedService.GetIconSize(shareLocationIcon).X + spacingX);
|
||||||
|
ImGui.SameLine(currentRightSide);
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.Text, shareColor, shareLocation || shareLocationToOther))
|
||||||
|
_uiSharedService.IconText(shareLocationIcon);
|
||||||
|
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
{
|
||||||
|
ImGui.BeginTooltip();
|
||||||
|
|
||||||
|
if (_pair.IsOnline)
|
||||||
|
{
|
||||||
|
if (shareLocation)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(location))
|
||||||
|
{
|
||||||
|
_uiSharedService.IconText(FontAwesomeIcon.LocationArrow);
|
||||||
|
ImGui.SameLine();
|
||||||
|
ImGui.TextUnformatted(location);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ImGui.TextUnformatted("Location info not updated, reconnect or wait for update.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ImGui.TextUnformatted("NOT Sharing location with you. o(TヘTo)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ImGui.TextUnformatted("User not online. (´・ω・`)?");
|
||||||
|
}
|
||||||
|
ImGui.Separator();
|
||||||
|
|
||||||
|
if (shareLocationToOther)
|
||||||
|
{
|
||||||
|
ImGui.TextUnformatted("Sharing your location. ヾ(•ω•`)o");
|
||||||
|
if (expireAt != DateTimeOffset.MaxValue)
|
||||||
|
{
|
||||||
|
ImGui.TextUnformatted("Expires at " + expireAt.ToLocalTime().ToString("g"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ImGui.TextUnformatted("NOT sharing your location.  ̄へ ̄");
|
||||||
|
}
|
||||||
|
ImGui.EndTooltip();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (individualAnimDisabled || individualSoundsDisabled || individualVFXDisabled || individualIsSticky)
|
if (individualAnimDisabled || individualSoundsDisabled || individualVFXDisabled || individualIsSticky)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2183,7 +2183,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
|||||||
bool toggleClicked = false;
|
bool toggleClicked = false;
|
||||||
if (showToggle)
|
if (showToggle)
|
||||||
{
|
{
|
||||||
var icon = isCollapsed ? FontAwesomeIcon.ChevronRight : FontAwesomeIcon.ChevronLeft;
|
var icon = !isCollapsed ? FontAwesomeIcon.ChevronRight : FontAwesomeIcon.ChevronLeft;
|
||||||
Vector2 iconSize;
|
Vector2 iconSize;
|
||||||
using (_uiSharedService.IconFont.Push())
|
using (_uiSharedService.IconFont.Push())
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ public class DrawEntityFactory
|
|||||||
private readonly LightlessConfigService _configService;
|
private readonly LightlessConfigService _configService;
|
||||||
private readonly UiSharedService _uiSharedService;
|
private readonly UiSharedService _uiSharedService;
|
||||||
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
|
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
|
||||||
|
private readonly LocationShareService _locationShareService;
|
||||||
private readonly CharaDataManager _charaDataManager;
|
private readonly CharaDataManager _charaDataManager;
|
||||||
private readonly SelectTagForPairUi _selectTagForPairUi;
|
private readonly SelectTagForPairUi _selectTagForPairUi;
|
||||||
private readonly RenamePairTagUi _renamePairTagUi;
|
private readonly RenamePairTagUi _renamePairTagUi;
|
||||||
@@ -53,6 +54,7 @@ public class DrawEntityFactory
|
|||||||
LightlessConfigService configService,
|
LightlessConfigService configService,
|
||||||
UiSharedService uiSharedService,
|
UiSharedService uiSharedService,
|
||||||
PlayerPerformanceConfigService playerPerformanceConfigService,
|
PlayerPerformanceConfigService playerPerformanceConfigService,
|
||||||
|
LocationShareService locationShareService,
|
||||||
CharaDataManager charaDataManager,
|
CharaDataManager charaDataManager,
|
||||||
SelectTagForSyncshellUi selectTagForSyncshellUi,
|
SelectTagForSyncshellUi selectTagForSyncshellUi,
|
||||||
RenameSyncshellTagUi renameSyncshellTagUi,
|
RenameSyncshellTagUi renameSyncshellTagUi,
|
||||||
@@ -72,6 +74,7 @@ public class DrawEntityFactory
|
|||||||
_configService = configService;
|
_configService = configService;
|
||||||
_uiSharedService = uiSharedService;
|
_uiSharedService = uiSharedService;
|
||||||
_playerPerformanceConfigService = playerPerformanceConfigService;
|
_playerPerformanceConfigService = playerPerformanceConfigService;
|
||||||
|
_locationShareService = locationShareService;
|
||||||
_charaDataManager = charaDataManager;
|
_charaDataManager = charaDataManager;
|
||||||
_selectTagForSyncshellUi = selectTagForSyncshellUi;
|
_selectTagForSyncshellUi = selectTagForSyncshellUi;
|
||||||
_renameSyncshellTagUi = renameSyncshellTagUi;
|
_renameSyncshellTagUi = renameSyncshellTagUi;
|
||||||
@@ -162,6 +165,7 @@ public class DrawEntityFactory
|
|||||||
_uiSharedService,
|
_uiSharedService,
|
||||||
_playerPerformanceConfigService,
|
_playerPerformanceConfigService,
|
||||||
_configService,
|
_configService,
|
||||||
|
_locationShareService,
|
||||||
_charaDataManager,
|
_charaDataManager,
|
||||||
_pairLedger);
|
_pairLedger);
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -69,6 +69,8 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
private readonly UiSharedService _uiShared;
|
private readonly UiSharedService _uiShared;
|
||||||
private readonly IProgress<(int, int, FileCacheEntity)> _validationProgress;
|
private readonly IProgress<(int, int, FileCacheEntity)> _validationProgress;
|
||||||
private readonly NameplateService _nameplateService;
|
private readonly NameplateService _nameplateService;
|
||||||
|
private readonly AnimatedHeader _animatedHeader = new();
|
||||||
|
|
||||||
private (int, int, FileCacheEntity) _currentProgress;
|
private (int, int, FileCacheEntity) _currentProgress;
|
||||||
private bool _deleteAccountPopupModalShown = false;
|
private bool _deleteAccountPopupModalShown = false;
|
||||||
private bool _deleteFilesPopupModalShown = false;
|
private bool _deleteFilesPopupModalShown = false;
|
||||||
@@ -205,7 +207,10 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
_nameplateService = nameplateService;
|
_nameplateService = nameplateService;
|
||||||
_actorObjectService = actorObjectService;
|
_actorObjectService = actorObjectService;
|
||||||
_validationProgress = new Progress<(int, int, FileCacheEntity)>(v => _currentProgress = v);
|
_validationProgress = new Progress<(int, int, FileCacheEntity)>(v => _currentProgress = v);
|
||||||
|
_animatedHeader.Height = 120f;
|
||||||
|
_animatedHeader.EnableBottomGradient = true;
|
||||||
|
_animatedHeader.GradientHeight = 250f;
|
||||||
|
_animatedHeader.EnableParticles = _configService.Current.EnableParticleEffects;
|
||||||
WindowBuilder.For(this)
|
WindowBuilder.For(this)
|
||||||
.AllowPinning(true)
|
.AllowPinning(true)
|
||||||
.AllowClickthrough(false)
|
.AllowClickthrough(false)
|
||||||
@@ -241,6 +246,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
public override void OnClose()
|
public override void OnClose()
|
||||||
{
|
{
|
||||||
|
_animatedHeader.ClearParticles();
|
||||||
_uiShared.EditTrackerPosition = false;
|
_uiShared.EditTrackerPosition = false;
|
||||||
_uidToAddForIgnore = string.Empty;
|
_uidToAddForIgnore = string.Empty;
|
||||||
_secretKeysConversionCts = _secretKeysConversionCts.CancelRecreate();
|
_secretKeysConversionCts = _secretKeysConversionCts.CancelRecreate();
|
||||||
@@ -255,8 +261,8 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
protected override void DrawInternal()
|
protected override void DrawInternal()
|
||||||
{
|
{
|
||||||
|
_animatedHeader.Draw(ImGui.GetContentRegionAvail().X, (_, _) => { });
|
||||||
_ = _uiShared.DrawOtherPluginState();
|
_ = _uiShared.DrawOtherPluginState();
|
||||||
|
|
||||||
DrawSettingsContent();
|
DrawSettingsContent();
|
||||||
}
|
}
|
||||||
private static Vector3 PackedColorToVector3(uint color)
|
private static Vector3 PackedColorToVector3(uint color)
|
||||||
@@ -2089,7 +2095,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
var openPopupOnAddition = _configService.Current.OpenPopupOnAdd;
|
var openPopupOnAddition = _configService.Current.OpenPopupOnAdd;
|
||||||
|
|
||||||
using (var popupTree = BeginGeneralTree("Popup & Auto Fill", UIColors.Get("LightlessPurple")))
|
using (var popupTree = BeginGeneralTree("Popup & Auto Fill", UIColors.Get("LightlessPurple")))
|
||||||
{
|
{
|
||||||
if (popupTree.Visible)
|
if (popupTree.Visible)
|
||||||
@@ -2146,11 +2152,20 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
var groupInVisible = _configService.Current.ShowSyncshellUsersInVisible;
|
var groupInVisible = _configService.Current.ShowSyncshellUsersInVisible;
|
||||||
var syncshellOfflineSeparate = _configService.Current.ShowSyncshellOfflineUsersSeparately;
|
var syncshellOfflineSeparate = _configService.Current.ShowSyncshellOfflineUsersSeparately;
|
||||||
var greenVisiblePair = _configService.Current.ShowVisiblePairsGreenEye;
|
var greenVisiblePair = _configService.Current.ShowVisiblePairsGreenEye;
|
||||||
|
var enableParticleEffects = _configService.Current.EnableParticleEffects;
|
||||||
|
|
||||||
using (var behaviorTree = BeginGeneralTree("Behavior", UIColors.Get("LightlessPurple")))
|
using (var behaviorTree = BeginGeneralTree("Behavior", UIColors.Get("LightlessPurple")))
|
||||||
{
|
{
|
||||||
if (behaviorTree.Visible)
|
if (behaviorTree.Visible)
|
||||||
{
|
{
|
||||||
|
if (ImGui.Checkbox("Enable Particle Effects", ref enableParticleEffects))
|
||||||
|
{
|
||||||
|
_configService.Current.EnableParticleEffects = enableParticleEffects;
|
||||||
|
_configService.Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
_uiShared.DrawHelpText("This will enable particle effects in the UI.");
|
||||||
|
|
||||||
if (ImGui.Checkbox("Enable Game Right Click Menu Entries", ref enableRightClickMenu))
|
if (ImGui.Checkbox("Enable Game Right Click Menu Entries", ref enableRightClickMenu))
|
||||||
{
|
{
|
||||||
_configService.Current.EnableRightClickMenus = enableRightClickMenu;
|
_configService.Current.EnableRightClickMenus = enableRightClickMenu;
|
||||||
@@ -2859,16 +2874,21 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
var colorNames = new[]
|
var colorNames = new[]
|
||||||
{
|
{
|
||||||
("LightlessPurple", "Primary Purple", "Section titles and dividers"),
|
("LightlessPurple", "Primary Purple", "Section titles and dividers"),
|
||||||
("LightlessPurpleActive", "Primary Purple (Active)", "Active tabs and hover highlights"),
|
("LightlessPurpleActive", "Primary Purple (Active)", "Active tabs and hover highlights"),
|
||||||
("LightlessPurpleDefault", "Primary Purple (Inactive)", "Inactive tabs and default dividers"),
|
("LightlessPurpleDefault", "Primary Purple (Inactive)", "Inactive tabs and default dividers"),
|
||||||
("LightlessBlue", "Secondary Blue", "Secondary title colors, visable pairs"),
|
("LightlessBlue", "Secondary Blue", "Secondary title colors, visable pairs"),
|
||||||
("LightlessGreen", "Success Green", "Join buttons and success messages"),
|
("LightlessGreen", "Success Green", "Join buttons and success messages"),
|
||||||
("LightlessYellow", "Warning Yellow", "Warning colors"),
|
("LightlessYellow", "Warning Yellow", "Warning colors"),
|
||||||
("LightlessOrange", "Performance Orange", "Performance notifications and warnings"),
|
("LightlessOrange", "Performance Orange", "Performance notifications and warnings"),
|
||||||
("PairBlue", "Syncshell Blue", "Syncshell headers, toggle highlights, and moderator actions"),
|
("PairBlue", "Syncshell Blue", "Syncshell headers, toggle highlights, and moderator actions"),
|
||||||
("DimRed", "Error Red", "Error and offline colors")
|
("DimRed", "Error Red", "Error and offline colors"),
|
||||||
};
|
("HeaderGradientTop", "Header Gradient (Top)", "Top color of the animated header background"),
|
||||||
|
("HeaderGradientBottom", "Header Gradient (Bottom)", "Bottom color of the animated header background"),
|
||||||
|
("HeaderStaticStar", "Header Stars", "Tint color for the static background stars in the header"),
|
||||||
|
("HeaderShootingStar", "Header Shooting Star", "Tint color for the shooting star effect"),
|
||||||
|
};
|
||||||
|
|
||||||
if (ImGui.BeginTable("##ColorTable", 3,
|
if (ImGui.BeginTable("##ColorTable", 3,
|
||||||
ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit))
|
ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -43,10 +43,23 @@ public class AnimatedHeader
|
|||||||
private const float _extendedParticleHeight = 40f;
|
private const float _extendedParticleHeight = 40f;
|
||||||
|
|
||||||
public float Height { get; set; } = 150f;
|
public float Height { get; set; } = 150f;
|
||||||
|
|
||||||
|
// Color keys for theming
|
||||||
|
public string? TopColorKey { get; set; } = "HeaderGradientTop";
|
||||||
|
public string? BottomColorKey { get; set; } = "HeaderGradientBottom";
|
||||||
|
public string? StaticStarColorKey { get; set; } = "HeaderStaticStar";
|
||||||
|
public string? ShootingStarColorKey { get; set; } = "HeaderShootingStar";
|
||||||
|
|
||||||
|
// Fallbacks if the color keys are not found
|
||||||
public Vector4 TopColor { get; set; } = new(0.08f, 0.05f, 0.15f, 1.0f);
|
public Vector4 TopColor { get; set; } = new(0.08f, 0.05f, 0.15f, 1.0f);
|
||||||
public Vector4 BottomColor { get; set; } = new(0.12f, 0.08f, 0.20f, 1.0f);
|
public Vector4 BottomColor { get; set; } = new(0.12f, 0.08f, 0.20f, 1.0f);
|
||||||
|
public Vector4 StaticStarColor { get; set; } = new(1f, 1f, 1f, 1f);
|
||||||
|
public Vector4 ShootingStarColor { get; set; } = new(0.4f, 0.8f, 1.0f, 1.0f);
|
||||||
|
|
||||||
public bool EnableParticles { get; set; } = true;
|
public bool EnableParticles { get; set; } = true;
|
||||||
public bool EnableBottomGradient { get; set; } = true;
|
public bool EnableBottomGradient { get; set; } = true;
|
||||||
|
|
||||||
|
public float GradientHeight { get; set; } = 60f;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Draws the animated header with some customizable content
|
/// Draws the animated header with some customizable content
|
||||||
@@ -146,16 +159,21 @@ public class AnimatedHeader
|
|||||||
{
|
{
|
||||||
var drawList = ImGui.GetWindowDrawList();
|
var drawList = ImGui.GetWindowDrawList();
|
||||||
|
|
||||||
|
var top = ResolveColor(TopColorKey, TopColor);
|
||||||
|
var bottom = ResolveColor(BottomColorKey, BottomColor);
|
||||||
|
|
||||||
drawList.AddRectFilledMultiColor(
|
drawList.AddRectFilledMultiColor(
|
||||||
headerStart,
|
headerStart,
|
||||||
headerEnd,
|
headerEnd,
|
||||||
ImGui.GetColorU32(TopColor),
|
ImGui.GetColorU32(top),
|
||||||
ImGui.GetColorU32(TopColor),
|
ImGui.GetColorU32(top),
|
||||||
ImGui.GetColorU32(BottomColor),
|
ImGui.GetColorU32(bottom),
|
||||||
ImGui.GetColorU32(BottomColor)
|
ImGui.GetColorU32(bottom)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Draw static background stars
|
// Draw static background stars
|
||||||
|
var starBase = ResolveColor(StaticStarColorKey, StaticStarColor);
|
||||||
|
|
||||||
var random = new Random(42);
|
var random = new Random(42);
|
||||||
for (int i = 0; i < 50; i++)
|
for (int i = 0; i < 50; i++)
|
||||||
{
|
{
|
||||||
@@ -164,23 +182,28 @@ public class AnimatedHeader
|
|||||||
(float)random.NextDouble() * (headerEnd.Y - headerStart.Y)
|
(float)random.NextDouble() * (headerEnd.Y - headerStart.Y)
|
||||||
);
|
);
|
||||||
var brightness = 0.3f + (float)random.NextDouble() * 0.4f;
|
var brightness = 0.3f + (float)random.NextDouble() * 0.4f;
|
||||||
drawList.AddCircleFilled(starPos, 1f, ImGui.GetColorU32(new Vector4(1f, 1f, 1f, brightness)));
|
var starColor = starBase with { W = starBase.W * brightness };
|
||||||
|
|
||||||
|
drawList.AddCircleFilled(starPos, 1f, ImGui.GetColorU32(starColor));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawBottomGradient(Vector2 headerStart, Vector2 headerEnd, float width)
|
private void DrawBottomGradient(Vector2 headerStart, Vector2 headerEnd, float width)
|
||||||
{
|
{
|
||||||
var drawList = ImGui.GetWindowDrawList();
|
var drawList = ImGui.GetWindowDrawList();
|
||||||
var gradientHeight = 60f;
|
var gradientHeight = GradientHeight;
|
||||||
|
var bottom = ResolveColor(BottomColorKey, BottomColor);
|
||||||
|
|
||||||
for (int i = 0; i < gradientHeight; i++)
|
for (int i = 0; i < gradientHeight; i++)
|
||||||
{
|
{
|
||||||
var progress = i / gradientHeight;
|
var progress = i / gradientHeight;
|
||||||
var smoothProgress = progress * progress;
|
var smoothProgress = progress * progress;
|
||||||
var r = BottomColor.X + (0.0f - BottomColor.X) * smoothProgress;
|
|
||||||
var g = BottomColor.Y + (0.0f - BottomColor.Y) * smoothProgress;
|
var r = bottom.X + (0.0f - bottom.X) * smoothProgress;
|
||||||
var b = BottomColor.Z + (0.0f - BottomColor.Z) * smoothProgress;
|
var g = bottom.Y + (0.0f - bottom.Y) * smoothProgress;
|
||||||
|
var b = bottom.Z + (0.0f - bottom.Z) * smoothProgress;
|
||||||
var alpha = 1f - smoothProgress;
|
var alpha = 1f - smoothProgress;
|
||||||
|
|
||||||
var gradientColor = new Vector4(r, g, b, alpha);
|
var gradientColor = new Vector4(r, g, b, alpha);
|
||||||
drawList.AddLine(
|
drawList.AddLine(
|
||||||
new Vector2(headerStart.X, headerEnd.Y + i),
|
new Vector2(headerStart.X, headerEnd.Y + i),
|
||||||
@@ -308,9 +331,11 @@ public class AnimatedHeader
|
|||||||
? baseAlpha * (0.6f + 0.4f * MathF.Sin(particle.Twinkle))
|
? baseAlpha * (0.6f + 0.4f * MathF.Sin(particle.Twinkle))
|
||||||
: baseAlpha;
|
: baseAlpha;
|
||||||
|
|
||||||
|
var shootingBase = ResolveColor(ShootingStarColorKey, ShootingStarColor);
|
||||||
|
|
||||||
if (particle.Type == ParticleType.ShootingStar && particle.Trail != null && particle.Trail.Count > 1)
|
if (particle.Type == ParticleType.ShootingStar && particle.Trail != null && particle.Trail.Count > 1)
|
||||||
{
|
{
|
||||||
var cyanColor = new Vector4(0.4f, 0.8f, 1.0f, 1.0f);
|
var baseColor = shootingBase;
|
||||||
|
|
||||||
for (int t = 1; t < particle.Trail.Count; t++)
|
for (int t = 1; t < particle.Trail.Count; t++)
|
||||||
{
|
{
|
||||||
@@ -319,17 +344,18 @@ public class AnimatedHeader
|
|||||||
var trailWidth = (1f - trailProgress) * 3f + 1f;
|
var trailWidth = (1f - trailProgress) * 3f + 1f;
|
||||||
|
|
||||||
var glowAlpha = trailAlpha * 0.4f;
|
var glowAlpha = trailAlpha * 0.4f;
|
||||||
|
|
||||||
drawList.AddLine(
|
drawList.AddLine(
|
||||||
bannerStart + particle.Trail[t - 1],
|
bannerStart + particle.Trail[t - 1],
|
||||||
bannerStart + particle.Trail[t],
|
bannerStart + particle.Trail[t],
|
||||||
ImGui.GetColorU32(cyanColor with { W = glowAlpha }),
|
ImGui.GetColorU32(baseColor with { W = glowAlpha }),
|
||||||
trailWidth + 4f
|
trailWidth + 4f
|
||||||
);
|
);
|
||||||
|
|
||||||
drawList.AddLine(
|
drawList.AddLine(
|
||||||
bannerStart + particle.Trail[t - 1],
|
bannerStart + particle.Trail[t - 1],
|
||||||
bannerStart + particle.Trail[t],
|
bannerStart + particle.Trail[t],
|
||||||
ImGui.GetColorU32(cyanColor with { W = trailAlpha }),
|
ImGui.GetColorU32(baseColor with { W = trailAlpha }),
|
||||||
trailWidth
|
trailWidth
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -448,6 +474,13 @@ public class AnimatedHeader
|
|||||||
Hue = 270f
|
Hue = 270f
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
private static Vector4 ResolveColor(string? key, Vector4 fallback)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(key))
|
||||||
|
return fallback;
|
||||||
|
|
||||||
|
return UIColors.Get(key);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Clears all active particles. Useful when closing or hiding a window with an animated header.
|
/// Clears all active particles. Useful when closing or hiding a window with an animated header.
|
||||||
|
|||||||
@@ -40,9 +40,10 @@ internal static class MainStyle
|
|||||||
new("color.frameBg", "Frame Background", () => Rgba(40, 40, 40, 255), ImGuiCol.FrameBg),
|
new("color.frameBg", "Frame Background", () => Rgba(40, 40, 40, 255), ImGuiCol.FrameBg),
|
||||||
new("color.frameBgHovered", "Frame Background (Hover)", () => Rgba(50, 50, 50, 100), ImGuiCol.FrameBgHovered),
|
new("color.frameBgHovered", "Frame Background (Hover)", () => Rgba(50, 50, 50, 100), ImGuiCol.FrameBgHovered),
|
||||||
new("color.frameBgActive", "Frame Background (Active)", () => Rgba(30, 30, 30, 255), ImGuiCol.FrameBgActive),
|
new("color.frameBgActive", "Frame Background (Active)", () => Rgba(30, 30, 30, 255), ImGuiCol.FrameBgActive),
|
||||||
new("color.titleBg", "Title Background", () => Rgba(24, 24, 24, 232), ImGuiCol.TitleBg),
|
new("color.titleBg", "Title Background", () => Rgba(22, 14, 41, 255), ImGuiCol.TitleBg),
|
||||||
new("color.titleBgActive", "Title Background (Active)", () => Rgba(30, 30, 30, 255), ImGuiCol.TitleBgActive),
|
new("color.titleBgActive", "Title Background (Active)", () => Rgba(22, 14, 41, 255), ImGuiCol.TitleBgActive),
|
||||||
new("color.titleBgCollapsed", "Title Background (Collapsed)", () => Rgba(27, 27, 27, 255), ImGuiCol.TitleBgCollapsed),
|
new("color.titleBgCollapsed", "Title Background (Collapsed)", () => Rgba(22, 14, 41, 255), ImGuiCol.TitleBgCollapsed),
|
||||||
|
|
||||||
new("color.menuBarBg", "Menu Bar Background", () => Rgba(36, 36, 36, 255), ImGuiCol.MenuBarBg),
|
new("color.menuBarBg", "Menu Bar Background", () => Rgba(36, 36, 36, 255), ImGuiCol.MenuBarBg),
|
||||||
new("color.scrollbarBg", "Scrollbar Background", () => Rgba(0, 0, 0, 0), ImGuiCol.ScrollbarBg),
|
new("color.scrollbarBg", "Scrollbar Background", () => Rgba(0, 0, 0, 0), ImGuiCol.ScrollbarBg),
|
||||||
new("color.scrollbarGrab", "Scrollbar Grab", () => Rgba(62, 62, 62, 255), ImGuiCol.ScrollbarGrab),
|
new("color.scrollbarGrab", "Scrollbar Grab", () => Rgba(62, 62, 62, 255), ImGuiCol.ScrollbarGrab),
|
||||||
|
|||||||
@@ -1,855 +0,0 @@
|
|||||||
using Dalamud.Bindings.ImGui;
|
|
||||||
using Dalamud.Interface;
|
|
||||||
using Dalamud.Interface.Colors;
|
|
||||||
using Dalamud.Interface.Textures.TextureWraps;
|
|
||||||
using Dalamud.Interface.Utility;
|
|
||||||
using Dalamud.Interface.Utility.Raii;
|
|
||||||
using Dalamud.Plugin.Services;
|
|
||||||
using LightlessSync.API.Data;
|
|
||||||
using LightlessSync.API.Data.Enum;
|
|
||||||
using LightlessSync.API.Data.Extensions;
|
|
||||||
using LightlessSync.API.Dto;
|
|
||||||
using LightlessSync.API.Dto.Group;
|
|
||||||
using LightlessSync.Services;
|
|
||||||
using LightlessSync.Services.LightFinder;
|
|
||||||
using LightlessSync.Services.Mediator;
|
|
||||||
using LightlessSync.UI.Services;
|
|
||||||
using LightlessSync.UI.Tags;
|
|
||||||
using LightlessSync.Utils;
|
|
||||||
using LightlessSync.WebAPI;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using System.Numerics;
|
|
||||||
|
|
||||||
namespace LightlessSync.UI;
|
|
||||||
|
|
||||||
public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
|
||||||
{
|
|
||||||
private readonly ApiController _apiController;
|
|
||||||
private readonly LightFinderService _broadcastService;
|
|
||||||
private readonly UiSharedService _uiSharedService;
|
|
||||||
private readonly LightFinderScannerService _broadcastScannerService;
|
|
||||||
private readonly PairUiService _pairUiService;
|
|
||||||
private readonly DalamudUtilService _dalamudUtilService;
|
|
||||||
|
|
||||||
private Vector4 _tagBackgroundColor = new(0.18f, 0.18f, 0.18f, 0.95f);
|
|
||||||
private Vector4 _tagBorderColor = new(0.35f, 0.35f, 0.35f, 0.4f);
|
|
||||||
|
|
||||||
private readonly List<SeStringUtils.SeStringSegment> _seResolvedSegments = new();
|
|
||||||
private readonly List<GroupJoinDto> _nearbySyncshells = [];
|
|
||||||
private List<GroupFullInfoDto> _currentSyncshells = [];
|
|
||||||
private int _selectedNearbyIndex = -1;
|
|
||||||
private int _syncshellPageIndex = 0;
|
|
||||||
private readonly HashSet<string> _recentlyJoined = new(StringComparer.Ordinal);
|
|
||||||
|
|
||||||
private GroupJoinDto? _joinDto;
|
|
||||||
private GroupJoinInfoDto? _joinInfo;
|
|
||||||
private DefaultPermissionsDto _ownPermissions = null!;
|
|
||||||
private bool _useTestSyncshells = false;
|
|
||||||
|
|
||||||
private bool _compactView = false;
|
|
||||||
private readonly LightlessProfileManager _lightlessProfileManager;
|
|
||||||
|
|
||||||
public SyncshellFinderUI(
|
|
||||||
ILogger<SyncshellFinderUI> logger,
|
|
||||||
LightlessMediator mediator,
|
|
||||||
PerformanceCollectorService performanceCollectorService,
|
|
||||||
LightFinderService broadcastService,
|
|
||||||
UiSharedService uiShared,
|
|
||||||
ApiController apiController,
|
|
||||||
LightFinderScannerService broadcastScannerService,
|
|
||||||
PairUiService pairUiService,
|
|
||||||
DalamudUtilService dalamudUtilService,
|
|
||||||
LightlessProfileManager lightlessProfileManager) : base(logger, mediator, "Shellfinder###LightlessSyncshellFinderUI", performanceCollectorService)
|
|
||||||
{
|
|
||||||
_broadcastService = broadcastService;
|
|
||||||
_uiSharedService = uiShared;
|
|
||||||
_apiController = apiController;
|
|
||||||
_broadcastScannerService = broadcastScannerService;
|
|
||||||
_pairUiService = pairUiService;
|
|
||||||
_dalamudUtilService = dalamudUtilService;
|
|
||||||
_lightlessProfileManager = lightlessProfileManager;
|
|
||||||
|
|
||||||
IsOpen = false;
|
|
||||||
WindowBuilder.For(this)
|
|
||||||
.SetSizeConstraints(new Vector2(600, 400), new Vector2(600, 550))
|
|
||||||
.Apply();
|
|
||||||
|
|
||||||
Mediator.Subscribe<SyncshellBroadcastsUpdatedMessage>(this, async _ => await RefreshSyncshellsAsync().ConfigureAwait(false));
|
|
||||||
Mediator.Subscribe<BroadcastStatusChangedMessage>(this, async _ => await RefreshSyncshellsAsync().ConfigureAwait(false));
|
|
||||||
Mediator.Subscribe<UserLeftSyncshell>(this, async _ => await RefreshSyncshellsAsync(_.gid).ConfigureAwait(false));
|
|
||||||
Mediator.Subscribe<UserJoinedSyncshell>(this, async _ => await RefreshSyncshellsAsync(_.gid).ConfigureAwait(false));
|
|
||||||
}
|
|
||||||
|
|
||||||
public override async void OnOpen()
|
|
||||||
{
|
|
||||||
_ownPermissions = _apiController.DefaultPermissions.DeepClone()!;
|
|
||||||
await RefreshSyncshellsAsync().ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void DrawInternal()
|
|
||||||
{
|
|
||||||
ImGui.BeginGroup();
|
|
||||||
_uiSharedService.MediumText("Nearby Syncshells", UIColors.Get("LightlessPurple"));
|
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
if (ImGui.SmallButton("Show test syncshells"))
|
|
||||||
{
|
|
||||||
_useTestSyncshells = !_useTestSyncshells;
|
|
||||||
_ = Task.Run(async () => await RefreshSyncshellsAsync().ConfigureAwait(false));
|
|
||||||
}
|
|
||||||
ImGui.SameLine();
|
|
||||||
#endif
|
|
||||||
|
|
||||||
string checkboxLabel = "Compact view";
|
|
||||||
float availWidth = ImGui.GetContentRegionAvail().X;
|
|
||||||
float checkboxWidth = ImGui.CalcTextSize(checkboxLabel).X + ImGui.GetFrameHeight();
|
|
||||||
|
|
||||||
float rightX = ImGui.GetCursorPosX() + availWidth - checkboxWidth - 4.0f;
|
|
||||||
ImGui.SetCursorPosX(rightX);
|
|
||||||
ImGui.Checkbox(checkboxLabel, ref _compactView);
|
|
||||||
ImGui.EndGroup();
|
|
||||||
|
|
||||||
UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"));
|
|
||||||
ImGui.Dummy(new Vector2(0, 2 * ImGuiHelpers.GlobalScale));
|
|
||||||
if (_nearbySyncshells.Count == 0)
|
|
||||||
{
|
|
||||||
ImGui.TextColored(ImGuiColors.DalamudGrey, "No nearby syncshells are being broadcasted.");
|
|
||||||
|
|
||||||
if (!_broadcastService.IsBroadcasting)
|
|
||||||
{
|
|
||||||
|
|
||||||
UiSharedService.ColoredSeparator(UIColors.Get("LightlessYellow"));
|
|
||||||
|
|
||||||
ImGui.TextColored(UIColors.Get("LightlessYellow"), "Lightfinder is currently disabled, to locate nearby syncshells, Lightfinder must be active.");
|
|
||||||
ImGuiHelpers.ScaledDummy(0.5f);
|
|
||||||
|
|
||||||
ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 10.0f);
|
|
||||||
ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("LightlessPurple"));
|
|
||||||
|
|
||||||
if (ImGui.Button("Open Lightfinder", new Vector2(200 * ImGuiHelpers.GlobalScale, 0)))
|
|
||||||
{
|
|
||||||
Mediator.Publish(new UiToggleMessage(typeof(LightFinderUI)));
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.PopStyleColor();
|
|
||||||
ImGui.PopStyleVar();
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var broadcasts = _broadcastScannerService.GetActiveSyncshellBroadcasts().ToList() ?? [];
|
|
||||||
_broadcastScannerService.TryGetLocalHashedCid(out var localHashedCid);
|
|
||||||
|
|
||||||
var cardData = new List<(GroupJoinDto Shell, string BroadcasterName, bool IsSelfBroadcast)>();
|
|
||||||
|
|
||||||
foreach (var shell in _nearbySyncshells)
|
|
||||||
{
|
|
||||||
string broadcasterName;
|
|
||||||
|
|
||||||
if (shell?.Group == null || string.IsNullOrEmpty(shell.Group.GID))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (_useTestSyncshells)
|
|
||||||
{
|
|
||||||
var displayName = !string.IsNullOrEmpty(shell.Group.Alias)
|
|
||||||
? shell.Group.Alias
|
|
||||||
: shell.Group.GID;
|
|
||||||
|
|
||||||
broadcasterName = $"{displayName} (Tester of TestWorld)";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var broadcast = broadcasts
|
|
||||||
.FirstOrDefault(b => string.Equals(b.GID, shell.Group.GID, StringComparison.Ordinal));
|
|
||||||
|
|
||||||
if (broadcast == null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var (name, address) = _dalamudUtilService.FindPlayerByNameHash(broadcast.HashedCID);
|
|
||||||
if (string.IsNullOrEmpty(name))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var worldName = _dalamudUtilService.GetWorldNameFromPlayerAddress(address);
|
|
||||||
broadcasterName = !string.IsNullOrEmpty(worldName)
|
|
||||||
? $"{name} ({worldName})"
|
|
||||||
: name;
|
|
||||||
|
|
||||||
var isSelfBroadcast = !string.IsNullOrEmpty(localHashedCid)
|
|
||||||
&& string.Equals(broadcast.HashedCID, localHashedCid, StringComparison.Ordinal);
|
|
||||||
|
|
||||||
cardData.Add((shell, broadcasterName, isSelfBroadcast));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
cardData.Add((shell, broadcasterName, false));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cardData.Count == 0)
|
|
||||||
{
|
|
||||||
ImGui.TextColored(ImGuiColors.DalamudGrey, "No nearby syncshells are being broadcasted.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_compactView)
|
|
||||||
{
|
|
||||||
DrawSyncshellGrid(cardData);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
DrawSyncshellList(cardData);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (_joinDto != null && _joinInfo != null && _joinInfo.Success)
|
|
||||||
DrawConfirmation();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawSyncshellList(List<(GroupJoinDto Shell, string BroadcasterName, bool IsSelfBroadcast)> listData)
|
|
||||||
{
|
|
||||||
const int shellsPerPage = 3;
|
|
||||||
var totalPages = (int)Math.Ceiling(listData.Count / (float)shellsPerPage);
|
|
||||||
if (totalPages <= 0)
|
|
||||||
totalPages = 1;
|
|
||||||
|
|
||||||
_syncshellPageIndex = Math.Clamp(_syncshellPageIndex, 0, totalPages - 1);
|
|
||||||
|
|
||||||
var firstIndex = _syncshellPageIndex * shellsPerPage;
|
|
||||||
var lastExclusive = Math.Min(firstIndex + shellsPerPage, listData.Count);
|
|
||||||
|
|
||||||
ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 8.0f);
|
|
||||||
ImGui.PushStyleVar(ImGuiStyleVar.FrameBorderSize, 1.0f);
|
|
||||||
|
|
||||||
for (int index = firstIndex; index < lastExclusive; index++)
|
|
||||||
{
|
|
||||||
var (shell, broadcasterName, isSelfBroadcast) = listData[index];
|
|
||||||
var broadcasterLabel = string.IsNullOrEmpty(broadcasterName)
|
|
||||||
? (isSelfBroadcast ? "You" : string.Empty)
|
|
||||||
: (isSelfBroadcast ? $"{broadcasterName} (You)" : broadcasterName);
|
|
||||||
|
|
||||||
ImGui.PushID(shell.Group.GID);
|
|
||||||
float rowHeight = 74f * ImGuiHelpers.GlobalScale;
|
|
||||||
|
|
||||||
ImGui.BeginChild($"ShellRow##{shell.Group.GID}", new Vector2(-1, rowHeight), border: true);
|
|
||||||
|
|
||||||
var displayName = !string.IsNullOrEmpty(shell.Group.Alias) ? shell.Group.Alias : shell.Group.GID;
|
|
||||||
|
|
||||||
var style = ImGui.GetStyle();
|
|
||||||
float startX = ImGui.GetCursorPosX();
|
|
||||||
float regionW = ImGui.GetContentRegionAvail().X;
|
|
||||||
float rightTxtW = ImGui.CalcTextSize(broadcasterLabel).X;
|
|
||||||
|
|
||||||
_uiSharedService.MediumText(displayName, UIColors.Get("LightlessPurple"));
|
|
||||||
if (ImGui.IsItemHovered())
|
|
||||||
ImGui.SetTooltip("Click to open profile.");
|
|
||||||
if (ImGui.IsItemClicked())
|
|
||||||
{
|
|
||||||
Mediator.Publish(new GroupProfileOpenStandaloneMessage(shell.Group));
|
|
||||||
}
|
|
||||||
|
|
||||||
float rightX = startX + regionW - rightTxtW - style.ItemSpacing.X;
|
|
||||||
ImGui.SameLine();
|
|
||||||
ImGui.SetCursorPosX(rightX);
|
|
||||||
ImGui.TextUnformatted(broadcasterLabel);
|
|
||||||
if (ImGui.IsItemHovered())
|
|
||||||
ImGui.SetTooltip("Broadcaster of the syncshell.");
|
|
||||||
|
|
||||||
UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"));
|
|
||||||
|
|
||||||
var groupProfile = _lightlessProfileManager.GetLightlessGroupProfile(shell.Group);
|
|
||||||
|
|
||||||
IReadOnlyList<ProfileTagDefinition> groupTags =
|
|
||||||
groupProfile != null && groupProfile.Tags.Count > 0
|
|
||||||
? ProfileTagService.ResolveTags(groupProfile.Tags)
|
|
||||||
: [];
|
|
||||||
|
|
||||||
var limitedTags = groupTags.Count > 3
|
|
||||||
? [.. groupTags.Take(3)]
|
|
||||||
: groupTags;
|
|
||||||
|
|
||||||
float tagScale = ImGuiHelpers.GlobalScale * 0.9f;
|
|
||||||
|
|
||||||
Vector2 rowStartLocal = ImGui.GetCursorPos();
|
|
||||||
|
|
||||||
float tagsWidth = 0f;
|
|
||||||
float tagsHeight = 0f;
|
|
||||||
|
|
||||||
if (limitedTags.Count > 0)
|
|
||||||
{
|
|
||||||
(tagsWidth, tagsHeight) = RenderProfileTagsSingleRow(limitedTags, tagScale);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ImGui.SetCursorPosX(startX);
|
|
||||||
ImGui.TextDisabled("-- No tags set --");
|
|
||||||
ImGui.Dummy(new Vector2(0, 4 * ImGuiHelpers.GlobalScale));
|
|
||||||
}
|
|
||||||
|
|
||||||
float btnBaselineY = rowStartLocal.Y;
|
|
||||||
float joinX = rowStartLocal.X + (tagsWidth > 0 ? tagsWidth + style.ItemSpacing.X : 0f);
|
|
||||||
|
|
||||||
ImGui.SetCursorPos(new Vector2(joinX, btnBaselineY));
|
|
||||||
DrawJoinButton(shell, isSelfBroadcast);
|
|
||||||
|
|
||||||
float btnHeight = ImGui.GetFrameHeightWithSpacing();
|
|
||||||
float rowHeightUsed = MathF.Max(tagsHeight, btnHeight);
|
|
||||||
|
|
||||||
ImGui.SetCursorPos(new Vector2(
|
|
||||||
rowStartLocal.X,
|
|
||||||
rowStartLocal.Y + rowHeightUsed));
|
|
||||||
|
|
||||||
ImGui.EndChild();
|
|
||||||
ImGui.PopID();
|
|
||||||
|
|
||||||
ImGui.Dummy(new Vector2(0, 2 * ImGuiHelpers.GlobalScale));
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.PopStyleVar(2);
|
|
||||||
|
|
||||||
DrawPagination(totalPages);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawSyncshellGrid(List<(GroupJoinDto Shell, string BroadcasterName, bool IsSelfBroadcast)> cardData)
|
|
||||||
{
|
|
||||||
const int shellsPerPage = 4;
|
|
||||||
var totalPages = (int)Math.Ceiling(cardData.Count / (float)shellsPerPage);
|
|
||||||
if (totalPages <= 0)
|
|
||||||
totalPages = 1;
|
|
||||||
|
|
||||||
_syncshellPageIndex = Math.Clamp(_syncshellPageIndex, 0, totalPages - 1);
|
|
||||||
|
|
||||||
var firstIndex = _syncshellPageIndex * shellsPerPage;
|
|
||||||
var lastExclusive = Math.Min(firstIndex + shellsPerPage, cardData.Count);
|
|
||||||
|
|
||||||
var avail = ImGui.GetContentRegionAvail();
|
|
||||||
var spacing = ImGui.GetStyle().ItemSpacing;
|
|
||||||
|
|
||||||
var cardWidth = (avail.X - spacing.X) / 2.0f;
|
|
||||||
var cardHeight = (avail.Y - spacing.Y - (ImGui.GetFrameHeightWithSpacing() * 2.0f)) / 2.0f;
|
|
||||||
cardHeight = MathF.Max(110f * ImGuiHelpers.GlobalScale, cardHeight);
|
|
||||||
|
|
||||||
ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 8.0f);
|
|
||||||
ImGui.PushStyleVar(ImGuiStyleVar.FrameBorderSize, 1.0f);
|
|
||||||
|
|
||||||
for (int index = firstIndex; index < lastExclusive; index++)
|
|
||||||
{
|
|
||||||
var localIndex = index - firstIndex;
|
|
||||||
var (shell, broadcasterName, isSelfBroadcast) = cardData[index];
|
|
||||||
var broadcasterLabel = string.IsNullOrEmpty(broadcasterName)
|
|
||||||
? (isSelfBroadcast ? "You" : string.Empty)
|
|
||||||
: (isSelfBroadcast ? $"{broadcasterName} (You)" : broadcasterName);
|
|
||||||
|
|
||||||
if (localIndex % 2 != 0)
|
|
||||||
ImGui.SameLine();
|
|
||||||
|
|
||||||
ImGui.PushID(shell.Group.GID);
|
|
||||||
|
|
||||||
ImGui.BeginGroup();
|
|
||||||
_ = ImGui.BeginChild("ShellCard##" + shell.Group.GID, new Vector2(cardWidth, cardHeight), border: true);
|
|
||||||
|
|
||||||
var displayName = !string.IsNullOrEmpty(shell.Group.Alias)
|
|
||||||
? shell.Group.Alias
|
|
||||||
: shell.Group.GID;
|
|
||||||
|
|
||||||
var style = ImGui.GetStyle();
|
|
||||||
float startX = ImGui.GetCursorPosX();
|
|
||||||
float availW = ImGui.GetContentRegionAvail().X;
|
|
||||||
|
|
||||||
ImGui.BeginGroup();
|
|
||||||
|
|
||||||
_uiSharedService.MediumText(displayName, UIColors.Get("LightlessPurple"));
|
|
||||||
if (ImGui.IsItemHovered())
|
|
||||||
ImGui.SetTooltip("Click to open profile.");
|
|
||||||
if (ImGui.IsItemClicked())
|
|
||||||
{
|
|
||||||
Mediator.Publish(new GroupProfileOpenStandaloneMessage(shell.Group));
|
|
||||||
}
|
|
||||||
|
|
||||||
float nameRightX = ImGui.GetItemRectMax().X;
|
|
||||||
|
|
||||||
var regionMinScreen = ImGui.GetCursorScreenPos();
|
|
||||||
float regionRightX = regionMinScreen.X + availW;
|
|
||||||
|
|
||||||
float minBroadcasterX = nameRightX + style.ItemSpacing.X;
|
|
||||||
|
|
||||||
float maxBroadcasterWidth = regionRightX - minBroadcasterX;
|
|
||||||
|
|
||||||
string broadcasterToShow = broadcasterLabel;
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(broadcasterLabel) && maxBroadcasterWidth > 0f)
|
|
||||||
{
|
|
||||||
float bcFullWidth = ImGui.CalcTextSize(broadcasterLabel).X;
|
|
||||||
string toolTip;
|
|
||||||
|
|
||||||
if (bcFullWidth > maxBroadcasterWidth)
|
|
||||||
{
|
|
||||||
broadcasterToShow = TruncateTextToWidth(broadcasterLabel, maxBroadcasterWidth);
|
|
||||||
toolTip = broadcasterLabel + 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();
|
|
||||||
|
|
||||||
UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"));
|
|
||||||
|
|
||||||
ImGui.Dummy(new Vector2(0, 6 * ImGuiHelpers.GlobalScale));
|
|
||||||
|
|
||||||
var groupProfile = _lightlessProfileManager.GetLightlessGroupProfile(shell.Group);
|
|
||||||
|
|
||||||
IReadOnlyList<ProfileTagDefinition> groupTags =
|
|
||||||
groupProfile != null && groupProfile.Tags.Count > 0
|
|
||||||
? ProfileTagService.ResolveTags(groupProfile.Tags)
|
|
||||||
: [];
|
|
||||||
|
|
||||||
float tagScale = ImGuiHelpers.GlobalScale * 0.9f;
|
|
||||||
|
|
||||||
if (groupTags.Count > 0)
|
|
||||||
{
|
|
||||||
var limitedTags = groupTags.Count > 2
|
|
||||||
? [.. groupTags.Take(2)]
|
|
||||||
: groupTags;
|
|
||||||
|
|
||||||
ImGui.SetCursorPosX(startX);
|
|
||||||
|
|
||||||
var (_, tagsHeight) = RenderProfileTagsSingleRow(limitedTags, tagScale);
|
|
||||||
|
|
||||||
ImGui.Dummy(new Vector2(0, 4 * ImGuiHelpers.GlobalScale));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ImGui.SetCursorPosX(startX);
|
|
||||||
ImGui.TextDisabled("-- No tags set --");
|
|
||||||
ImGui.Dummy(new Vector2(0, 4 * ImGuiHelpers.GlobalScale));
|
|
||||||
}
|
|
||||||
|
|
||||||
var buttonHeight = ImGui.GetFrameHeightWithSpacing();
|
|
||||||
var remainingY = ImGui.GetContentRegionAvail().Y - buttonHeight;
|
|
||||||
if (remainingY > 0)
|
|
||||||
ImGui.Dummy(new Vector2(0, remainingY));
|
|
||||||
|
|
||||||
DrawJoinButton(shell, isSelfBroadcast);
|
|
||||||
|
|
||||||
ImGui.EndChild();
|
|
||||||
ImGui.EndGroup();
|
|
||||||
|
|
||||||
ImGui.PopID();
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.Dummy(new Vector2(0, 2 * ImGuiHelpers.GlobalScale));
|
|
||||||
ImGui.PopStyleVar(2);
|
|
||||||
|
|
||||||
DrawPagination(totalPages);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawPagination(int totalPages)
|
|
||||||
{
|
|
||||||
if (totalPages > 1)
|
|
||||||
{
|
|
||||||
UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"));
|
|
||||||
|
|
||||||
var style = ImGui.GetStyle();
|
|
||||||
string pageLabel = $"Page {_syncshellPageIndex + 1}/{totalPages}";
|
|
||||||
|
|
||||||
float prevWidth = ImGui.CalcTextSize("<").X + style.FramePadding.X * 2;
|
|
||||||
float nextWidth = ImGui.CalcTextSize(">").X + style.FramePadding.X * 2;
|
|
||||||
float textWidth = ImGui.CalcTextSize(pageLabel).X;
|
|
||||||
|
|
||||||
float totalWidth = prevWidth + textWidth + nextWidth + style.ItemSpacing.X * 2;
|
|
||||||
|
|
||||||
float availWidth = ImGui.GetContentRegionAvail().X;
|
|
||||||
float offsetX = (availWidth - totalWidth) * 0.5f;
|
|
||||||
|
|
||||||
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + offsetX);
|
|
||||||
|
|
||||||
if (ImGui.Button("<##PrevSyncshellPage") && _syncshellPageIndex > 0)
|
|
||||||
_syncshellPageIndex--;
|
|
||||||
|
|
||||||
ImGui.SameLine();
|
|
||||||
ImGui.Text(pageLabel);
|
|
||||||
|
|
||||||
ImGui.SameLine();
|
|
||||||
if (ImGui.Button(">##NextSyncshellPage") && _syncshellPageIndex < totalPages - 1)
|
|
||||||
_syncshellPageIndex++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawJoinButton(GroupJoinDto shell, bool isSelfBroadcast)
|
|
||||||
{
|
|
||||||
const string visibleLabel = "Join";
|
|
||||||
var label = $"{visibleLabel}##{shell.Group.GID}";
|
|
||||||
|
|
||||||
var isAlreadyMember = _currentSyncshells.Exists(g => string.Equals(g.GID, shell.GID, StringComparison.Ordinal));
|
|
||||||
var isRecentlyJoined = _recentlyJoined.Contains(shell.GID);
|
|
||||||
|
|
||||||
Vector2 buttonSize;
|
|
||||||
|
|
||||||
if (!_compactView)
|
|
||||||
{
|
|
||||||
var style = ImGui.GetStyle();
|
|
||||||
var textSize = ImGui.CalcTextSize(visibleLabel);
|
|
||||||
|
|
||||||
var width = textSize.X + style.FramePadding.X * 20f;
|
|
||||||
buttonSize = new Vector2(width, 30f);
|
|
||||||
|
|
||||||
float availX = ImGui.GetContentRegionAvail().X;
|
|
||||||
float curX = ImGui.GetCursorPosX();
|
|
||||||
float newX = curX + (availX - buttonSize.X);
|
|
||||||
ImGui.SetCursorPosX(newX);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
buttonSize = new Vector2(-1, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isAlreadyMember && !isRecentlyJoined && !isSelfBroadcast)
|
|
||||||
{
|
|
||||||
ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("LightlessGreen"));
|
|
||||||
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, UIColors.Get("LightlessGreen").WithAlpha(0.85f));
|
|
||||||
ImGui.PushStyleColor(ImGuiCol.ButtonActive, UIColors.Get("LightlessGreen").WithAlpha(0.75f));
|
|
||||||
if (ImGui.Button(label, buttonSize))
|
|
||||||
{
|
|
||||||
_logger.LogInformation($"Join requested for Syncshell {shell.Group.GID} ({shell.Group.Alias})");
|
|
||||||
|
|
||||||
_ = Task.Run(async () =>
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var info = await _apiController.GroupJoinHashed(new GroupJoinHashedDto(
|
|
||||||
shell.Group,
|
|
||||||
shell.Password,
|
|
||||||
shell.GroupUserPreferredPermissions
|
|
||||||
)).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (info != null && info.Success)
|
|
||||||
{
|
|
||||||
_joinDto = new GroupJoinDto(shell.Group, shell.Password, shell.GroupUserPreferredPermissions);
|
|
||||||
_joinInfo = info;
|
|
||||||
_ownPermissions = _apiController.DefaultPermissions.DeepClone()!;
|
|
||||||
|
|
||||||
_logger.LogInformation($"Fetched join info for {shell.Group.GID}");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_logger.LogWarning($"Failed to join {shell.Group.GID}: info was null or unsuccessful");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, $"Join failed for {shell.Group.GID}");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("DimRed"));
|
|
||||||
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, UIColors.Get("DimRed").WithAlpha(0.85f));
|
|
||||||
ImGui.PushStyleColor(ImGuiCol.ButtonActive, UIColors.Get("DimRed").WithAlpha(0.75f));
|
|
||||||
|
|
||||||
using (ImRaii.Disabled())
|
|
||||||
{
|
|
||||||
ImGui.Button(label, buttonSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
UiSharedService.AttachToolTip(isSelfBroadcast
|
|
||||||
? "This is your own Syncshell."
|
|
||||||
: "Already a member or owner of this Syncshell.");
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.PopStyleColor(3);
|
|
||||||
}
|
|
||||||
|
|
||||||
private (float widthUsed, float rowHeight) RenderProfileTagsSingleRow(IReadOnlyList<ProfileTagDefinition> tags, float scale)
|
|
||||||
{
|
|
||||||
if (tags == null || tags.Count == 0)
|
|
||||||
return (0f, 0f);
|
|
||||||
|
|
||||||
var drawList = ImGui.GetWindowDrawList();
|
|
||||||
var style = ImGui.GetStyle();
|
|
||||||
var defaultTextColorU32 = ImGui.GetColorU32(ImGuiCol.Text);
|
|
||||||
|
|
||||||
var baseLocal = ImGui.GetCursorPos();
|
|
||||||
var baseScreen = ImGui.GetCursorScreenPos();
|
|
||||||
float availableWidth = ImGui.GetContentRegionAvail().X;
|
|
||||||
if (availableWidth <= 0f)
|
|
||||||
availableWidth = 1f;
|
|
||||||
|
|
||||||
float cursorLocalX = baseLocal.X;
|
|
||||||
float cursorScreenX = baseScreen.X;
|
|
||||||
float rowHeight = 0f;
|
|
||||||
|
|
||||||
for (int i = 0; i < tags.Count; i++)
|
|
||||||
{
|
|
||||||
var tag = tags[i];
|
|
||||||
if (!tag.HasContent)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var tagSize = ProfileTagRenderer.MeasureTag(tag, scale, style, _tagBackgroundColor, _tagBorderColor, defaultTextColorU32, _seResolvedSegments, GetIconWrap, _logger);
|
|
||||||
|
|
||||||
float tagWidth = tagSize.X;
|
|
||||||
float tagHeight = tagSize.Y;
|
|
||||||
|
|
||||||
if (cursorLocalX > baseLocal.X && cursorLocalX + tagWidth > baseLocal.X + availableWidth)
|
|
||||||
break;
|
|
||||||
|
|
||||||
var tagScreenPos = new Vector2(cursorScreenX, baseScreen.Y);
|
|
||||||
ImGui.SetCursorScreenPos(tagScreenPos);
|
|
||||||
ImGui.InvisibleButton($"##profileTagInline_{i}", tagSize);
|
|
||||||
|
|
||||||
ProfileTagRenderer.RenderTag(tag, tagScreenPos, scale, drawList, style, _tagBackgroundColor, _tagBorderColor, defaultTextColorU32, _seResolvedSegments, GetIconWrap, _logger);
|
|
||||||
|
|
||||||
cursorLocalX += tagWidth + style.ItemSpacing.X;
|
|
||||||
cursorScreenX += tagWidth + style.ItemSpacing.X;
|
|
||||||
rowHeight = MathF.Max(rowHeight, tagHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.SetCursorPos(new Vector2(baseLocal.X, baseLocal.Y + rowHeight));
|
|
||||||
|
|
||||||
float widthUsed = cursorLocalX - baseLocal.X;
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (_uiSharedService.TryGetIcon(iconId, out var wrap) && wrap != null)
|
|
||||||
return wrap;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogDebug(ex, "Failed to resolve icon {IconId} for profile tags", iconId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawConfirmation()
|
|
||||||
{
|
|
||||||
if (_joinDto != null && _joinInfo != null)
|
|
||||||
{
|
|
||||||
ImGui.Separator();
|
|
||||||
ImGui.TextUnformatted($"Join Syncshell: {_joinDto.Group.AliasOrGID} by {_joinInfo.OwnerAliasOrUID}");
|
|
||||||
ImGuiHelpers.ScaledDummy(2f);
|
|
||||||
ImGui.TextUnformatted("Suggested Syncshell Permissions:");
|
|
||||||
|
|
||||||
DrawPermissionRow("Sounds", _joinInfo.GroupPermissions.IsPreferDisableSounds(), _ownPermissions.DisableGroupSounds, v => _ownPermissions.DisableGroupSounds = v);
|
|
||||||
DrawPermissionRow("Animations", _joinInfo.GroupPermissions.IsPreferDisableAnimations(), _ownPermissions.DisableGroupAnimations, v => _ownPermissions.DisableGroupAnimations = v);
|
|
||||||
DrawPermissionRow("VFX", _joinInfo.GroupPermissions.IsPreferDisableVFX(), _ownPermissions.DisableGroupVFX, v => _ownPermissions.DisableGroupVFX = v);
|
|
||||||
|
|
||||||
ImGui.NewLine();
|
|
||||||
ImGui.NewLine();
|
|
||||||
|
|
||||||
if (_uiSharedService.IconTextButton(Dalamud.Interface.FontAwesomeIcon.Plus, $"Finalize and join {_joinDto.Group.AliasOrGID}"))
|
|
||||||
{
|
|
||||||
var finalPermissions = GroupUserPreferredPermissions.NoneSet;
|
|
||||||
finalPermissions.SetDisableSounds(_ownPermissions.DisableGroupSounds);
|
|
||||||
finalPermissions.SetDisableAnimations(_ownPermissions.DisableGroupAnimations);
|
|
||||||
finalPermissions.SetDisableVFX(_ownPermissions.DisableGroupVFX);
|
|
||||||
|
|
||||||
_ = _apiController.GroupJoinFinalize(new GroupJoinDto(_joinDto.Group, _joinDto.Password, finalPermissions));
|
|
||||||
|
|
||||||
_recentlyJoined.Add(_joinDto.Group.GID);
|
|
||||||
|
|
||||||
_joinDto = null;
|
|
||||||
_joinInfo = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawPermissionRow(string label, bool suggested, bool current, Action<bool> apply)
|
|
||||||
{
|
|
||||||
ImGui.AlignTextToFramePadding();
|
|
||||||
ImGui.TextUnformatted($"- {label}");
|
|
||||||
|
|
||||||
ImGui.SameLine(150 * ImGuiHelpers.GlobalScale);
|
|
||||||
ImGui.TextUnformatted("Current:");
|
|
||||||
ImGui.SameLine();
|
|
||||||
_uiSharedService.BooleanToColoredIcon(!current);
|
|
||||||
|
|
||||||
ImGui.SameLine(300 * ImGuiHelpers.GlobalScale);
|
|
||||||
ImGui.TextUnformatted("Suggested:");
|
|
||||||
ImGui.SameLine();
|
|
||||||
_uiSharedService.BooleanToColoredIcon(!suggested);
|
|
||||||
|
|
||||||
ImGui.SameLine(450 * ImGuiHelpers.GlobalScale);
|
|
||||||
using var id = ImRaii.PushId(label);
|
|
||||||
if (current != suggested)
|
|
||||||
{
|
|
||||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowRight, "Apply"))
|
|
||||||
apply(suggested);
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.NewLine();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task RefreshSyncshellsAsync(string? gid = null)
|
|
||||||
{
|
|
||||||
var syncshellBroadcasts = _broadcastScannerService.GetActiveSyncshellBroadcasts();
|
|
||||||
var snapshot = _pairUiService.GetSnapshot();
|
|
||||||
_currentSyncshells = [.. snapshot.GroupPairs.Keys];
|
|
||||||
|
|
||||||
_recentlyJoined.RemoveWhere(gid =>
|
|
||||||
_currentSyncshells.Exists(s => string.Equals(s.GID, gid, StringComparison.Ordinal)));
|
|
||||||
|
|
||||||
List<GroupJoinDto>? updatedList = [];
|
|
||||||
|
|
||||||
if (_useTestSyncshells)
|
|
||||||
{
|
|
||||||
updatedList = BuildTestSyncshells();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (syncshellBroadcasts.Count == 0)
|
|
||||||
{
|
|
||||||
ClearSyncshells();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var groups = await _apiController.GetBroadcastedGroups(syncshellBroadcasts)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
updatedList = groups?.DistinctBy(g => g.Group.GID).ToList();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Failed to refresh broadcasted syncshells.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updatedList == null || updatedList.Count == 0)
|
|
||||||
{
|
|
||||||
ClearSyncshells();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (gid != null && _recentlyJoined.Contains(gid))
|
|
||||||
{
|
|
||||||
_recentlyJoined.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
var previousGid = GetSelectedGid();
|
|
||||||
|
|
||||||
_nearbySyncshells.Clear();
|
|
||||||
_nearbySyncshells.AddRange(updatedList);
|
|
||||||
|
|
||||||
if (previousGid != null)
|
|
||||||
{
|
|
||||||
var newIndex = _nearbySyncshells.FindIndex(s =>
|
|
||||||
string.Equals(s.Group.GID, previousGid, StringComparison.Ordinal));
|
|
||||||
|
|
||||||
if (newIndex >= 0)
|
|
||||||
{
|
|
||||||
_selectedNearbyIndex = newIndex;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ClearSelection();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<GroupJoinDto> BuildTestSyncshells()
|
|
||||||
{
|
|
||||||
var testGroup1 = new GroupData("TEST-ALPHA", "Alpha Shell");
|
|
||||||
var testGroup2 = new GroupData("TEST-BETA", "Beta Shell");
|
|
||||||
var testGroup3 = new GroupData("TEST-GAMMA", "Gamma Shell");
|
|
||||||
var testGroup4 = new GroupData("TEST-DELTA", "Delta Shell");
|
|
||||||
var testGroup5 = new GroupData("TEST-CHARLIE", "Charlie Shell");
|
|
||||||
var testGroup6 = new GroupData("TEST-OMEGA", "Omega Shell");
|
|
||||||
var testGroup7 = new GroupData("TEST-POINT", "Point Shell");
|
|
||||||
var testGroup8 = new GroupData("TEST-HOTEL", "Hotel Shell");
|
|
||||||
|
|
||||||
return
|
|
||||||
[
|
|
||||||
new(testGroup1, "", GroupUserPreferredPermissions.NoneSet),
|
|
||||||
new(testGroup2, "", GroupUserPreferredPermissions.NoneSet),
|
|
||||||
new(testGroup3, "", GroupUserPreferredPermissions.NoneSet),
|
|
||||||
new(testGroup4, "", GroupUserPreferredPermissions.NoneSet),
|
|
||||||
new(testGroup5, "", GroupUserPreferredPermissions.NoneSet),
|
|
||||||
new(testGroup6, "", GroupUserPreferredPermissions.NoneSet),
|
|
||||||
new(testGroup7, "", GroupUserPreferredPermissions.NoneSet),
|
|
||||||
new(testGroup8, "", GroupUserPreferredPermissions.NoneSet),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ClearSyncshells()
|
|
||||||
{
|
|
||||||
if (_nearbySyncshells.Count == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
_nearbySyncshells.Clear();
|
|
||||||
ClearSelection();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ClearSelection()
|
|
||||||
{
|
|
||||||
_selectedNearbyIndex = -1;
|
|
||||||
_syncshellPageIndex = 0;
|
|
||||||
_joinDto = null;
|
|
||||||
_joinInfo = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private string? GetSelectedGid()
|
|
||||||
{
|
|
||||||
if (_selectedNearbyIndex < 0 || _selectedNearbyIndex >= _nearbySyncshells.Count)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return _nearbySyncshells[_selectedNearbyIndex].Group.GID;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -162,24 +162,32 @@ public class TopTabMenu
|
|||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
using (ImRaii.PushFont(UiBuilder.IconFont))
|
using (ImRaii.PushFont(UiBuilder.IconFont))
|
||||||
{
|
{
|
||||||
var x = ImGui.GetCursorScreenPos();
|
|
||||||
if (ImGui.Button(FontAwesomeIcon.Compass.ToIconString(), buttonSize))
|
if (ImGui.Button(FontAwesomeIcon.Compass.ToIconString(), buttonSize))
|
||||||
{
|
{
|
||||||
TabSelection = TabSelection == SelectedTab.Lightfinder ? SelectedTab.None : SelectedTab.Lightfinder;
|
_lightlessMediator.Publish(new UiToggleMessage(typeof(LightFinderUI)));
|
||||||
}
|
}
|
||||||
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem) || ImGui.IsItemActive())
|
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem) || ImGui.IsItemActive())
|
||||||
{
|
{
|
||||||
Selune.RegisterHighlight(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), SeluneHighlightMode.Both, true, buttonBorderThickness, exactSize: true, clipToElement: true, roundingOverride: buttonRounding);
|
Selune.RegisterHighlight(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), SeluneHighlightMode.Both, true, buttonBorderThickness, exactSize: true, clipToElement: true, roundingOverride: buttonRounding);
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui.SameLine();
|
|
||||||
var xAfter = ImGui.GetCursorScreenPos();
|
|
||||||
if (TabSelection == SelectedTab.Lightfinder)
|
|
||||||
drawList.AddLine(x with { Y = x.Y + buttonSize.Y + spacing.Y },
|
|
||||||
xAfter with { Y = xAfter.Y + buttonSize.Y + spacing.Y, X = xAfter.X - spacing.X },
|
|
||||||
underlineColor, 2);
|
|
||||||
}
|
}
|
||||||
UiSharedService.AttachToolTip("Lightfinder");
|
|
||||||
|
var nearbyCount = GetNearbySyncshellCount();
|
||||||
|
if (nearbyCount > 0)
|
||||||
|
{
|
||||||
|
var buttonMax = ImGui.GetItemRectMax();
|
||||||
|
var badgeRadius = 8f * ImGuiHelpers.GlobalScale;
|
||||||
|
var badgeCenter = new Vector2(buttonMax.X - badgeRadius * 1.3f, buttonMax.Y - buttonSize.Y + badgeRadius * 0.5f);
|
||||||
|
var badgeText = nearbyCount > 99 ? "99+" : nearbyCount.ToString();
|
||||||
|
var textSize = ImGui.CalcTextSize(badgeText);
|
||||||
|
|
||||||
|
drawList.AddCircleFilled(badgeCenter, badgeRadius + 1f, ImGui.GetColorU32(new Vector4(0, 0, 0, 0.6f)));
|
||||||
|
drawList.AddCircleFilled(badgeCenter, badgeRadius, ImGui.GetColorU32(UIColors.Get("LightlessPurple")));
|
||||||
|
|
||||||
|
var textPos = new Vector2(badgeCenter.X - textSize.X * 0.45f, badgeCenter.Y - textSize.Y * 0.55f);
|
||||||
|
drawList.AddText(textPos, ImGui.GetColorU32(new Vector4(1, 1, 1, 1)), badgeText);
|
||||||
|
}
|
||||||
|
UiSharedService.AttachToolTip(nearbyCount > 0 ? $"Lightfinder ({nearbyCount} nearby)" : "Open Lightfinder");
|
||||||
|
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
using (ImRaii.PushFont(UiBuilder.IconFont))
|
using (ImRaii.PushFont(UiBuilder.IconFont))
|
||||||
@@ -234,10 +242,7 @@ public class TopTabMenu
|
|||||||
DrawSyncshellMenu(availableWidth, spacing.X);
|
DrawSyncshellMenu(availableWidth, spacing.X);
|
||||||
DrawGlobalSyncshellButtons(availableWidth, spacing.X);
|
DrawGlobalSyncshellButtons(availableWidth, spacing.X);
|
||||||
}
|
}
|
||||||
else if (TabSelection == SelectedTab.Lightfinder)
|
|
||||||
{
|
|
||||||
DrawLightfinderMenu(availableWidth, spacing.X);
|
|
||||||
}
|
|
||||||
else if (TabSelection == SelectedTab.UserConfig)
|
else if (TabSelection == SelectedTab.UserConfig)
|
||||||
{
|
{
|
||||||
DrawUserConfig(availableWidth, spacing.X);
|
DrawUserConfig(availableWidth, spacing.X);
|
||||||
@@ -776,53 +781,22 @@ public class TopTabMenu
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawLightfinderMenu(float availableWidth, float spacingX)
|
private int GetNearbySyncshellCount()
|
||||||
{
|
|
||||||
var buttonX = (availableWidth - (spacingX)) / 2f;
|
|
||||||
|
|
||||||
var lightFinderLabel = GetLightfinderFinderLabel();
|
|
||||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.PersonCirclePlus, lightFinderLabel, buttonX, center: true))
|
|
||||||
{
|
|
||||||
_lightlessMediator.Publish(new UiToggleMessage(typeof(LightFinderUI)));
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.SameLine();
|
|
||||||
|
|
||||||
var syncshellFinderLabel = GetSyncshellFinderLabel();
|
|
||||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Globe, syncshellFinderLabel, buttonX, center: true))
|
|
||||||
{
|
|
||||||
_lightlessMediator.Publish(new UiToggleMessage(typeof(SyncshellFinderUI)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GetLightfinderFinderLabel()
|
|
||||||
{
|
|
||||||
string label = "Lightfinder";
|
|
||||||
|
|
||||||
if (_lightFinderService.IsBroadcasting)
|
|
||||||
{
|
|
||||||
var hashExclude = _dalamudUtilService.GetCID().ToString().GetHash256();
|
|
||||||
var nearbyCount = _lightFinderScannerService.GetActiveBroadcasts(hashExclude).Count;
|
|
||||||
return $"{label} ({nearbyCount})";
|
|
||||||
}
|
|
||||||
|
|
||||||
return label;
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GetSyncshellFinderLabel()
|
|
||||||
{
|
{
|
||||||
if (!_lightFinderService.IsBroadcasting)
|
if (!_lightFinderService.IsBroadcasting)
|
||||||
return "Syncshell Finder";
|
return 0;
|
||||||
|
|
||||||
var nearbyCount = _lightFinderScannerService
|
var myHashedCid = _dalamudUtilService.GetCID().ToString().GetHash256();
|
||||||
.GetActiveSyncshellBroadcasts(excludeLocal: true)
|
|
||||||
.Where(b => !string.IsNullOrEmpty(b.GID))
|
return _lightFinderScannerService
|
||||||
|
.GetActiveSyncshellBroadcasts()
|
||||||
|
.Where(b =>
|
||||||
|
!string.IsNullOrEmpty(b.GID) &&
|
||||||
|
!string.Equals(b.HashedCID, myHashedCid, StringComparison.Ordinal))
|
||||||
.Select(b => b.GID!)
|
.Select(b => b.GID!)
|
||||||
.Distinct(StringComparer.Ordinal)
|
.Distinct(StringComparer.Ordinal)
|
||||||
.Count();
|
.Count();
|
||||||
|
|
||||||
return nearbyCount > 0 ? $"Syncshell Finder ({nearbyCount})" : "Syncshell Finder";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawUserConfig(float availableWidth, float spacingX)
|
private void DrawUserConfig(float availableWidth, float spacingX)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ namespace LightlessSync.UI
|
|||||||
{
|
{
|
||||||
internal static class UIColors
|
internal static class UIColors
|
||||||
{
|
{
|
||||||
private static readonly Dictionary<string, string> DefaultHexColors = new(StringComparer.OrdinalIgnoreCase)
|
private static readonly Dictionary<string, string> _defaultHexColors = new(StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
{
|
||||||
{ "LightlessPurple", "#ad8af5" },
|
{ "LightlessPurple", "#ad8af5" },
|
||||||
{ "LightlessPurpleActive", "#be9eff" },
|
{ "LightlessPurpleActive", "#be9eff" },
|
||||||
@@ -31,6 +31,12 @@ namespace LightlessSync.UI
|
|||||||
|
|
||||||
{ "ProfileBodyGradientTop", "#2f283fff" },
|
{ "ProfileBodyGradientTop", "#2f283fff" },
|
||||||
{ "ProfileBodyGradientBottom", "#372d4d00" },
|
{ "ProfileBodyGradientBottom", "#372d4d00" },
|
||||||
|
|
||||||
|
{ "HeaderGradientTop", "#140D26FF" },
|
||||||
|
{ "HeaderGradientBottom", "#1F1433FF" },
|
||||||
|
|
||||||
|
{ "HeaderStaticStar", "#FFFFFFFF" },
|
||||||
|
{ "HeaderShootingStar", "#66CCFFFF" },
|
||||||
};
|
};
|
||||||
|
|
||||||
private static LightlessConfigService? _configService;
|
private static LightlessConfigService? _configService;
|
||||||
@@ -45,7 +51,7 @@ namespace LightlessSync.UI
|
|||||||
if (_configService?.Current.CustomUIColors.TryGetValue(name, out var customColorHex) == true)
|
if (_configService?.Current.CustomUIColors.TryGetValue(name, out var customColorHex) == true)
|
||||||
return HexToRgba(customColorHex);
|
return HexToRgba(customColorHex);
|
||||||
|
|
||||||
if (!DefaultHexColors.TryGetValue(name, out var hex))
|
if (!_defaultHexColors.TryGetValue(name, out var hex))
|
||||||
throw new ArgumentException($"Color '{name}' not found in UIColors.", nameof(name));
|
throw new ArgumentException($"Color '{name}' not found in UIColors.", nameof(name));
|
||||||
|
|
||||||
return HexToRgba(hex);
|
return HexToRgba(hex);
|
||||||
@@ -53,7 +59,7 @@ namespace LightlessSync.UI
|
|||||||
|
|
||||||
public static void Set(string name, Vector4 color)
|
public static void Set(string name, Vector4 color)
|
||||||
{
|
{
|
||||||
if (!DefaultHexColors.ContainsKey(name))
|
if (!_defaultHexColors.ContainsKey(name))
|
||||||
throw new ArgumentException($"Color '{name}' not found in UIColors.", nameof(name));
|
throw new ArgumentException($"Color '{name}' not found in UIColors.", nameof(name));
|
||||||
|
|
||||||
if (_configService != null)
|
if (_configService != null)
|
||||||
@@ -83,7 +89,7 @@ namespace LightlessSync.UI
|
|||||||
|
|
||||||
public static Vector4 GetDefault(string name)
|
public static Vector4 GetDefault(string name)
|
||||||
{
|
{
|
||||||
if (!DefaultHexColors.TryGetValue(name, out var hex))
|
if (!_defaultHexColors.TryGetValue(name, out var hex))
|
||||||
throw new ArgumentException($"Color '{name}' not found in UIColors.", nameof(name));
|
throw new ArgumentException($"Color '{name}' not found in UIColors.", nameof(name));
|
||||||
|
|
||||||
return HexToRgba(hex);
|
return HexToRgba(hex);
|
||||||
@@ -96,7 +102,7 @@ namespace LightlessSync.UI
|
|||||||
|
|
||||||
public static IEnumerable<string> GetColorNames()
|
public static IEnumerable<string> GetColorNames()
|
||||||
{
|
{
|
||||||
return DefaultHexColors.Keys;
|
return _defaultHexColors.Keys;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Vector4 HexToRgba(string hexColor)
|
public static Vector4 HexToRgba(string hexColor)
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
|
|||||||
logger.LogInformation("UpdateNotesUi constructor called");
|
logger.LogInformation("UpdateNotesUi constructor called");
|
||||||
_uiShared = uiShared;
|
_uiShared = uiShared;
|
||||||
_configService = configService;
|
_configService = configService;
|
||||||
|
_animatedHeader.EnableParticles = _configService.Current.EnableParticleEffects;
|
||||||
|
|
||||||
RespectCloseHotkey = true;
|
RespectCloseHotkey = true;
|
||||||
ShowCloseButton = true;
|
ShowCloseButton = true;
|
||||||
@@ -48,7 +49,8 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
|
|||||||
ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoMove;
|
ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoMove;
|
||||||
|
|
||||||
PositionCondition = ImGuiCond.Always;
|
PositionCondition = ImGuiCond.Always;
|
||||||
|
|
||||||
|
|
||||||
WindowBuilder.For(this)
|
WindowBuilder.For(this)
|
||||||
.AllowPinning(false)
|
.AllowPinning(false)
|
||||||
.AllowClickthrough(false)
|
.AllowClickthrough(false)
|
||||||
|
|||||||
@@ -200,5 +200,21 @@ public partial class ApiController
|
|||||||
|
|
||||||
await UserPushData(new(visibleCharacters, character, censusDto)).ConfigureAwait(false);
|
await UserPushData(new(visibleCharacters, character, censusDto)).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task UpdateLocation(LocationDto locationDto, bool offline = false)
|
||||||
|
{
|
||||||
|
if (!IsConnected) return;
|
||||||
|
await _lightlessHub!.SendAsync(nameof(UpdateLocation), locationDto, offline).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
public async Task<(List<LocationWithTimeDto>, List<SharingStatusDto>)> RequestAllLocationInfo()
|
||||||
|
{
|
||||||
|
if (!IsConnected) return ([],[]);
|
||||||
|
return await _lightlessHub!.InvokeAsync<(List<LocationWithTimeDto>, List<SharingStatusDto>)>(nameof(RequestAllLocationInfo)).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
public async Task<bool> ToggleLocationSharing(LocationSharingToggleDto dto)
|
||||||
|
{
|
||||||
|
if (!IsConnected) return false;
|
||||||
|
return await _lightlessHub!.InvokeAsync<bool>(nameof(ToggleLocationSharing), dto).ConfigureAwait(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#pragma warning restore MA0040
|
#pragma warning restore MA0040
|
||||||
@@ -259,6 +259,13 @@ public partial class ApiController
|
|||||||
ExecuteSafely(() => Mediator.Publish(new GPoseLobbyReceiveWorldData(userData, worldData)));
|
ExecuteSafely(() => Mediator.Publish(new GPoseLobbyReceiveWorldData(userData, worldData)));
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task Client_SendLocationToClient(LocationDto locationDto, DateTimeOffset expireAt)
|
||||||
|
{
|
||||||
|
Logger.LogDebug($"{nameof(Client_SendLocationToClient)}: {locationDto.User} {expireAt}");
|
||||||
|
ExecuteSafely(() => Mediator.Publish(new LocationSharingMessage(locationDto.User, locationDto.Location, expireAt)));
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
public void OnDownloadReady(Action<Guid> act)
|
public void OnDownloadReady(Action<Guid> act)
|
||||||
{
|
{
|
||||||
@@ -441,6 +448,12 @@ public partial class ApiController
|
|||||||
_lightlessHub!.On(nameof(Client_GposeLobbyPushWorldData), act);
|
_lightlessHub!.On(nameof(Client_GposeLobbyPushWorldData), act);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void OnReceiveLocation(Action<LocationDto, DateTimeOffset> act)
|
||||||
|
{
|
||||||
|
if (_initialized) return;
|
||||||
|
_lightlessHub!.On(nameof(Client_SendLocationToClient), act);
|
||||||
|
}
|
||||||
|
|
||||||
private void ExecuteSafely(Action act)
|
private void ExecuteSafely(Action act)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -606,6 +606,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
|
|||||||
OnGposeLobbyPushCharacterData((dto) => _ = Client_GposeLobbyPushCharacterData(dto));
|
OnGposeLobbyPushCharacterData((dto) => _ = Client_GposeLobbyPushCharacterData(dto));
|
||||||
OnGposeLobbyPushPoseData((dto, data) => _ = Client_GposeLobbyPushPoseData(dto, data));
|
OnGposeLobbyPushPoseData((dto, data) => _ = Client_GposeLobbyPushPoseData(dto, data));
|
||||||
OnGposeLobbyPushWorldData((dto, data) => _ = Client_GposeLobbyPushWorldData(dto, data));
|
OnGposeLobbyPushWorldData((dto, data) => _ = Client_GposeLobbyPushWorldData(dto, data));
|
||||||
|
OnReceiveLocation((dto, time) => _ = Client_SendLocationToClient(dto, time));
|
||||||
|
|
||||||
_healthCheckTokenSource?.Cancel();
|
_healthCheckTokenSource?.Cancel();
|
||||||
_healthCheckTokenSource?.Dispose();
|
_healthCheckTokenSource?.Dispose();
|
||||||
@@ -774,5 +775,6 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
|
|||||||
|
|
||||||
ServerState = state;
|
ServerState = state;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
#pragma warning restore MA0040
|
#pragma warning restore MA0040
|
||||||
|
|||||||
Reference in New Issue
Block a user